diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index 6609ea44d2af3f..84cc6c793f0e86 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -233,6 +233,9 @@ OrganizationMonitorEnvironmentDetailsEndpoint, ) from sentry.monitors.endpoints.organization_monitor_index import OrganizationMonitorIndexEndpoint +from sentry.monitors.endpoints.organization_monitor_index_count import ( + OrganizationMonitorIndexCountEndpoint, +) from sentry.monitors.endpoints.organization_monitor_index_stats import ( OrganizationMonitorIndexStatsEndpoint, ) @@ -1816,6 +1819,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrganizationMonitorIndexEndpoint.as_view(), name="sentry-api-0-organization-monitor-index", ), + re_path( + r"^(?P[^\/]+)/monitors-count/$", + OrganizationMonitorIndexCountEndpoint.as_view(), + name="sentry-api-0-organization-monitor-index-count", + ), re_path( r"^(?P[^\/]+)/monitors-stats/$", OrganizationMonitorIndexStatsEndpoint.as_view(), diff --git a/src/sentry/monitors/endpoints/organization_monitor_index_count.py b/src/sentry/monitors/endpoints/organization_monitor_index_count.py new file mode 100644 index 00000000000000..4b964edf1621ba --- /dev/null +++ b/src/sentry/monitors/endpoints/organization_monitor_index_count.py @@ -0,0 +1,64 @@ +from django.db.models import Q +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 OrganizationAlertRulePermission, OrganizationEndpoint +from sentry.constants import ObjectStatus +from sentry.models.organization import Organization +from sentry.monitors.models import Monitor +from sentry.utils.auth import AuthenticatedHttpRequest + + +@region_silo_endpoint +@extend_schema(tags=["Crons"]) +class OrganizationMonitorIndexCountEndpoint(OrganizationEndpoint): + publish_status = { + "GET": ApiPublishStatus.PRIVATE, + } + owner = ApiOwner.CRONS + permission_classes = (OrganizationAlertRulePermission,) + + def get(self, request: AuthenticatedHttpRequest, organization: Organization) -> Response: + """ + Retrieves the count of cron monitors for an organization. + """ + try: + filter_params = self.get_filter_params(request, organization, date_filter_optional=True) + except NoProjects: + return self.respond([]) + + queryset = Monitor.objects.filter( + 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: + environment_ids = [e.id for e in environments] + # use a distinct() filter as queries spanning multiple tables can include duplicates + queryset = queryset.filter( + Q(monitorenvironment__environment_id__in=environment_ids) + | Q(monitorenvironment=None) + ).distinct() + + all_monitors_count = queryset.count() + disabled_monitors_count = queryset.filter(status=ObjectStatus.DISABLED).count() + active_monitors_count = all_monitors_count - disabled_monitors_count + + return self.respond( + { + "counts": { + "total": all_monitors_count, + "active": active_monitors_count, + "disabled": disabled_monitors_count, + }, + } + ) diff --git a/tests/sentry/monitors/endpoints/test_organization_monitor_index_count.py b/tests/sentry/monitors/endpoints/test_organization_monitor_index_count.py new file mode 100644 index 00000000000000..eac5907fec6d37 --- /dev/null +++ b/tests/sentry/monitors/endpoints/test_organization_monitor_index_count.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from sentry.constants import ObjectStatus +from sentry.testutils.cases import MonitorTestCase + + +class OrganizationMonitorsCountTest(MonitorTestCase): + endpoint = "sentry-api-0-organization-monitor-index-count" + + def setUp(self): + super().setUp() + self.login_as(self.user) + + def test_simple(self): + self._create_monitor(name="Active Monitor 1") + self._create_monitor(name="Active Monitor 2") + self._create_monitor(name="Disabled Monitor", status=ObjectStatus.DISABLED) + + # Monitors pending deletion should be excluded + self._create_monitor(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): + # Create monitors with different environments + monitor1 = self._create_monitor(name="Monitor 1") + monitor2 = self._create_monitor(name="Monitor 2") + monitor3 = self._create_monitor(name="Monitor 3", status=ObjectStatus.DISABLED) + + self._create_monitor_environment(monitor1, name="production") + self._create_monitor_environment(monitor2, name="staging") + self._create_monitor_environment(monitor3, name="production") + + 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, + }, + }