Skip to content
Open
2 changes: 1 addition & 1 deletion migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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

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:
AlertRuleTrigger = apps.get_model("sentry", "AlertRuleTrigger")
DataCondition = apps.get_model("workflow_engine", "DataCondition")
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(
anomaly_condition_ids,
step=BATCH_SIZE,
result_value_getter=lambda condition_id: condition_id,
),
BATCH_SIZE,
):
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")

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
or alert_rule.seasonality is None
or alert_rule.threshold_type is None
):
continue

conditions.update(
comparison={
"seasonality": alert_rule.seasonality,
"sensitivity": alert_rule.sensitivity,
"threshold_type": alert_rule.threshold_type,
}
)


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": [
"sentry_alertrule",
"sentry_alertruletrigger",
"workflow_engine_datacondition",
"workflow_engine_dataconditionalertruletrigger",
]
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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):
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(
condition_group_id=malformed_group.id,
type="anomaly_detection",
comparison=0.0,
condition_result=75,
)
self.malformed_condition = DataCondition.objects.create(
condition_group_id=malformed_group.id,
type="lte",
comparison=0,
condition_result=0,
)
DataConditionAlertRuleTrigger.objects.create(
data_condition_id=self.anomaly_condition.id,
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,
)

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,
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
)
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,
)
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(
condition_group_id=static_group.id,
type="lte",
comparison=0,
condition_result=0,
)

def test(self):
DataCondition = self.apps.get_model("workflow_engine", "DataCondition")

anomaly_condition = DataCondition.objects.get(id=self.anomaly_condition.id)
assert anomaly_condition.comparison == {
"seasonality": "weekly",
"sensitivity": "medium",
"threshold_type": 1,
}
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()

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
)
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",
"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()
Loading