Skip to content

Commit 437abff

Browse files
authored
fix(API Usage): Allow self-hosted API usage alerts (#6975)
1 parent 7c8e69d commit 437abff

6 files changed

Lines changed: 118 additions & 97 deletions

File tree

api/app_analytics/analytics_db_service.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import date, datetime, timedelta
1+
from datetime import datetime, timedelta
22

33
import structlog
44
from common.core.utils import using_database_replica
@@ -122,20 +122,31 @@ def get_usage_data_from_local_db(
122122
return map_annotated_api_usage_buckets_to_usage_data(qs)
123123

124124

125-
def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
125+
def get_total_events_count(
126+
organisation: Organisation,
127+
date_start: datetime | None = None,
128+
date_stop: datetime | None = None,
129+
) -> int:
126130
"""
127-
Return total number of events for an organisation in the last 30 days
131+
Return total number of events for an organisation for a range, or last 30 days
128132
"""
133+
today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
134+
date_start = date_start or (today - timedelta(days=30))
135+
date_stop = date_stop or today
129136
if settings.USE_POSTGRES_FOR_ANALYTICS:
130-
count = APIUsageBucket.objects.filter(
137+
count: int = APIUsageBucket.objects.filter(
131138
environment_id__in=_get_environment_ids_for_org(organisation),
132-
created_at__date__lte=date.today(),
133-
created_at__date__gt=date.today() - timedelta(days=30),
139+
created_at__date__lte=date_stop,
140+
created_at__date__gt=date_start,
134141
bucket_size=constants.ANALYTICS_READ_BUCKET_SIZE,
135142
).aggregate(total_count=Sum("total_count"))["total_count"]
136143
else:
137-
count = get_events_for_organisation(organisation.id)
138-
return count # type: ignore[no-any-return]
144+
count = get_events_for_organisation(
145+
organisation.id,
146+
date_start=date_start,
147+
date_stop=date_stop,
148+
)
149+
return count
139150

140151

141152
def get_feature_evaluation_data(

api/integrations/flagsmith/data/environment.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@
104104
"feature_state_value": null,
105105
"featurestate_uuid": "f976df2f-2341-4623-8425-d6eda23a2ebc",
106106
"multivariate_feature_state_values": []
107+
},
108+
{
109+
"django_id": 1229327,
110+
"enabled": false,
111+
"feature": {
112+
"id": 195393,
113+
"name": "get_current_api_usage_deprecated",
114+
"type": "STANDARD"
115+
},
116+
"feature_segment": null,
117+
"feature_state_value": null,
118+
"featurestate_uuid": "e7ed0d54-b17c-4df1-ab98-5f8dc9597127",
119+
"multivariate_feature_state_values": []
107120
}
108121
],
109122
"id": 0,

api/organisations/task_helpers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
from django.template.loader import render_to_string
88
from django.utils import timezone
99

10+
from app_analytics.analytics_db_service import get_total_events_count
1011
from app_analytics.influxdb_wrapper import get_current_api_usage
1112
from core.helpers import get_current_site_url
13+
from integrations.flagsmith.client import get_client
1214
from organisations.models import (
1315
Organisation,
1416
OrganisationAPIUsageNotification,
@@ -126,7 +128,16 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) -
126128

127129
allowed_api_calls = subscription_cache.allowed_30d_api_calls
128130

129-
api_usage = get_current_api_usage(organisation.id, period_starts_at)
131+
flagsmith_client = get_client("local", local_eval=True)
132+
flags = flagsmith_client.get_identity_flags(
133+
organisation.flagsmith_identifier,
134+
traits=organisation.flagsmith_on_flagsmith_api_traits,
135+
)
136+
# TODO: Default to get_total_events_count — https://github.com/Flagsmith/flagsmith/issues/6985
137+
if flags.is_feature_enabled("get_current_api_usage_deprecated"): # pragma: no cover
138+
api_usage = get_total_events_count(organisation, period_starts_at)
139+
else:
140+
api_usage = get_current_api_usage(organisation.id, period_starts_at)
130141

131142
# For some reason the allowed API calls is set to 0 so default to the max free plan.
132143
allowed_api_calls = allowed_api_calls or MAX_API_CALLS_IN_FREE_PLAN

api/organisations/tasks.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
register_task_handler,
1313
)
1414

15+
from app_analytics.analytics_db_service import get_total_events_count
1516
from app_analytics.influxdb_wrapper import get_current_api_usage
1617
from integrations.flagsmith.client import get_client
1718
from organisations import subscription_info_cache
@@ -214,10 +215,19 @@ def charge_for_api_call_count_overages(): # type: ignore[no-untyped-def]
214215
continue
215216

216217
subscription_cache = organisation.subscription_information_cache
217-
api_usage = get_current_api_usage(
218-
organisation_id=organisation.id,
219-
date_start=subscription_cache.current_billing_term_starts_at,
220-
)
218+
# TODO: Default to get_total_events_count — https://github.com/Flagsmith/flagsmith/issues/6985
219+
if flags.is_feature_enabled(
220+
"get_current_api_usage_deprecated"
221+
): # pragma: no cover
222+
api_usage = get_total_events_count(
223+
organisation,
224+
date_start=subscription_cache.current_billing_term_starts_at,
225+
)
226+
else:
227+
api_usage = get_current_api_usage(
228+
organisation_id=organisation.id,
229+
date_start=subscription_cache.current_billing_term_starts_at,
230+
)
221231

222232
# Grace period for organisations < 200% of usage.
223233
if (
@@ -347,10 +357,19 @@ def restrict_use_due_to_api_limit_grace_period_over() -> None:
347357
OrganisationBreachedGracePeriod.objects.get_or_create(organisation=organisation)
348358

349359
subscription_cache = organisation.subscription_information_cache
350-
api_usage = get_current_api_usage(
351-
organisation_id=organisation.id,
352-
date_start=now - timedelta(days=30),
353-
)
360+
# TODO: Default to get_total_events_count — https://github.com/Flagsmith/flagsmith/issues/6985
361+
if flags.is_feature_enabled(
362+
"get_current_api_usage_deprecated"
363+
): # pragma: no cover
364+
api_usage = get_total_events_count(
365+
organisation,
366+
date_start=now - timedelta(days=30),
367+
)
368+
else:
369+
api_usage = get_current_api_usage(
370+
organisation_id=organisation.id,
371+
date_start=now - timedelta(days=30),
372+
)
354373
if api_usage / subscription_cache.allowed_30d_api_calls < 1.0:
355374
logger.info(
356375
f"API use for organisation {organisation.id} has fallen to below limit, so not restricting use."

api/tests/unit/app_analytics/test_analytics_db_service.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def cache(organisation: Organisation) -> OrganisationSubscriptionInformationCach
4545

4646

4747
@pytest.mark.use_analytics_db
48+
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
4849
def test_get_usage_data_from_local_db__multiple_buckets__returns_aggregated_daily_data( # type: ignore[no-untyped-def]
4950
organisation, environment, settings
5051
):
@@ -197,6 +198,7 @@ def test_get_usage_data_from_local_db__environment_filter__returns_expected(
197198

198199

199200
@pytest.mark.use_analytics_db
201+
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
200202
def test_get_usage_data_from_local_db__labels_filter__returns_expected(
201203
organisation: Organisation,
202204
environment: Environment,
@@ -269,6 +271,7 @@ def test_get_usage_data_from_local_db__labels_filter__returns_expected(
269271

270272

271273
@pytest.mark.use_analytics_db
274+
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
272275
def test_get_total_events_count__multiple_buckets__returns_correct_total( # type: ignore[no-untyped-def]
273276
organisation, environment, settings
274277
):
@@ -322,6 +325,7 @@ def test_get_total_events_count__multiple_buckets__returns_correct_total( # typ
322325

323326

324327
@pytest.mark.use_analytics_db
328+
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
325329
def test_get_feature_evaluation_data_from_local_db__multiple_buckets__returns_aggregated_daily_data(
326330
feature: Feature,
327331
environment: Environment,
@@ -538,6 +542,7 @@ def test_get_usage_data__no_analytics_configured__no_calls_expected(
538542
mocked_get_usage_data_from_local_db.assert_not_called()
539543

540544

545+
@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
541546
def test_get_total_events_count__postgres_not_configured__calls_influx( # type: ignore[no-untyped-def]
542547
mocker, settings, organisation
543548
):
@@ -553,7 +558,9 @@ def test_get_total_events_count__postgres_not_configured__calls_influx( # type:
553558
# Then
554559
assert total_events_count == mocked_get_events_for_organisation.return_value
555560
mocked_get_events_for_organisation.assert_called_once_with(
556-
organisation_id=organisation.id
561+
organisation.id,
562+
date_start=datetime(2022, 12, 20, 0, 0, tzinfo=UTC),
563+
date_stop=datetime(2023, 1, 19, 0, 0, tzinfo=UTC),
557564
)
558565

559566

0 commit comments

Comments
 (0)