Skip to content

Commit 7bb5349

Browse files
ref(workflow_engine): Route detector deletion through validators (#102919)
Adds delete() method to BaseDetectorTypeValidator to consolidate all CRUD operations (create, update, delete) within validators. This allows detector-specific logic to be cleanly overridden in subclass validators and replaces the need for lifecycle hooks. - Validators already handle create() and update() with access to old state via self.instance - Validators can raise ValidationError for user-facing errors (e.g., billing seat failures) - Overriding delete() allows immediate actions when a user perceives their detector as deleted (e.g., freeing billing seats, stopping checks) - Additional async cleanup still happens via data source-specific deletion tasks that the detector cascades into The default implementation schedules the detector for deletion and updates its status. Subclasses can override to perform detector-specific cleanup before calling super().delete().
1 parent 35f2723 commit 7bb5349

File tree

3 files changed

+36
-4
lines changed

3 files changed

+36
-4
lines changed

src/sentry/workflow_engine/endpoints/organization_detector_details.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@
2020
RESPONSE_UNAUTHORIZED,
2121
)
2222
from sentry.apidocs.parameters import DetectorParams, GlobalParams
23-
from sentry.constants import ObjectStatus
2423
from sentry.db.postgres.transactions import in_test_hide_transaction_boundary
25-
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
2624
from sentry.grouping.grouptype import ErrorGroupType
2725
from sentry.incidents.grouptype import MetricIssue
2826
from sentry.incidents.metric_issue_detector import schedule_update_project_config
@@ -202,8 +200,10 @@ def delete(self, request: Request, organization: Organization, detector: Detecto
202200
if detector.type == ErrorGroupType.slug:
203201
return Response(status=403)
204202

205-
RegionScheduledDeletion.schedule(detector, days=0, actor=request.user)
206-
detector.update(status=ObjectStatus.PENDING_DELETION)
203+
validator = get_detector_validator(
204+
request, detector.project, detector.type, instance=detector
205+
)
206+
validator.delete()
207207

208208
DetectorLifeCycleHooks.on_pending_delete(detector)
209209

src/sentry/workflow_engine/endpoints/validators/base/detector.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from sentry import audit_log
1010
from sentry.api.fields.actor import ActorField
1111
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
12+
from sentry.constants import ObjectStatus
13+
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
1214
from sentry.issues import grouptype
1315
from sentry.issues.grouptype import GroupType
1416
from sentry.utils.audit import create_audit_entry
@@ -144,6 +146,18 @@ def update(self, instance: Detector, validated_data: dict[str, Any]):
144146
DetectorLifeCycleHooks.on_after_update(instance)
145147
return instance
146148

149+
def delete(self) -> None:
150+
"""
151+
Delete the detector by scheduling it for deletion.
152+
153+
Subclasses can override this to perform detector-specific cleanup before
154+
deletion (e.g., removing billing seats, cleaning up external resources).
155+
They should call super().delete() to perform the actual deletion.
156+
"""
157+
assert self.instance is not None
158+
RegionScheduledDeletion.schedule(self.instance, days=0, actor=self.context["request"].user)
159+
self.instance.update(status=ObjectStatus.PENDING_DELETION)
160+
147161
def _create_data_source(self, validated_data_source, detector: Detector):
148162
data_source_creator = validated_data_source["_creator"]
149163
data_source = data_source_creator.create()

tests/sentry/workflow_engine/endpoints/test_validators.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from rest_framework.exceptions import ErrorDetail, ValidationError
66

77
from sentry import audit_log
8+
from sentry.constants import ObjectStatus
9+
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
810
from sentry.incidents.grouptype import MetricIssue
911
from sentry.incidents.metric_issue_detector import (
1012
MetricIssueComparisonConditionValidator,
@@ -247,3 +249,19 @@ def test_validate_type_incompatible(self) -> None:
247249
assert validator.errors.get("type") == [
248250
ErrorDetail(string="Detector type not compatible with detectors", code="invalid")
249251
]
252+
253+
def test_delete(self) -> None:
254+
"""Test that delete() schedules the detector for deletion"""
255+
validator = MockDetectorValidator(data=self.valid_data, context=self.context)
256+
assert validator.is_valid()
257+
detector = validator.save()
258+
259+
delete_validator = MockDetectorValidator(instance=detector, data={}, context=self.context)
260+
delete_validator.delete()
261+
262+
assert RegionScheduledDeletion.objects.filter(
263+
model_name="Detector", object_id=detector.id
264+
).exists()
265+
266+
detector.refresh_from_db()
267+
assert detector.status == ObjectStatus.PENDING_DELETION

0 commit comments

Comments
 (0)