diff --git a/src/sentry/uptime/endpoints/validators.py b/src/sentry/uptime/endpoints/validators.py index 28155b43ad5755..e64fa3e4b732f3 100644 --- a/src/sentry/uptime/endpoints/validators.py +++ b/src/sentry/uptime/endpoints/validators.py @@ -25,6 +25,7 @@ MaxManualUptimeSubscriptionsReached, MaxUrlsForDomainReachedException, UptimeMonitorNoSeatAvailable, + check_uptime_subscription_limit, check_url_limits, create_uptime_detector, create_uptime_subscription, @@ -202,6 +203,16 @@ class UptimeMonitorValidator(CamelSnakeSerializer): ) def validate(self, attrs): + # When creating a new uptime monitor, check if we would exceed the organization limit + if not self.instance: + organization = self.context["organization"] + try: + check_uptime_subscription_limit(organization.id) + except MaxManualUptimeSubscriptionsReached: + raise serializers.ValidationError( + f"You may have at most {MAX_MANUAL_SUBSCRIPTIONS_PER_ORG} uptime monitors per organization" + ) + headers = [] method = "GET" body = None @@ -246,34 +257,29 @@ def create(self, validated_data): method_headers_body = { k: v for k, v in validated_data.items() if k in {"method", "headers", "body"} } - try: - detector = create_uptime_detector( - project=self.context["project"], - environment=environment, - url=validated_data["url"], - interval_seconds=validated_data["interval_seconds"], - timeout_ms=validated_data["timeout_ms"], - name=validated_data["name"], - status=validated_data.get("status"), - mode=validated_data.get("mode", UptimeMonitorMode.MANUAL), - owner=validated_data.get("owner"), - trace_sampling=validated_data.get("trace_sampling", False), - recovery_threshold=validated_data["recovery_threshold"], - downtime_threshold=validated_data["downtime_threshold"], - **method_headers_body, - ) + detector = create_uptime_detector( + project=self.context["project"], + environment=environment, + url=validated_data["url"], + interval_seconds=validated_data["interval_seconds"], + timeout_ms=validated_data["timeout_ms"], + name=validated_data["name"], + status=validated_data.get("status"), + mode=validated_data.get("mode", UptimeMonitorMode.MANUAL), + owner=validated_data.get("owner"), + trace_sampling=validated_data.get("trace_sampling", False), + recovery_threshold=validated_data["recovery_threshold"], + downtime_threshold=validated_data["downtime_threshold"], + **method_headers_body, + ) - create_audit_entry( - request=self.context["request"], - organization=self.context["organization"], - target_object=detector.id, - event=audit_log.get_event_id("UPTIME_MONITOR_ADD"), - data=get_audit_log_data(detector), - ) - except MaxManualUptimeSubscriptionsReached: - raise serializers.ValidationError( - f"You may have at most {MAX_MANUAL_SUBSCRIPTIONS_PER_ORG} uptime monitors per organization" - ) + create_audit_entry( + request=self.context["request"], + organization=self.context["organization"], + target_object=detector.id, + event=audit_log.get_event_id("UPTIME_MONITOR_ADD"), + data=get_audit_log_data(detector), + ) return detector diff --git a/src/sentry/uptime/subscriptions/subscriptions.py b/src/sentry/uptime/subscriptions/subscriptions.py index d61d14549a53a1..87d3d7295dfa12 100644 --- a/src/sentry/uptime/subscriptions/subscriptions.py +++ b/src/sentry/uptime/subscriptions/subscriptions.py @@ -86,6 +86,22 @@ class MaxManualUptimeSubscriptionsReached(ValueError): pass +def check_uptime_subscription_limit(organization_id: int) -> None: + """ + Check if adding a new manual uptime monitor would exceed the organization's limit. + Raises MaxManualUptimeSubscriptionsReached if the limit would be exceeded. + """ + manual_subscription_count = Detector.objects.filter( + status=ObjectStatus.ACTIVE, + type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, + project__organization_id=organization_id, + config__mode=UptimeMonitorMode.MANUAL, + ).count() + + if manual_subscription_count >= MAX_MANUAL_SUBSCRIPTIONS_PER_ORG: + raise MaxManualUptimeSubscriptionsReached + + class UptimeMonitorNoSeatAvailable(Exception): """ Indicates that the quotes system is unable to allocate a seat for the new @@ -234,13 +250,6 @@ def create_uptime_detector( Creates an UptimeSubscription and associated Detector """ if mode == UptimeMonitorMode.MANUAL: - manual_subscription_count = Detector.objects.filter( - status=ObjectStatus.ACTIVE, - type=GROUP_TYPE_UPTIME_DOMAIN_CHECK_FAILURE, - project__organization=project.organization, - config__mode=UptimeMonitorMode.MANUAL, - ).count() - # Once a user has created a subscription manually, make sure we disable all autodetection, and remove any # onboarding monitors if project.organization.get_option("sentry:uptime_autodetection", False): @@ -250,11 +259,8 @@ def create_uptime_detector( ): delete_uptime_detector(detector) - if ( - not override_manual_org_limit - and manual_subscription_count >= MAX_MANUAL_SUBSCRIPTIONS_PER_ORG - ): - raise MaxManualUptimeSubscriptionsReached + if not override_manual_org_limit: + check_uptime_subscription_limit(project.organization_id) with atomic_transaction( using=(