From 6ba7d9f6d47a66fd86721ef65aa8e06be529e5bb Mon Sep 17 00:00:00 2001 From: Shruthilaya Jaganathan Date: Thu, 3 Jul 2025 13:58:54 -0400 Subject: [PATCH 1/3] feat: Snapshot SnubaQueryEventTypes when updating alert rule --- .../endpoints/serializers/incident.py | 4 +- src/sentry/incidents/logic.py | 10 +++ tests/sentry/incidents/test_logic.py | 67 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/sentry/incidents/endpoints/serializers/incident.py b/src/sentry/incidents/endpoints/serializers/incident.py index 547e8f6e5d97df..03168ca70f384e 100644 --- a/src/sentry/incidents/endpoints/serializers/incident.py +++ b/src/sentry/incidents/endpoints/serializers/incident.py @@ -7,8 +7,8 @@ from sentry.api.serializers import Serializer, register, serialize from sentry.api.serializers.models.incidentactivity import IncidentActivitySerializerResponse from sentry.incidents.endpoints.serializers.alert_rule import ( - AlertRuleSerializer, AlertRuleSerializerResponse, + DetailedAlertRuleSerializer, ) from sentry.incidents.models.incident import Incident, IncidentActivity, IncidentProject from sentry.snuba.entity_subscription import apply_dataset_query_conditions @@ -54,7 +54,7 @@ def get_attrs(self, item_list, user, **kwargs): for d in serialize( {i.alert_rule for i in item_list if i.alert_rule.id}, user, - AlertRuleSerializer(expand=self.expand), + DetailedAlertRuleSerializer(expand=self.expand), ) } diff --git a/src/sentry/incidents/logic.py b/src/sentry/incidents/logic.py index 30eb80ba68be90..3ba2544e7fa832 100644 --- a/src/sentry/incidents/logic.py +++ b/src/sentry/incidents/logic.py @@ -698,6 +698,16 @@ def nullify_id(model: Model) -> None: snuba_query_snapshot: SnubaQuery = deepcopy(_unpack_snuba_query(alert_rule)) nullify_id(snuba_query_snapshot) snuba_query_snapshot.save() + + event_types = SnubaQueryEventType.objects.filter( + snuba_query=_unpack_snuba_query(alert_rule) + ) + for event_type in event_types: + event_type_snapshot = deepcopy(event_type) + nullify_id(event_type_snapshot) + event_type_snapshot.snuba_query = snuba_query_snapshot + event_type_snapshot.save() + alert_rule_snapshot = deepcopy(alert_rule) nullify_id(alert_rule_snapshot) alert_rule_snapshot.status = AlertRuleStatus.SNAPSHOT.value diff --git a/tests/sentry/incidents/test_logic.py b/tests/sentry/incidents/test_logic.py index 79a18e2be74032..d98c5352081368 100644 --- a/tests/sentry/incidents/test_logic.py +++ b/tests/sentry/incidents/test_logic.py @@ -1822,6 +1822,73 @@ def test_update_invalid_time_window(self, mock_seer_request): with pytest.raises(ValidationError): update_alert_rule(rule, detection_type=AlertRuleDetectionType.DYNAMIC, time_window=300) + def test_snapshot_alert_rule_with_event_types(self): + # Create alert rule with event types + alert_rule = create_alert_rule( + self.organization, + [self.project], + "test alert rule", + "severity:error", + "count()", + 1, + AlertRuleThresholdType.ABOVE, + 1, + event_types=[SnubaQueryEventType.EventType.TRACE_ITEM_LOG], + query_type=SnubaQuery.Type.PERFORMANCE, + dataset=Dataset.EventsAnalyticsPlatform, + ) + + # Create incident to trigger snapshot + incident = self.create_incident() + incident.update(alert_rule=alert_rule) + + # Verify original event types exist + original_event_types = SnubaQueryEventType.objects.filter( + snuba_query=alert_rule.snuba_query + ) + assert [snuba_event_type.type for snuba_event_type in original_event_types] == [ + SnubaQueryEventType.EventType.TRACE_ITEM_LOG.value + ] + + # Update alert rule to trigger snapshot + with self.tasks(): + updated_rule = update_alert_rule( + alert_rule, + query="level:warning", + event_types=[SnubaQueryEventType.EventType.TRACE_ITEM_SPAN], + ) + + # Find the snapshot + rule_snapshot = ( + AlertRule.objects_with_snapshots.filter(name=alert_rule.name) + .exclude(id=updated_rule.id) + .get() + ) + + # Verify snapshot has its own SnubaQuery + assert rule_snapshot.snuba_query_id != updated_rule.snuba_query_id + + # Verify snapshot has the original event types + snapshot_event_types = SnubaQueryEventType.objects.filter( + snuba_query=rule_snapshot.snuba_query + ) + assert [snuba_event_type.type for snuba_event_type in snapshot_event_types] == [ + SnubaQueryEventType.EventType.TRACE_ITEM_LOG.value + ] + + # Verify event types are different objects but have same values + original_types = {snuba_event_type.type for snuba_event_type in original_event_types} + snapshot_types = {snuba_event_type.type for snuba_event_type in snapshot_event_types} + assert original_types == snapshot_types + + # Verify updated rule has new event types + updated_event_types = SnubaQueryEventType.objects.filter( + snuba_query=updated_rule.snuba_query + ) + assert [snuba_event_type.type for snuba_event_type in updated_event_types] == [ + SnubaQueryEventType.EventType.TRACE_ITEM_SPAN.value + ] + class DeleteAlertRuleTest(TestCase, BaseIncidentsTest): def setUp(self): From 41e422ecc45ef840d6618633b3bac5664e049670 Mon Sep 17 00:00:00 2001 From: Shruthilaya Jaganathan Date: Thu, 3 Jul 2025 14:36:21 -0400 Subject: [PATCH 2/3] fix incident serializer tests --- .../endpoints/serializers/test_incident.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/sentry/incidents/endpoints/serializers/test_incident.py b/tests/sentry/incidents/endpoints/serializers/test_incident.py index fad3ba967b7321..87ce4685cc40f0 100644 --- a/tests/sentry/incidents/endpoints/serializers/test_incident.py +++ b/tests/sentry/incidents/endpoints/serializers/test_incident.py @@ -3,6 +3,7 @@ from django.utils import timezone from sentry.api.serializers import serialize +from sentry.incidents.endpoints.serializers.alert_rule import DetailedAlertRuleSerializer from sentry.incidents.endpoints.serializers.incident import DetailedIncidentSerializer from sentry.snuba.dataset import Dataset from sentry.testutils.cases import TestCase @@ -36,7 +37,10 @@ def test_error_alert_rule(self): serializer = DetailedIncidentSerializer() result = serialize(incident, serializer=serializer) - assert result["alertRule"] == serialize(incident.alert_rule) + alert_rule_serializer = DetailedAlertRuleSerializer() + assert result["alertRule"] == serialize( + incident.alert_rule, serializer=alert_rule_serializer + ) assert result["discoverQuery"] == f"(event.type:error) AND ({query})" def test_error_alert_rule_unicode(self): @@ -45,7 +49,11 @@ def test_error_alert_rule_unicode(self): serializer = DetailedIncidentSerializer() result = serialize(incident, serializer=serializer) - assert result["alertRule"] == serialize(incident.alert_rule) + + alert_rule_serializer = DetailedAlertRuleSerializer() + assert result["alertRule"] == serialize( + incident.alert_rule, serializer=alert_rule_serializer + ) assert result["discoverQuery"] == f"(event.type:error) AND ({query})" def test_transaction_alert_rule(self): @@ -55,5 +63,8 @@ def test_transaction_alert_rule(self): serializer = DetailedIncidentSerializer() result = serialize(incident, serializer=serializer) - assert result["alertRule"] == serialize(incident.alert_rule) + alert_rule_serializer = DetailedAlertRuleSerializer() + assert result["alertRule"] == serialize( + incident.alert_rule, serializer=alert_rule_serializer + ) assert result["discoverQuery"] == f"(event.type:transaction) AND ({query})" From 5d12928959e779f69e70558a60e50e6e2d232102 Mon Sep 17 00:00:00 2001 From: Shruthilaya Jaganathan Date: Mon, 7 Jul 2025 16:37:54 -0400 Subject: [PATCH 3/3] bulk create --- src/sentry/incidents/logic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry/incidents/logic.py b/src/sentry/incidents/logic.py index 3ba2544e7fa832..d6279b49edb231 100644 --- a/src/sentry/incidents/logic.py +++ b/src/sentry/incidents/logic.py @@ -702,11 +702,14 @@ def nullify_id(model: Model) -> None: event_types = SnubaQueryEventType.objects.filter( snuba_query=_unpack_snuba_query(alert_rule) ) + new_event_snapshots = [] for event_type in event_types: event_type_snapshot = deepcopy(event_type) nullify_id(event_type_snapshot) event_type_snapshot.snuba_query = snuba_query_snapshot - event_type_snapshot.save() + new_event_snapshots.append(event_type_snapshot) + + SnubaQueryEventType.objects.bulk_create(new_event_snapshots) alert_rule_snapshot = deepcopy(alert_rule) nullify_id(alert_rule_snapshot)