11import copy
22from datetime import timedelta
33from functools import cached_property
4- from unittest .mock import call , patch
4+ from unittest .mock import MagicMock , call , patch
55
66from django .utils import timezone
77
8+ from sentry .constants import ObjectStatus
9+ from sentry .incidents .subscription_processor import SubscriptionProcessor
10+ from sentry .snuba .dataset import Dataset
811from sentry .testutils .factories import DEFAULT_EVENT_DATA
912from sentry .workflow_engine .models .data_condition import Condition , DataCondition
1013from sentry .workflow_engine .types import DetectorPriorityLevel
@@ -18,80 +21,158 @@ class ProcessUpdateTest(ProcessUpdateBaseClass):
1821 Test early return scenarios + simple cases.
1922 """
2023
21- # TODO: tests for early return scenarios. These will need to be added once
22- # we've decoupled the subscription processor from the alert rule model.
23-
2424 def test_simple (self ) -> None :
2525 """
2626 Verify that an alert can trigger.
2727 """
2828 self .send_update (self .critical_threshold + 1 )
2929 assert self .get_detector_state (self .metric_detector ) == DetectorPriorityLevel .HIGH
3030
31+ @patch ("sentry.incidents.subscription_processor.metrics" )
32+ def test_missing_project (self , mock_metrics : MagicMock ) -> None :
33+ metrics_call = "incidents.alert_rules.ignore_deleted_project"
34+
35+ self .sub .project .update (status = ObjectStatus .PENDING_DELETION )
36+ self .sub .project .save ()
37+
38+ assert self .send_update (self .critical_threshold + 1 ) is False
39+ mock_metrics .incr .assert_has_calls ([call (metrics_call )])
40+
41+ mock_metrics .reset_mock ()
42+
43+ self .sub .project .delete ()
44+ assert self .send_update (self .critical_threshold + 1 ) is False
45+ mock_metrics .incr .assert_has_calls ([call (metrics_call )])
46+
47+ @patch ("sentry.incidents.subscription_processor.metrics" )
48+ def test_has_downgraded_incidents (self , mock_metrics : MagicMock ) -> None :
49+ processor = SubscriptionProcessor (self .sub )
50+ message = self .build_subscription_update (
51+ self .sub , value = self .critical_threshold + 1 , time_delta = timedelta ()
52+ )
53+
54+ with self .capture_on_commit_callbacks (execute = True ):
55+ assert processor .process_update (message ) is False
56+ mock_metrics .incr .assert_has_calls (
57+ [call ("incidents.alert_rules.ignore_update_missing_incidents" )]
58+ )
59+
60+ @patch ("sentry.incidents.subscription_processor.metrics" )
61+ def test_has_downgraded_incidents_performance (self , mock_metrics : MagicMock ) -> None :
62+ snuba_query = self .get_snuba_query (self .detector )
63+ snuba_query .update (time_window = 15 * 60 , dataset = Dataset .Transactions .value )
64+ snuba_query .save ()
65+
66+ processor = SubscriptionProcessor (self .sub )
67+ message = self .build_subscription_update (
68+ self .sub , value = self .critical_threshold + 1 , time_delta = timedelta ()
69+ )
70+
71+ with self .capture_on_commit_callbacks (execute = True ):
72+ assert processor .process_update (message ) is False
73+ mock_metrics .incr .assert_has_calls (
74+ [call ("incidents.alert_rules.ignore_update_missing_incidents_performance" )]
75+ )
76+
77+ @patch ("sentry.incidents.subscription_processor.metrics" )
78+ def test_has_downgraded_on_demand (self , mock_metrics : MagicMock ) -> None :
79+ snuba_query = self .get_snuba_query (self .detector )
80+ snuba_query .update (time_window = 15 * 60 , dataset = Dataset .PerformanceMetrics .value )
81+ snuba_query .save ()
82+
83+ processor = SubscriptionProcessor (self .sub )
84+ message = self .build_subscription_update (
85+ self .sub , value = self .critical_threshold + 1 , time_delta = timedelta ()
86+ )
87+
88+ with self .capture_on_commit_callbacks (execute = True ):
89+ assert processor .process_update (message ) is False
90+ mock_metrics .incr .assert_has_calls (
91+ [call ("incidents.alert_rules.ignore_update_missing_on_demand" )]
92+ )
93+
94+ @patch ("sentry.incidents.subscription_processor.metrics" )
95+ def test_skip_already_processed_update (self , mock_metrics : MagicMock ) -> None :
96+ assert self .send_update (value = self .critical_threshold + 1 ) is True
97+ mock_metrics .incr .reset_mock ()
98+ assert self .send_update (value = self .critical_threshold + 1 ) is False
99+
100+ mock_metrics .incr .assert_called_once_with (
101+ "incidents.alert_rules.skipping_already_processed_update"
102+ )
103+ mock_metrics .incr .reset_mock ()
104+ assert (
105+ self .send_update (value = self .critical_threshold + 1 , time_delta = timedelta (hours = 1 ))
106+ is True
107+ )
108+
109+ mock_metrics .incr .assert_called_once_with ("incidents.alert_rules.process_update.start" )
110+
31111 def test_resolve (self ) -> None :
32- detector = self .metric_detector
33112 self .send_update (self .critical_threshold + 1 , timedelta (minutes = - 2 ))
34- assert self .get_detector_state (detector ) == DetectorPriorityLevel .HIGH
113+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .HIGH
35114
36115 self .send_update (self .resolve_threshold - 1 , timedelta (minutes = - 1 ))
37- assert self .get_detector_state (detector ) == DetectorPriorityLevel .OK
116+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .OK
38117
39118 def test_resolve_percent_boundary (self ) -> None :
40- detector = self .metric_detector
41- self .update_threshold (detector , DetectorPriorityLevel .HIGH , 0.5 )
42- self .update_threshold (detector , DetectorPriorityLevel .OK , 0.5 )
119+ self .update_threshold (self .detector , DetectorPriorityLevel .HIGH , 0.5 )
120+ self .update_threshold (self .detector , DetectorPriorityLevel .OK , 0.5 )
43121 self .send_update (self .critical_threshold + 0.1 , timedelta (minutes = - 2 ))
44- assert self .get_detector_state (detector ) == DetectorPriorityLevel .HIGH
122+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .HIGH
45123
46124 self .send_update (self .resolve_threshold , timedelta (minutes = - 1 ))
47- assert self .get_detector_state (detector ) == DetectorPriorityLevel .OK
125+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .OK
48126
49127 def test_reversed (self ) -> None :
50128 """
51129 Test that resolutions work when the threshold is reversed.
52130 """
53- detector = self .metric_detector
54- DataCondition .objects .filter (condition_group = detector .workflow_condition_group ).delete ()
55- self .set_up_data_conditions (detector , Condition .LESS , 100 , None , 100 )
131+ DataCondition .objects .filter (
132+ condition_group = self .detector .workflow_condition_group
133+ ).delete ()
134+ self .set_up_data_conditions (self .detector , Condition .LESS , 100 , None , 100 )
56135 self .send_update (self .critical_threshold - 1 , timedelta (minutes = - 2 ))
57- assert self .get_detector_state (detector ) == DetectorPriorityLevel .HIGH
136+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .HIGH
58137
59138 self .send_update (self .resolve_threshold , timedelta (minutes = - 1 ))
60- assert self .get_detector_state (detector ) == DetectorPriorityLevel .OK
139+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .OK
61140
62141 def test_multiple_triggers (self ) -> None :
63- detector = self .metric_detector
64- DataCondition .objects .filter (condition_group = detector .workflow_condition_group ).delete ()
65- self .set_up_data_conditions (detector , Condition .GREATER , 100 , 50 , 50 )
142+ DataCondition .objects .filter (
143+ condition_group = self .detector .workflow_condition_group
144+ ).delete ()
145+ self .set_up_data_conditions (self .detector , Condition .GREATER , 100 , 50 , 50 )
66146
67147 self .send_update (self .warning_threshold + 1 , timedelta (minutes = - 5 ))
68- assert self .get_detector_state (detector ) == DetectorPriorityLevel .MEDIUM
148+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .MEDIUM
69149
70150 self .send_update (self .critical_threshold + 1 , timedelta (minutes = - 4 ))
71- assert self .get_detector_state (detector ) == DetectorPriorityLevel .HIGH
151+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .HIGH
72152
73153 self .send_update (self .critical_threshold - 1 , timedelta (minutes = - 3 ))
74- assert self .get_detector_state (detector ) == DetectorPriorityLevel .MEDIUM
154+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .MEDIUM
75155
76156 self .send_update (self .warning_threshold - 1 , timedelta (minutes = - 2 ))
77- assert self .get_detector_state (detector ) == DetectorPriorityLevel .OK
157+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .OK
78158
79159 def test_multiple_triggers_reversed (self ) -> None :
80- detector = self .metric_detector
81- DataCondition .objects .filter (condition_group = detector .workflow_condition_group ).delete ()
82- self .set_up_data_conditions (detector , Condition .LESS , 50 , 100 , 100 )
160+ DataCondition .objects .filter (
161+ condition_group = self .detector .workflow_condition_group
162+ ).delete ()
163+ self .set_up_data_conditions (self .detector , Condition .LESS , 50 , 100 , 100 )
83164
84165 self .send_update (self .warning_threshold - 1 , timedelta (minutes = - 5 ))
85- assert self .get_detector_state (detector ) == DetectorPriorityLevel .MEDIUM
166+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .MEDIUM
86167
87168 self .send_update (self .critical_threshold - 1 , timedelta (minutes = - 4 ))
88- assert self .get_detector_state (detector ) == DetectorPriorityLevel .HIGH
169+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .HIGH
89170
90171 self .send_update (self .critical_threshold + 1 , timedelta (minutes = - 3 ))
91- assert self .get_detector_state (detector ) == DetectorPriorityLevel .MEDIUM
172+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .MEDIUM
92173
93174 self .send_update (self .warning_threshold + 1 , timedelta (minutes = - 2 ))
94- assert self .get_detector_state (detector ) == DetectorPriorityLevel .OK
175+ assert self .get_detector_state (self . detector ) == DetectorPriorityLevel .OK
95176
96177 # TODO: the subscription processor has a 10 minute cooldown period for creating new incidents
97178 # We probably need similar logic within workflow engine.
@@ -100,25 +181,25 @@ def test_multiple_triggers_reversed(self) -> None:
100181class ProcessUpdateComparisonAlertTest (ProcessUpdateBaseClass ):
101182 @cached_property
102183 def comparison_detector_above (self ):
103- detector = self .metric_detector
104- detector .config .update ({"comparison_delta" : 60 * 60 })
105- detector .save ()
106- self .update_threshold (detector , DetectorPriorityLevel .HIGH , 150 )
107- self .update_threshold (detector , DetectorPriorityLevel .OK , 150 )
108- snuba_query = self .get_snuba_query (detector )
109- snuba_query .update (time_window = 60 * 60 )
110- return detector
184+ self .detector .config .update ({"comparison_delta" : 60 * 60 })
185+ self .detector .save ()
186+ self .update_threshold (self .detector , DetectorPriorityLevel .HIGH , 150 )
187+ self .update_threshold (self .detector , DetectorPriorityLevel .OK , 150 )
188+ self .snuba_query = self .get_snuba_query (self .detector )
189+ self .snuba_query .update (time_window = 60 * 60 )
190+ return self .detector
111191
112192 @cached_property
113193 def comparison_detector_below (self ):
114- detector = self .metric_detector
115- detector .config .update ({"comparison_delta" : 60 * 60 })
116- detector .save ()
117- DataCondition .objects .filter (condition_group = detector .workflow_condition_group ).delete ()
118- self .set_up_data_conditions (detector , Condition .LESS , 50 , None , 50 )
119- snuba_query = self .get_snuba_query (detector )
194+ self .detector .config .update ({"comparison_delta" : 60 * 60 })
195+ self .detector .save ()
196+ DataCondition .objects .filter (
197+ condition_group = self .detector .workflow_condition_group
198+ ).delete ()
199+ self .set_up_data_conditions (self .detector , Condition .LESS , 50 , None , 50 )
200+ snuba_query = self .get_snuba_query (self .detector )
120201 snuba_query .update (time_window = 60 * 60 )
121- return detector
202+ return self . detector
122203
123204 @patch ("sentry.incidents.utils.process_update_helpers.metrics" )
124205 def test_comparison_alert_above (self , helper_metrics ):
0 commit comments