diff --git a/api/organisations/task_helpers.py b/api/organisations/task_helpers.py index 0062633322dc..3f99da1947d0 100644 --- a/api/organisations/task_helpers.py +++ b/api/organisations/task_helpers.py @@ -29,6 +29,14 @@ def send_api_flags_blocked_notification(organisation: Organisation) -> None: userorganisation__organisation=organisation, ) + recipient_emails = list(recipient_list.values_list("email", flat=True)) + if not recipient_emails: + logger.warning( + "notification.no_recipients_for_blocked_notification", + organisation__id=organisation.id, + ) + return + url = get_current_site_url() context = { "organisation": organisation, @@ -43,7 +51,7 @@ def send_api_flags_blocked_notification(organisation: Organisation) -> None: subject="Flagsmith API use has been blocked due to overuse", message=render_to_string(message, context), from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=list(recipient_list.values_list("email", flat=True)), + recipient_list=recipient_emails, html_message=render_to_string(html_message, context), fail_silently=True, ) @@ -75,6 +83,21 @@ def _send_api_usage_notification( message = "organisations/api_usage_notification_limit.txt" html_message = "organisations/api_usage_notification_limit.html" + recipient_emails = list(recipient_list.values_list("email", flat=True)) + + if not recipient_emails: + logger.warning( + "notification.no_recipients", + organisation__id=organisation.id, + matched_threshold=matched_threshold, + ) + OrganisationAPIUsageNotification.objects.create( + organisation=organisation, + percent_usage=matched_threshold, + notified_at=timezone.now(), + ) + return + url = get_current_site_url() context = { "organisation": organisation, @@ -87,7 +110,7 @@ def _send_api_usage_notification( subject=f"Flagsmith API use has reached {matched_threshold}%", message=render_to_string(message, context), from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=list(recipient_list.values_list("email", flat=True)), + recipient_list=recipient_emails, html_message=render_to_string(html_message, context), fail_silently=True, ) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index d953ed2845e3..1c23ed8d0c04 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -39,6 +39,7 @@ from organisations.subscriptions.xero.metadata import XeroSubscriptionMetadata from organisations.task_helpers import ( handle_api_usage_notification_for_organisation, + send_api_flags_blocked_notification, ) from organisations.tasks import ( # type: ignore[attr-defined] ALERT_EMAIL_MESSAGE, @@ -530,6 +531,45 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce ) +@pytest.mark.django_db +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +def test_handle_api_usage_notifications__no_admin_users__skips_notification( + mocker: MockerFixture, + mailoutbox: list[EmailMultiAlternatives], + log: StructuredLogCapture, + enable_features: EnableFeaturesFixture, +) -> None: + # Given - an organisation with no users + organisation = Organisation.objects.create(name="No Users Org") + now = timezone.now() + organisation.subscription.plan = SCALE_UP + organisation.subscription.subscription_id = "fancy_id" + organisation.subscription.save() + OrganisationSubscriptionInformationCache.objects.create( + organisation=organisation, + allowed_30d_api_calls=100, + current_billing_term_starts_at=now - timedelta(days=45), + current_billing_term_ends_at=now + timedelta(days=320), + api_calls_30d=91, + ) + mock_api_usage = mocker.patch( + "organisations.task_helpers.get_current_api_usage", + ) + mock_api_usage.return_value = 91 + enable_features("api_usage_alerting") + + # When + handle_api_usage_notifications() + + # Then - no email sent, warning logged + assert len(mailoutbox) == 0 + assert any(e.get("event") == "notification.no_recipients" for e in log.events) + assert OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + percent_usage=91, + ).exists() + + @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_handle_api_usage_notifications__usage_below_alert_thresholds__sends_no_email( mocker: MockerFixture, @@ -2136,3 +2176,27 @@ def test_update_organisation_subscription_information_cache__called__calls_updat SubscriptionCacheEntity.CHARGEBEE, SubscriptionCacheEntity.API_USAGE ) ] + + +@pytest.mark.django_db +def test_send_api_flags_blocked_notification__no_recipients__skips_notification( + organisation: Organisation, + log: StructuredLogCapture, + mailoutbox: list[EmailMultiAlternatives], +) -> None: + # Given + # Ensure no users are associated with the organisation + UserOrganisation.objects.filter(organisation=organisation).delete() + + # When + send_api_flags_blocked_notification(organisation) + + # Then + assert len(mailoutbox) == 0 + assert log.events == [ + { + "level": "warning", + "event": "notification.no_recipients_for_blocked_notification", + "organisation__id": organisation.id, + } + ] diff --git a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md index 2ddb52d2f39b..18d85e685fa1 100644 --- a/docs/docs/deployment-self-hosting/observability/_events-catalogue.md +++ b/docs/docs/deployment-self-hosting/observability/_events-catalogue.md @@ -2,7 +2,7 @@ ### `api_usage.notification.evaluated` Logged at `info` from: - - `api/organisations/task_helpers.py:153` + - `api/organisations/task_helpers.py:171` Attributes: - `allowed_api_calls` @@ -16,7 +16,24 @@ Attributes: ### `api_usage.notification.missing_billing_starts_at` Logged at `error` from: - - `api/organisations/task_helpers.py:118` + - `api/organisations/task_helpers.py:136` + +Attributes: + - `organisation.id` + +### `api_usage.notification.no_recipients` + +Logged at `warning` from: + - `api/organisations/task_helpers.py:89` + +Attributes: + - `matched_threshold` + - `organisation.id` + +### `api_usage.notification.no_recipients_for_blocked_notification` + +Logged at `warning` from: + - `api/organisations/task_helpers.py:34` Attributes: - `organisation.id` @@ -24,7 +41,7 @@ Attributes: ### `api_usage.notification.sent` Logged at `info` from: - - `api/organisations/task_helpers.py:176` + - `api/organisations/task_helpers.py:194` Attributes: - `matched_threshold`