From 8ec06d7d8e8d27f9b5a2a858833238a5cd3d1aec Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 30 May 2025 12:24:13 -0700 Subject: [PATCH 1/2] feat(uptime): Add uptime monitor count endpoint --- src/sentry/api/urls.py | 8 +++ .../organization_monitor_index_count.py | 10 ++- .../organization_uptime_alert_index_count.py | 66 +++++++++++++++++++ ...t_organization_uptime_alert_index_count.py | 62 +++++++++++++++++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py create mode 100644 tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 84cc6c793f0e86..211694e0c95b68 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -377,6 +377,9 @@ from sentry.uptime.endpoints.organiation_uptime_alert_index import ( OrganizationUptimeAlertIndexEndpoint, ) +from sentry.uptime.endpoints.organization_uptime_alert_index_count import ( + OrganizationUptimeAlertIndexCountEndpoint, +) from sentry.uptime.endpoints.organization_uptime_stats import OrganizationUptimeStatsEndpoint from sentry.uptime.endpoints.project_uptime_alert_checks_index import ( ProjectUptimeAlertCheckIndexEndpoint, @@ -2335,6 +2338,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrganizationUptimeAlertIndexEndpoint.as_view(), name="sentry-api-0-organization-uptime-alert-index", ), + re_path( + r"^(?P[^\/]+)/uptime-count/$", + OrganizationUptimeAlertIndexCountEndpoint.as_view(), + name="sentry-api-0-organization-uptime-alert-index-count", + ), re_path( r"^(?P[^\/]+)/uptime-stats/$", OrganizationUptimeStatsEndpoint.as_view(), diff --git a/src/sentry/monitors/endpoints/organization_monitor_index_count.py b/src/sentry/monitors/endpoints/organization_monitor_index_count.py index 4b964edf1621ba..56522a69769d93 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_index_count.py +++ b/src/sentry/monitors/endpoints/organization_monitor_index_count.py @@ -29,7 +29,15 @@ def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> try: filter_params = self.get_filter_params(request, organization, date_filter_optional=True) except NoProjects: - return self.respond([]) + return self.respond( + { + "counts": { + "total": 0, + "active": 0, + "disabled": 0, + }, + } + ) queryset = Monitor.objects.filter( organization_id=organization.id, project_id__in=filter_params["project_id"] diff --git a/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py b/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py new file mode 100644 index 00000000000000..18404f0ac35124 --- /dev/null +++ b/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py @@ -0,0 +1,66 @@ +from drf_spectacular.utils import extend_schema +from rest_framework.response import Response + +from sentry.api.api_owners import ApiOwner +from sentry.api.api_publish_status import ApiPublishStatus +from sentry.api.base import region_silo_endpoint +from sentry.api.bases import NoProjects +from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission +from sentry.constants import ObjectStatus +from sentry.models.organization import Organization +from sentry.uptime.models import ProjectUptimeSubscription +from sentry.utils.auth import AuthenticatedHttpRequest + + +@region_silo_endpoint +@extend_schema(tags=["Uptime Monitors"]) +class OrganizationUptimeAlertIndexCountEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + } + owner = ApiOwner.CRONS + permission_classes = (OrganizationPermission,) + + def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> Response: + """ + Retrieves the count of uptime monitors for an organization. + """ + try: + filter_params = self.get_filter_params(request, organization, date_filter_optional=True) + except NoProjects: + return self.respond( + { + "counts": { + "total": 0, + "active": 0, + "disabled": 0, + }, + } + ) + + queryset = ProjectUptimeSubscription.objects.filter( + project__organization_id=organization.id, project_id__in=filter_params["project_id"] + ).exclude( + status__in=[ + ObjectStatus.PENDING_DELETION, + ObjectStatus.DELETION_IN_PROGRESS, + ] + ) + + environments = filter_params.get("environment_objects") + if environments is not None: + queryset = queryset.filter(environment__in=environments) + + all_uptime_alerts_count = queryset.count() + disabled_uptime_alerts_count = queryset.filter(status=ObjectStatus.DISABLED).count() + active_uptime_alerts_count = all_uptime_alerts_count - disabled_uptime_alerts_count + + return self.respond( + { + "counts": { + "total": all_uptime_alerts_count, + "active": active_uptime_alerts_count, + "disabled": disabled_uptime_alerts_count, + }, + } + ) diff --git a/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py new file mode 100644 index 00000000000000..79e9aa558426b2 --- /dev/null +++ b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from sentry.constants import ObjectStatus +from tests.sentry.uptime.endpoints import UptimeAlertBaseEndpointTest + + +class OrganizationUptimeAlertCountTest(UptimeAlertBaseEndpointTest): + endpoint = "sentry-api-0-organization-uptime-alert-index-count" + + def setUp(self): + super().setUp() + self.login_as(self.user) + + def test_simple(self): + self.create_project_uptime_subscription(name="Active Alert 1") + self.create_project_uptime_subscription(name="Active Alert 2") + self.create_project_uptime_subscription(name="Disabled Alert", status=ObjectStatus.DISABLED) + + # Uptime alerts pending deletion should be excluded + self.create_project_uptime_subscription( + name="Pending Deletion", status=ObjectStatus.PENDING_DELETION + ) + + response = self.get_success_response(self.organization.slug) + + assert response.data == { + "counts": { + "total": 3, + "active": 2, + "disabled": 1, + }, + } + + def test_filtered_by_environment(self): + env1 = self.create_environment(name="production") + env2 = self.create_environment(name="staging") + + self.create_project_uptime_subscription(name="Alert 1", env=env1) + self.create_project_uptime_subscription(name="Alert 2", env=env2) + self.create_project_uptime_subscription( + name="Alert 3", env=env1, status=ObjectStatus.DISABLED + ) + + response = self.get_success_response(self.organization.slug, environment=["production"]) + + assert response.data == { + "counts": { + "total": 2, + "active": 1, + "disabled": 1, + }, + } + + response = self.get_success_response(self.organization.slug, environment=["staging"]) + + assert response.data == { + "counts": { + "total": 1, + "active": 1, + "disabled": 0, + }, + } From 3487ff585519ceb4645bb2e2701765bbadfef492 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 30 May 2025 14:37:21 -0700 Subject: [PATCH 2/2] Use Detector model instead --- .../organization_uptime_alert_index_count.py | 29 +++++------- ...t_organization_uptime_alert_index_count.py | 47 ++++++++++++++----- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py b/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py index 18404f0ac35124..e6ece15e7ddd89 100644 --- a/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py +++ b/src/sentry/uptime/endpoints/organization_uptime_alert_index_count.py @@ -6,10 +6,10 @@ from sentry.api.base import region_silo_endpoint from sentry.api.bases import NoProjects from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission -from sentry.constants import ObjectStatus from sentry.models.organization import Organization -from sentry.uptime.models import ProjectUptimeSubscription +from sentry.uptime.types import GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE from sentry.utils.auth import AuthenticatedHttpRequest +from sentry.workflow_engine.models import Detector @region_silo_endpoint @@ -38,28 +38,23 @@ def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> } ) - queryset = ProjectUptimeSubscription.objects.filter( - project__organization_id=organization.id, project_id__in=filter_params["project_id"] - ).exclude( - status__in=[ - ObjectStatus.PENDING_DELETION, - ObjectStatus.DELETION_IN_PROGRESS, - ] + queryset = Detector.objects.filter( + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + project__organization_id=organization.id, + project_id__in=filter_params["project_id"], ) - environments = filter_params.get("environment_objects") - if environments is not None: - queryset = queryset.filter(environment__in=environments) + if "environment" in filter_params: + queryset = queryset.filter(config__environment__in=filter_params["environment"]) - all_uptime_alerts_count = queryset.count() - disabled_uptime_alerts_count = queryset.filter(status=ObjectStatus.DISABLED).count() - active_uptime_alerts_count = all_uptime_alerts_count - disabled_uptime_alerts_count + enabled_uptime_alerts_count = queryset.filter(enabled=True).count() + disabled_uptime_alerts_count = queryset.filter(enabled=False).count() return self.respond( { "counts": { - "total": all_uptime_alerts_count, - "active": active_uptime_alerts_count, + "total": enabled_uptime_alerts_count + disabled_uptime_alerts_count, + "active": enabled_uptime_alerts_count, "disabled": disabled_uptime_alerts_count, }, } diff --git a/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py index 79e9aa558426b2..96f8adbadcb1c5 100644 --- a/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py +++ b/tests/sentry/uptime/endpoints/test_organization_uptime_alert_index_count.py @@ -1,6 +1,6 @@ from __future__ import annotations -from sentry.constants import ObjectStatus +from sentry.uptime.types import GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE from tests.sentry.uptime.endpoints import UptimeAlertBaseEndpointTest @@ -12,13 +12,23 @@ def setUp(self): self.login_as(self.user) def test_simple(self): - self.create_project_uptime_subscription(name="Active Alert 1") - self.create_project_uptime_subscription(name="Active Alert 2") - self.create_project_uptime_subscription(name="Disabled Alert", status=ObjectStatus.DISABLED) - - # Uptime alerts pending deletion should be excluded - self.create_project_uptime_subscription( - name="Pending Deletion", status=ObjectStatus.PENDING_DELETION + self.create_detector( + name="Active Alert 1", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=True, + config={"environment": self.environment.name, "mode": 1}, + ) + self.create_detector( + name="Active Alert 2", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=True, + config={"environment": self.environment.name, "mode": 1}, + ) + self.create_detector( + name="Disabled Alert", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=False, + config={"environment": self.environment.name, "mode": 1}, ) response = self.get_success_response(self.organization.slug) @@ -35,10 +45,23 @@ def test_filtered_by_environment(self): env1 = self.create_environment(name="production") env2 = self.create_environment(name="staging") - self.create_project_uptime_subscription(name="Alert 1", env=env1) - self.create_project_uptime_subscription(name="Alert 2", env=env2) - self.create_project_uptime_subscription( - name="Alert 3", env=env1, status=ObjectStatus.DISABLED + self.create_detector( + name="Alert 1", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=True, + config={"environment": env1.name, "mode": 1}, + ) + self.create_detector( + name="Alert 2", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=True, + config={"environment": env2.name, "mode": 1}, + ) + self.create_detector( + name="Alert 3", + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + enabled=False, + config={"environment": env1.name, "mode": 1}, ) response = self.get_success_response(self.organization.slug, environment=["production"])