From 9460490cdcdaa7f92a36ea107b1b23b402debe1e Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:04:44 +0000 Subject: [PATCH 1/6] migration(workflow-engine): Delete malformed anomaly conditions Co-Authored-By: Kyle Consalus --- migrations_lockfile.txt | 2 +- ...121_delete_malformed_anomaly_conditions.py | 51 +++++++++++++++++++ ...121_delete_malformed_anomaly_conditions.py | 44 ++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py create mode 100644 tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 56944e1f3f26..6117250245e5 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -43,4 +43,4 @@ tempest: 0001_squashed_0003_use_encrypted_char_field uptime: 0001_squashed_0055_backfill_2xx_status_assertion -workflow_engine: 0120_replace_boolean_deescalation_comparisons +workflow_engine: 0121_delete_malformed_anomaly_conditions diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py new file mode 100644 index 000000000000..1cdfdcadd250 --- /dev/null +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -0,0 +1,51 @@ +# Generated by Django 5.2.16 on 2026-09-17 21:56 + +from django.db import migrations +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps + +from sentry.new_migrations.migrations import CheckedMigration +from sentry.utils.iterators import chunked +from sentry.utils.query import RangeQuerySetWrapper + +BATCH_SIZE = 1000 + + +def delete_malformed_anomaly_conditions( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + DataCondition = apps.get_model("workflow_engine", "DataCondition") + anomaly_condition_group_ids = DataCondition.objects.filter( + type="anomaly_detection" + ).values_list("condition_group_id", flat=True) + conditions = ( + DataCondition.objects.filter(condition_group_id__in=anomaly_condition_group_ids) + .exclude(type="anomaly_detection") + .values_list("id", flat=True) + ) + + for condition_ids in chunked( + RangeQuerySetWrapper( + conditions, + step=BATCH_SIZE, + result_value_getter=lambda condition_id: condition_id, + ), + BATCH_SIZE, + ): + DataCondition.objects.filter(id__in=condition_ids).delete() + + +class Migration(CheckedMigration): + is_post_deployment = True + + dependencies = [ + ("workflow_engine", "0120_replace_boolean_deescalation_comparisons"), + ] + + operations = [ + migrations.RunPython( + delete_malformed_anomaly_conditions, + reverse_code=migrations.RunPython.noop, + hints={"tables": ["workflow_engine_datacondition"]}, + ), + ] diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py new file mode 100644 index 000000000000..9682996d5275 --- /dev/null +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -0,0 +1,44 @@ +from sentry.testutils.cases import TestMigrations + + +class DeleteMalformedAnomalyConditionsTest(TestMigrations): + app = "workflow_engine" + migrate_from = "0120_replace_boolean_deescalation_comparisons" + migrate_to = "0121_delete_malformed_anomaly_conditions" + + def setup_before_migration(self, apps): + DataCondition = apps.get_model("workflow_engine", "DataCondition") + DataConditionGroup = apps.get_model("workflow_engine", "DataConditionGroup") + + malformed_group = DataConditionGroup.objects.create(organization_id=self.organization.id) + self.anomaly_condition = DataCondition.objects.create( + condition_group_id=malformed_group.id, + type="anomaly_detection", + comparison={ + "sensitivity": "low", + "seasonality": "auto", + "threshold_type": 0, + }, + condition_result=75, + ) + self.malformed_condition = DataCondition.objects.create( + condition_group_id=malformed_group.id, + type="lte", + comparison=0, + condition_result=0, + ) + + static_group = DataConditionGroup.objects.create(organization_id=self.organization.id) + self.valid_static_condition = DataCondition.objects.create( + condition_group_id=static_group.id, + type="lte", + comparison=0, + condition_result=0, + ) + + def test(self): + DataCondition = self.apps.get_model("workflow_engine", "DataCondition") + + assert DataCondition.objects.filter(id=self.anomaly_condition.id).exists() + assert not DataCondition.objects.filter(id=self.malformed_condition.id).exists() + assert DataCondition.objects.filter(id=self.valid_static_condition.id).exists() From 5af9ca299465bcd792508313a9958697c90a95ed Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:56:12 +0000 Subject: [PATCH 2/6] fix(workflow-engine): Repair malformed anomaly comparisons --- ...121_delete_malformed_anomaly_conditions.py | 13 +++++-- ...121_delete_malformed_anomaly_conditions.py | 34 +++++++++++++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py index 1cdfdcadd250..cc321d41f472 100644 --- a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -15,15 +15,22 @@ def delete_malformed_anomaly_conditions( apps: StateApps, schema_editor: BaseDatabaseSchemaEditor ) -> None: DataCondition = apps.get_model("workflow_engine", "DataCondition") - anomaly_condition_group_ids = DataCondition.objects.filter( - type="anomaly_detection" - ).values_list("condition_group_id", flat=True) + anomaly_conditions = DataCondition.objects.filter(type="anomaly_detection") + anomaly_condition_group_ids = anomaly_conditions.values_list("condition_group_id", flat=True) conditions = ( DataCondition.objects.filter(condition_group_id__in=anomaly_condition_group_ids) .exclude(type="anomaly_detection") .values_list("id", flat=True) ) + anomaly_conditions.filter(comparison=0.0).update( + comparison={ + "seasonality": "auto", + "sensitivity": "low", + "threshold_type": 2, + } + ) + for condition_ids in chunked( RangeQuerySetWrapper( conditions, diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py index 9682996d5275..6e4b59c86f06 100644 --- a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -14,11 +14,7 @@ def setup_before_migration(self, apps): self.anomaly_condition = DataCondition.objects.create( condition_group_id=malformed_group.id, type="anomaly_detection", - comparison={ - "sensitivity": "low", - "seasonality": "auto", - "threshold_type": 0, - }, + comparison=0.0, condition_result=75, ) self.malformed_condition = DataCondition.objects.create( @@ -28,6 +24,20 @@ def setup_before_migration(self, apps): condition_result=0, ) + valid_anomaly_group = DataConditionGroup.objects.create( + organization_id=self.organization.id + ) + self.valid_anomaly_condition = DataCondition.objects.create( + condition_group_id=valid_anomaly_group.id, + type="anomaly_detection", + comparison={ + "sensitivity": "high", + "seasonality": "auto", + "threshold_type": 0, + }, + condition_result=75, + ) + static_group = DataConditionGroup.objects.create(organization_id=self.organization.id) self.valid_static_condition = DataCondition.objects.create( condition_group_id=static_group.id, @@ -39,6 +49,18 @@ def setup_before_migration(self, apps): def test(self): DataCondition = self.apps.get_model("workflow_engine", "DataCondition") - assert DataCondition.objects.filter(id=self.anomaly_condition.id).exists() + anomaly_condition = DataCondition.objects.get(id=self.anomaly_condition.id) + assert anomaly_condition.comparison == { + "seasonality": "auto", + "sensitivity": "low", + "threshold_type": 2, + } assert not DataCondition.objects.filter(id=self.malformed_condition.id).exists() + + valid_anomaly_condition = DataCondition.objects.get(id=self.valid_anomaly_condition.id) + assert valid_anomaly_condition.comparison == { + "sensitivity": "high", + "seasonality": "auto", + "threshold_type": 0, + } assert DataCondition.objects.filter(id=self.valid_static_condition.id).exists() From e45849a4197f108bf17e0dd2bddf6eab62ff1f42 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:08:03 +0000 Subject: [PATCH 3/6] perf(workflow-engine): Scope malformed condition cleanup --- ...0121_delete_malformed_anomaly_conditions.py | 18 +++++++++--------- ...0121_delete_malformed_anomaly_conditions.py | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py index cc321d41f472..7f0c6d073999 100644 --- a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -15,7 +15,7 @@ def delete_malformed_anomaly_conditions( apps: StateApps, schema_editor: BaseDatabaseSchemaEditor ) -> None: DataCondition = apps.get_model("workflow_engine", "DataCondition") - anomaly_conditions = DataCondition.objects.filter(type="anomaly_detection") + anomaly_conditions = DataCondition.objects.filter(type="anomaly_detection", comparison=0.0) anomaly_condition_group_ids = anomaly_conditions.values_list("condition_group_id", flat=True) conditions = ( DataCondition.objects.filter(condition_group_id__in=anomaly_condition_group_ids) @@ -23,14 +23,6 @@ def delete_malformed_anomaly_conditions( .values_list("id", flat=True) ) - anomaly_conditions.filter(comparison=0.0).update( - comparison={ - "seasonality": "auto", - "sensitivity": "low", - "threshold_type": 2, - } - ) - for condition_ids in chunked( RangeQuerySetWrapper( conditions, @@ -41,6 +33,14 @@ def delete_malformed_anomaly_conditions( ): DataCondition.objects.filter(id__in=condition_ids).delete() + anomaly_conditions.update( + comparison={ + "seasonality": "auto", + "sensitivity": "low", + "threshold_type": 2, + } + ) + class Migration(CheckedMigration): is_post_deployment = True diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py index 6e4b59c86f06..b4184c2d767d 100644 --- a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -37,6 +37,12 @@ def setup_before_migration(self, apps): }, condition_result=75, ) + self.valid_mixed_condition = DataCondition.objects.create( + condition_group_id=valid_anomaly_group.id, + type="lte", + comparison=0, + condition_result=0, + ) static_group = DataConditionGroup.objects.create(organization_id=self.organization.id) self.valid_static_condition = DataCondition.objects.create( @@ -63,4 +69,5 @@ def test(self): "seasonality": "auto", "threshold_type": 0, } + assert DataCondition.objects.filter(id=self.valid_mixed_condition.id).exists() assert DataCondition.objects.filter(id=self.valid_static_condition.id).exists() From 0d7f8928e84532473e7c59767e635894316bc81f Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:26:11 +0000 Subject: [PATCH 4/6] fix(workflow-engine): Restore anomaly alert configuration Co-Authored-By: Kyle Consalus --- ...121_delete_malformed_anomaly_conditions.py | 63 ++++++++++++++----- ...121_delete_malformed_anomaly_conditions.py | 61 +++++++++++++++++- 2 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py index 7f0c6d073999..31f6c54ade4c 100644 --- a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -14,32 +14,54 @@ def delete_malformed_anomaly_conditions( apps: StateApps, schema_editor: BaseDatabaseSchemaEditor ) -> None: + AlertRuleTrigger = apps.get_model("sentry", "AlertRuleTrigger") DataCondition = apps.get_model("workflow_engine", "DataCondition") - anomaly_conditions = DataCondition.objects.filter(type="anomaly_detection", comparison=0.0) - anomaly_condition_group_ids = anomaly_conditions.values_list("condition_group_id", flat=True) - conditions = ( - DataCondition.objects.filter(condition_group_id__in=anomaly_condition_group_ids) - .exclude(type="anomaly_detection") - .values_list("id", flat=True) + DataConditionAlertRuleTrigger = apps.get_model( + "workflow_engine", "DataConditionAlertRuleTrigger" ) + anomaly_condition_ids = DataCondition.objects.filter( + type="anomaly_detection", comparison=0.0 + ).values_list("id", flat=True) for condition_ids in chunked( RangeQuerySetWrapper( - conditions, + anomaly_condition_ids, step=BATCH_SIZE, result_value_getter=lambda condition_id: condition_id, ), BATCH_SIZE, ): - DataCondition.objects.filter(id__in=condition_ids).delete() - - anomaly_conditions.update( - comparison={ - "seasonality": "auto", - "sensitivity": "low", - "threshold_type": 2, - } - ) + condition_ids_by_trigger_id = dict( + DataConditionAlertRuleTrigger.objects.filter( + data_condition_id__in=condition_ids, + alert_rule_trigger_id__isnull=False, + ).values_list("alert_rule_trigger_id", "data_condition_id") + ) + triggers = AlertRuleTrigger.objects.filter( + id__in=condition_ids_by_trigger_id + ).select_related("alert_rule") + + for trigger in triggers: + alert_rule = trigger.alert_rule + if ( + alert_rule.sensitivity is None + or alert_rule.seasonality is None + or alert_rule.threshold_type is None + ): + continue + + condition_id = condition_ids_by_trigger_id[trigger.id] + condition = DataCondition.objects.get(id=condition_id) + DataCondition.objects.filter(condition_group_id=condition.condition_group_id).exclude( + type="anomaly_detection" + ).delete() + DataCondition.objects.filter(id=condition_id).update( + comparison={ + "seasonality": alert_rule.seasonality, + "sensitivity": alert_rule.sensitivity, + "threshold_type": alert_rule.threshold_type, + } + ) class Migration(CheckedMigration): @@ -53,6 +75,13 @@ class Migration(CheckedMigration): migrations.RunPython( delete_malformed_anomaly_conditions, reverse_code=migrations.RunPython.noop, - hints={"tables": ["workflow_engine_datacondition"]}, + hints={ + "tables": [ + "sentry_alertrule", + "sentry_alertruletrigger", + "workflow_engine_datacondition", + "workflow_engine_dataconditionalertruletrigger", + ] + }, ), ] diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py index b4184c2d767d..4186a90adb8a 100644 --- a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -7,8 +7,39 @@ class DeleteMalformedAnomalyConditionsTest(TestMigrations): migrate_to = "0121_delete_malformed_anomaly_conditions" def setup_before_migration(self, apps): + AlertRule = apps.get_model("sentry", "AlertRule") + AlertRuleTrigger = apps.get_model("sentry", "AlertRuleTrigger") DataCondition = apps.get_model("workflow_engine", "DataCondition") + DataConditionAlertRuleTrigger = apps.get_model( + "workflow_engine", "DataConditionAlertRuleTrigger" + ) DataConditionGroup = apps.get_model("workflow_engine", "DataConditionGroup") + SnubaQuery = apps.get_model("sentry", "SnubaQuery") + + snuba_query = SnubaQuery.objects.create( + type=0, + dataset="events", + query="", + aggregate="count()", + time_window=60, + resolution=60, + ) + alert_rule = AlertRule.objects_with_snapshots.create( + organization_id=self.organization.id, + snuba_query_id=snuba_query.id, + name="Malformed anomaly detector", + threshold_type=1, + threshold_period=1, + detection_type="dynamic", + sensitivity="medium", + seasonality="weekly", + ) + alert_rule_trigger = AlertRuleTrigger.objects.create( + alert_rule_id=alert_rule.id, + label="critical", + threshold_type=1, + alert_threshold=0, + ) malformed_group = DataConditionGroup.objects.create(organization_id=self.organization.id) self.anomaly_condition = DataCondition.objects.create( @@ -23,6 +54,24 @@ def setup_before_migration(self, apps): comparison=0, condition_result=0, ) + DataConditionAlertRuleTrigger.objects.create( + data_condition_id=self.anomaly_condition.id, + alert_rule_trigger_id=alert_rule_trigger.id, + ) + + orphaned_group = DataConditionGroup.objects.create(organization_id=self.organization.id) + self.orphaned_anomaly_condition = DataCondition.objects.create( + condition_group_id=orphaned_group.id, + type="anomaly_detection", + comparison=0.0, + condition_result=75, + ) + self.orphaned_mixed_condition = DataCondition.objects.create( + condition_group_id=orphaned_group.id, + type="lte", + comparison=0, + condition_result=0, + ) valid_anomaly_group = DataConditionGroup.objects.create( organization_id=self.organization.id @@ -57,12 +106,18 @@ def test(self): anomaly_condition = DataCondition.objects.get(id=self.anomaly_condition.id) assert anomaly_condition.comparison == { - "seasonality": "auto", - "sensitivity": "low", - "threshold_type": 2, + "seasonality": "weekly", + "sensitivity": "medium", + "threshold_type": 1, } assert not DataCondition.objects.filter(id=self.malformed_condition.id).exists() + orphaned_anomaly_condition = DataCondition.objects.get( + id=self.orphaned_anomaly_condition.id + ) + assert orphaned_anomaly_condition.comparison == 0.0 + assert DataCondition.objects.filter(id=self.orphaned_mixed_condition.id).exists() + valid_anomaly_condition = DataCondition.objects.get(id=self.valid_anomaly_condition.id) assert valid_anomaly_condition.comparison == { "sensitivity": "high", From 00d938cae372bc1d9f14d9694e4e0cb5b5201991 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:35:36 +0000 Subject: [PATCH 5/6] fix(workflow-engine): Preserve duplicate trigger links --- ...121_delete_malformed_anomaly_conditions.py | 25 +++++++++++-------- ...121_delete_malformed_anomaly_conditions.py | 24 ++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py index 31f6c54ade4c..8ef0e4bd4173 100644 --- a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -1,5 +1,7 @@ # Generated by Django 5.2.16 on 2026-09-17 21:56 +from collections import defaultdict + from django.db import migrations from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.migrations.state import StateApps @@ -31,12 +33,13 @@ def delete_malformed_anomaly_conditions( ), BATCH_SIZE, ): - condition_ids_by_trigger_id = dict( - DataConditionAlertRuleTrigger.objects.filter( - data_condition_id__in=condition_ids, - alert_rule_trigger_id__isnull=False, - ).values_list("alert_rule_trigger_id", "data_condition_id") - ) + condition_ids_by_trigger_id = defaultdict(list) + for trigger_id, condition_id in DataConditionAlertRuleTrigger.objects.filter( + data_condition_id__in=condition_ids, + alert_rule_trigger_id__isnull=False, + ).values_list("alert_rule_trigger_id", "data_condition_id"): + condition_ids_by_trigger_id[trigger_id].append(condition_id) + triggers = AlertRuleTrigger.objects.filter( id__in=condition_ids_by_trigger_id ).select_related("alert_rule") @@ -50,12 +53,14 @@ def delete_malformed_anomaly_conditions( ): continue - condition_id = condition_ids_by_trigger_id[trigger.id] - condition = DataCondition.objects.get(id=condition_id) - DataCondition.objects.filter(condition_group_id=condition.condition_group_id).exclude( + conditions = DataCondition.objects.filter( + id__in=condition_ids_by_trigger_id[trigger.id] + ) + condition_group_ids = conditions.values_list("condition_group_id", flat=True) + DataCondition.objects.filter(condition_group_id__in=condition_group_ids).exclude( type="anomaly_detection" ).delete() - DataCondition.objects.filter(id=condition_id).update( + conditions.update( comparison={ "seasonality": alert_rule.seasonality, "sensitivity": alert_rule.sensitivity, diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py index 4186a90adb8a..7772a91e1d25 100644 --- a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -59,6 +59,26 @@ def setup_before_migration(self, apps): alert_rule_trigger_id=alert_rule_trigger.id, ) + second_malformed_group = DataConditionGroup.objects.create( + organization_id=self.organization.id + ) + self.second_anomaly_condition = DataCondition.objects.create( + condition_group_id=second_malformed_group.id, + type="anomaly_detection", + comparison=0.0, + condition_result=75, + ) + self.second_malformed_condition = DataCondition.objects.create( + condition_group_id=second_malformed_group.id, + type="lte", + comparison=0, + condition_result=0, + ) + DataConditionAlertRuleTrigger.objects.create( + data_condition_id=self.second_anomaly_condition.id, + alert_rule_trigger_id=alert_rule_trigger.id, + ) + orphaned_group = DataConditionGroup.objects.create(organization_id=self.organization.id) self.orphaned_anomaly_condition = DataCondition.objects.create( condition_group_id=orphaned_group.id, @@ -112,6 +132,10 @@ def test(self): } assert not DataCondition.objects.filter(id=self.malformed_condition.id).exists() + second_anomaly_condition = DataCondition.objects.get(id=self.second_anomaly_condition.id) + assert second_anomaly_condition.comparison == anomaly_condition.comparison + assert not DataCondition.objects.filter(id=self.second_malformed_condition.id).exists() + orphaned_anomaly_condition = DataCondition.objects.get( id=self.orphaned_anomaly_condition.id ) From a497f5bce8ad8d6dc9d49d83e5de5c508792f17c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:46:09 +0000 Subject: [PATCH 6/6] fix(workflow-engine): Clean incomplete anomaly groups --- ...121_delete_malformed_anomaly_conditions.py | 15 +++--- ...121_delete_malformed_anomaly_conditions.py | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py index 8ef0e4bd4173..ddd4b1239c3a 100644 --- a/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py +++ b/src/sentry/workflow_engine/migrations/0121_delete_malformed_anomaly_conditions.py @@ -45,6 +45,14 @@ def delete_malformed_anomaly_conditions( ).select_related("alert_rule") for trigger in triggers: + conditions = DataCondition.objects.filter( + id__in=condition_ids_by_trigger_id[trigger.id] + ) + condition_group_ids = conditions.values_list("condition_group_id", flat=True) + DataCondition.objects.filter(condition_group_id__in=condition_group_ids).exclude( + type="anomaly_detection" + ).delete() + alert_rule = trigger.alert_rule if ( alert_rule.sensitivity is None @@ -53,13 +61,6 @@ def delete_malformed_anomaly_conditions( ): continue - conditions = DataCondition.objects.filter( - id__in=condition_ids_by_trigger_id[trigger.id] - ) - condition_group_ids = conditions.values_list("condition_group_id", flat=True) - DataCondition.objects.filter(condition_group_id__in=condition_group_ids).exclude( - type="anomaly_detection" - ).delete() conditions.update( comparison={ "seasonality": alert_rule.seasonality, diff --git a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py index 7772a91e1d25..6cf7e9cefdf8 100644 --- a/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py +++ b/tests/sentry/migrations/test_0121_delete_malformed_anomaly_conditions.py @@ -79,6 +79,48 @@ def setup_before_migration(self, apps): alert_rule_trigger_id=alert_rule_trigger.id, ) + incomplete_snuba_query = SnubaQuery.objects.create( + type=0, + dataset="events", + query="", + aggregate="count()", + time_window=60, + resolution=60, + ) + incomplete_alert_rule = AlertRule.objects_with_snapshots.create( + organization_id=self.organization.id, + snuba_query_id=incomplete_snuba_query.id, + name="Incomplete anomaly detector", + threshold_type=1, + threshold_period=1, + detection_type="dynamic", + sensitivity=None, + seasonality="weekly", + ) + incomplete_alert_rule_trigger = AlertRuleTrigger.objects.create( + alert_rule_id=incomplete_alert_rule.id, + label="critical", + threshold_type=1, + alert_threshold=0, + ) + incomplete_group = DataConditionGroup.objects.create(organization_id=self.organization.id) + self.incomplete_anomaly_condition = DataCondition.objects.create( + condition_group_id=incomplete_group.id, + type="anomaly_detection", + comparison=0.0, + condition_result=75, + ) + self.incomplete_mixed_condition = DataCondition.objects.create( + condition_group_id=incomplete_group.id, + type="lte", + comparison=0, + condition_result=0, + ) + DataConditionAlertRuleTrigger.objects.create( + data_condition_id=self.incomplete_anomaly_condition.id, + alert_rule_trigger_id=incomplete_alert_rule_trigger.id, + ) + orphaned_group = DataConditionGroup.objects.create(organization_id=self.organization.id) self.orphaned_anomaly_condition = DataCondition.objects.create( condition_group_id=orphaned_group.id, @@ -136,6 +178,12 @@ def test(self): assert second_anomaly_condition.comparison == anomaly_condition.comparison assert not DataCondition.objects.filter(id=self.second_malformed_condition.id).exists() + incomplete_anomaly_condition = DataCondition.objects.get( + id=self.incomplete_anomaly_condition.id + ) + assert incomplete_anomaly_condition.comparison == 0.0 + assert not DataCondition.objects.filter(id=self.incomplete_mixed_condition.id).exists() + orphaned_anomaly_condition = DataCondition.objects.get( id=self.orphaned_anomaly_condition.id )