11from datetime import date , datetime , timedelta
2+ from logging import getLogger
23from typing import TYPE_CHECKING , List
34
45from dateutil .relativedelta import relativedelta
56from django .conf import settings
6- from django .db .models import Sum
7+ from django .db .models import Q , Sum
78from django .utils import timezone
89from rest_framework .exceptions import NotFound
910
11+ from app_analytics import constants
1012from app_analytics .dataclasses import FeatureEvaluationData , UsageData
1113from app_analytics .influxdb_wrapper import (
1214 get_events_for_organisation ,
1719from app_analytics .influxdb_wrapper import (
1820 get_usage_data as get_usage_data_from_influxdb ,
1921)
22+ from app_analytics .mappers import map_annotated_api_usage_buckets_to_usage_data
2023from app_analytics .models import (
2124 APIUsageBucket ,
2225 FeatureEvaluationBucket ,
23- Resource ,
2426)
27+ from app_analytics .types import Labels , PeriodType
2528from environments .models import Environment
2629from features .models import Feature
27- from organisations .models import Organisation
30+ from organisations .models import Organisation , OrganisationSubscriptionInformationCache
2831
29- from . import constants
30- from .types import PERIOD_TYPE
32+ logger = getLogger (__name__ )
3133
3234
3335def get_usage_data (
3436 organisation : Organisation ,
3537 environment_id : int | None = None ,
3638 project_id : int | None = None ,
37- period : PERIOD_TYPE | None = None ,
39+ period : PeriodType | None = None ,
40+ labels_filter : Labels | None = None ,
3841) -> list [UsageData ]:
39- now = timezone . now ()
40- date_start = date_stop = None
41- sub_cache = getattr ( organisation , "subscription_information_cache" , None )
42+ sub_cache = OrganisationSubscriptionInformationCache . objects . filter (
43+ organisation = organisation
44+ ). first ( )
4245
43- is_subscription_valid = (
44- sub_cache is not None and sub_cache .is_billing_terms_dates_set ()
46+ date_start , date_stop = _get_start_date_and_stop_date_for_subscribed_organisation (
47+ sub_cache = sub_cache ,
48+ period = period ,
4549 )
4650
47- if period in (constants .CURRENT_BILLING_PERIOD , constants .PREVIOUS_BILLING_PERIOD ):
48- if not is_subscription_valid :
49- raise NotFound ("No billing periods found for this organisation." )
50-
51- if TYPE_CHECKING :
52- assert sub_cache is not None
53-
54- match period :
55- case constants .CURRENT_BILLING_PERIOD :
56- starts_at = sub_cache .current_billing_term_starts_at or now - timedelta (
57- days = 30
58- )
59- month_delta = relativedelta (now , starts_at ).months
60- date_start = relativedelta (months = month_delta ) + starts_at
61- date_stop = now
62-
63- case constants .PREVIOUS_BILLING_PERIOD :
64- starts_at = sub_cache .current_billing_term_starts_at or now - timedelta (
65- days = 30
66- )
67- month_delta = relativedelta (now , starts_at ).months - 1
68- month_delta += relativedelta (now , starts_at ).years * 12
69- date_start = relativedelta (months = month_delta ) + starts_at
70- date_stop = relativedelta (months = month_delta + 1 ) + starts_at
71- case constants .NINETY_DAY_PERIOD :
72- date_start = now - relativedelta (days = 90 )
73- date_stop = now
74-
7551 if settings .USE_POSTGRES_FOR_ANALYTICS :
76- kwargs = {
77- "organisation" : organisation ,
78- "environment_id" : environment_id ,
79- "project_id" : project_id ,
80- }
81- if date_start :
82- assert date_stop
83- kwargs ["date_start" ] = date_start # type: ignore[assignment]
84- kwargs ["date_stop" ] = date_stop # type: ignore[assignment]
85-
86- return get_usage_data_from_local_db (** kwargs ) # type: ignore[arg-type]
87-
88- kwargs = {
89- "organisation_id" : organisation .id ,
90- "environment_id" : environment_id ,
91- "project_id" : project_id ,
92- }
52+ return get_usage_data_from_local_db (
53+ organisation = organisation ,
54+ environment_id = environment_id ,
55+ project_id = project_id ,
56+ date_start = date_start ,
57+ date_stop = date_stop ,
58+ labels_filter = labels_filter ,
59+ )
9360
94- if date_start :
95- assert date_stop
96- kwargs ["date_start" ] = date_start # type: ignore[assignment]
97- kwargs ["date_stop" ] = date_stop # type: ignore[assignment]
61+ if settings .INFLUXDB_TOKEN :
62+ return get_usage_data_from_influxdb (
63+ organisation_id = organisation .id ,
64+ environment_id = environment_id ,
65+ project_id = project_id ,
66+ date_start = date_start ,
67+ date_stop = date_stop ,
68+ labels_filter = labels_filter ,
69+ )
9870
99- return get_usage_data_from_influxdb (** kwargs ) # type: ignore[arg-type]
71+ logger .warning (constants .NO_ANALYTICS_DATABASE_CONFIGURED_WARNING )
72+ return []
10073
10174
10275def get_usage_data_from_local_db (
@@ -105,6 +78,7 @@ def get_usage_data_from_local_db(
10578 project_id : int | None = None ,
10679 date_start : datetime | None = None ,
10780 date_stop : datetime | None = None ,
81+ labels_filter : Labels | None = None ,
10882) -> List [UsageData ]:
10983 if date_start is None :
11084 date_start = timezone .now () - timedelta (days = 30 )
@@ -127,28 +101,20 @@ def get_usage_data_from_local_db(
127101 if environment_id :
128102 qs = qs .filter (environment_id = environment_id )
129103
104+ if labels_filter :
105+ qs = qs .filter (labels__contains = labels_filter )
106+
130107 qs = (
131108 qs .filter ( # type: ignore[assignment]
132109 created_at__date__lte = date_stop ,
133110 created_at__date__gt = date_start ,
134111 )
135112 .order_by ("created_at__date" )
136- .values ("created_at__date" , "resource" )
113+ .values ("created_at__date" , "resource" , "labels" )
137114 .annotate (count = Sum ("total_count" ))
138115 )
139- data_by_day = {}
140- for row in qs : # TODO Write proper mappers for this?
141- day = row ["created_at__date" ]
142- if day not in data_by_day :
143- data_by_day [day ] = UsageData (day = day )
144- if column_name := Resource (row ["resource" ]).column_name :
145- setattr (
146- data_by_day [day ],
147- column_name ,
148- row ["count" ],
149- )
150116
151- return data_by_day . values () # type: ignore[return-value]
117+ return map_annotated_api_usage_buckets_to_usage_data ( qs )
152118
153119
154120def get_total_events_count (organisation ) -> int : # type: ignore[no-untyped-def]
@@ -168,30 +134,50 @@ def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
168134
169135
170136def get_feature_evaluation_data (
171- feature : Feature , environment_id : int , period : int = 30
137+ feature : Feature ,
138+ environment_id : int ,
139+ period : int = 30 ,
140+ labels_filter : Labels | None = None ,
172141) -> List [FeatureEvaluationData ]:
173142 if settings .USE_POSTGRES_FOR_ANALYTICS :
174143 return get_feature_evaluation_data_from_local_db (
175- feature , environment_id , period
144+ feature = feature ,
145+ environment_id = environment_id ,
146+ period = period ,
147+ labels_filter = labels_filter ,
148+ )
149+
150+ if settings .INFLUXDB_TOKEN :
151+ return get_feature_evaluation_data_from_influxdb (
152+ feature_name = feature .name ,
153+ environment_id = environment_id ,
154+ period = f"{ period } d" ,
155+ labels_filter = labels_filter ,
176156 )
177- return get_feature_evaluation_data_from_influxdb (
178- feature_name = feature . name , environment_id = environment_id , period = f" { period } d"
179- )
157+
158+ logger . warning ( constants . NO_ANALYTICS_DATABASE_CONFIGURED_WARNING )
159+ return []
180160
181161
182162def get_feature_evaluation_data_from_local_db (
183- feature : Feature , environment_id : int , period : int = 30
163+ feature : Feature ,
164+ environment_id : int ,
165+ period : int = 30 ,
166+ labels_filter : Labels | None = None ,
184167) -> List [FeatureEvaluationData ]:
168+ filter = Q (
169+ environment_id = environment_id ,
170+ bucket_size = constants .ANALYTICS_READ_BUCKET_SIZE ,
171+ feature_name = feature .name ,
172+ created_at__date__lte = timezone .now (),
173+ created_at__date__gt = timezone .now () - timedelta (days = period ),
174+ )
175+ if labels_filter :
176+ filter &= Q (labels__contains = labels_filter )
185177 feature_evaluation_data = (
186- FeatureEvaluationBucket .objects .filter (
187- environment_id = environment_id ,
188- bucket_size = constants .ANALYTICS_READ_BUCKET_SIZE ,
189- feature_name = feature .name ,
190- created_at__date__lte = timezone .now (),
191- created_at__date__gt = timezone .now () - timedelta (days = period ),
192- )
178+ FeatureEvaluationBucket .objects .filter (filter )
193179 .order_by ("created_at__date" )
194- .values ("created_at__date" , "feature_name" , "environment_id" )
180+ .values ("created_at__date" , "feature_name" , "environment_id" , "labels" )
195181 .annotate (count = Sum ("total_count" ))
196182 )
197183 usage_list = []
@@ -200,15 +186,61 @@ def get_feature_evaluation_data_from_local_db(
200186 FeatureEvaluationData (
201187 day = data ["created_at__date" ],
202188 count = data ["count" ],
189+ labels = data ["labels" ],
203190 )
204191 )
205192 return usage_list
206193
207194
208- def _get_environment_ids_for_org (organisation ) -> List [int ]: # type: ignore[no-untyped-def]
195+ def _get_environment_ids_for_org (organisation : Organisation ) -> list [int ]:
209196 # We need to do this to prevent Django from generating a query that
210197 # references the environments and projects tables,
211198 # as they do not exist in the analytics database.
212199 return [
213200 e .id for e in Environment .objects .filter (project__organisation = organisation )
214201 ]
202+
203+
204+ def _get_start_date_and_stop_date_for_subscribed_organisation (
205+ sub_cache : OrganisationSubscriptionInformationCache | None ,
206+ period : PeriodType | None = None ,
207+ ) -> tuple [datetime | None , datetime | None ]:
208+ """
209+ Populate start and stop date for the given period
210+ from the organisation's subscription information.
211+ """
212+ if period in {constants .CURRENT_BILLING_PERIOD , constants .PREVIOUS_BILLING_PERIOD }:
213+ if not (sub_cache and sub_cache .is_billing_terms_dates_set ()):
214+ raise NotFound ("No billing periods found for this organisation." )
215+
216+ if TYPE_CHECKING :
217+ assert sub_cache
218+
219+ now = timezone .now ()
220+
221+ match period :
222+ case constants .CURRENT_BILLING_PERIOD :
223+ starts_at = sub_cache .current_billing_term_starts_at or now - timedelta (
224+ days = 30
225+ )
226+ month_delta = relativedelta (now , starts_at ).months
227+ date_start = relativedelta (months = month_delta ) + starts_at
228+ date_stop = now
229+
230+ case constants .PREVIOUS_BILLING_PERIOD :
231+ starts_at = sub_cache .current_billing_term_starts_at or now - timedelta (
232+ days = 30
233+ )
234+ month_delta = relativedelta (now , starts_at ).months - 1
235+ month_delta += relativedelta (now , starts_at ).years * 12
236+ date_start = relativedelta (months = month_delta ) + starts_at
237+ date_stop = relativedelta (months = month_delta + 1 ) + starts_at
238+
239+ case constants .NINETY_DAY_PERIOD :
240+ date_start = now - relativedelta (days = 90 )
241+ date_stop = now
242+
243+ case _:
244+ return None , None
245+
246+ return date_start , date_stop
0 commit comments