11
11
from sentry .incidents .models .alert_rule import AlertRuleDetectionType
12
12
from sentry .incidents .utils .constants import INCIDENTS_SNUBA_SUBSCRIPTION_TYPE
13
13
from sentry .models .environment import Environment
14
+ from sentry .seer .anomaly_detection .types import (
15
+ AnomalyDetectionSeasonality ,
16
+ AnomalyDetectionSensitivity ,
17
+ AnomalyDetectionThresholdType ,
18
+ )
14
19
from sentry .snuba .dataset import Dataset
15
20
from sentry .snuba .models import (
16
21
QuerySubscription ,
26
31
27
32
28
33
class MetricIssueComparisonConditionValidatorTest (BaseValidatorTest ):
34
+ def setUp (self ):
35
+ super ().setUp ()
36
+ self .valid_data = {
37
+ "type" : Condition .GREATER ,
38
+ "comparison" : 100 ,
39
+ "conditionResult" : DetectorPriorityLevel .HIGH ,
40
+ "conditionGroupId" : self .data_condition_group .id ,
41
+ }
42
+
29
43
def test (self ):
30
- validator = MetricIssueComparisonConditionValidator (
31
- data = {
32
- "type" : Condition .GREATER ,
33
- "comparison" : 100 ,
34
- "conditionResult" : DetectorPriorityLevel .HIGH ,
35
- "conditionGroupId" : self .data_condition_group .id ,
36
- }
37
- )
44
+ validator = MetricIssueComparisonConditionValidator (data = self .valid_data )
38
45
assert validator .is_valid ()
39
46
assert validator .validated_data == {
40
47
"comparison" : 100.0 ,
@@ -46,9 +53,8 @@ def test(self):
46
53
def test_invalid_condition (self ):
47
54
unsupported_condition = Condition .EQUAL
48
55
data = {
56
+ ** self .valid_data ,
49
57
"type" : unsupported_condition ,
50
- "comparison" : 100 ,
51
- "result" : DetectorPriorityLevel .HIGH ,
52
58
}
53
59
validator = MetricIssueComparisonConditionValidator (data = data )
54
60
assert not validator .is_valid ()
@@ -58,7 +64,7 @@ def test_invalid_condition(self):
58
64
59
65
def test_unregistered_condition (self ):
60
66
validator = MetricIssueComparisonConditionValidator (
61
- data = {"type" : "invalid" , "comparison" : 100 , "result" : DetectorPriorityLevel . HIGH }
67
+ data = {** self . valid_data , "type" : "invalid" }
62
68
)
63
69
assert not validator .is_valid ()
64
70
assert validator .errors .get ("type" ) == [
@@ -68,23 +74,36 @@ def test_unregistered_condition(self):
68
74
def test_invalid_comparison (self ):
69
75
validator = MetricIssueComparisonConditionValidator (
70
76
data = {
71
- "type" : Condition . GREATER ,
77
+ ** self . valid_data ,
72
78
"comparison" : "not_a_number" ,
73
- "result" : DetectorPriorityLevel .HIGH ,
74
79
}
75
80
)
76
81
assert not validator .is_valid ()
77
82
assert validator .errors .get ("comparison" ) == [
78
- ErrorDetail (string = "A valid number is required." , code = "invalid" )
83
+ ErrorDetail (string = "A valid number or dict is required." , code = "invalid" )
84
+ ]
85
+
86
+ def test_invalid_comparison_dict (self ):
87
+ comparison = {"foo" : "bar" }
88
+ validator = MetricIssueComparisonConditionValidator (
89
+ data = {
90
+ ** self .valid_data ,
91
+ "comparison" : comparison ,
92
+ }
93
+ )
94
+ assert not validator .is_valid ()
95
+ assert validator .errors .get ("comparison" ) == [
96
+ ErrorDetail (
97
+ string = f"Invalid json primitive value: { comparison } . Must be a string, number, or boolean." ,
98
+ code = "invalid" ,
99
+ )
79
100
]
80
101
81
102
def test_invalid_result (self ):
82
103
validator = MetricIssueComparisonConditionValidator (
83
104
data = {
84
- "type" : Condition .GREATER ,
85
- "comparison" : 100 ,
86
- "condition_result" : 25 ,
87
- "condition_group_id" : self .data_condition_group .id ,
105
+ ** self .valid_data ,
106
+ "conditionResult" : 25 ,
88
107
}
89
108
)
90
109
assert not validator .is_valid ()
@@ -109,13 +128,13 @@ def setUp(self):
109
128
"name" : "Test Detector" ,
110
129
"type" : MetricIssue .slug ,
111
130
"dataSource" : {
112
- "query_type " : SnubaQuery .Type .ERROR .value ,
131
+ "queryType " : SnubaQuery .Type .ERROR .value ,
113
132
"dataset" : Dataset .Events .value ,
114
133
"query" : "test query" ,
115
134
"aggregate" : "count()" ,
116
- "time_window " : 3600 ,
135
+ "timeWindow " : 3600 ,
117
136
"environment" : self .environment .name ,
118
- "event_types " : [SnubaQueryEventType .EventType .ERROR .name .lower ()],
137
+ "eventTypes " : [SnubaQueryEventType .EventType .ERROR .name .lower ()],
119
138
},
120
139
"conditionGroup" : {
121
140
"id" : self .data_condition_group .id ,
@@ -131,23 +150,12 @@ def setUp(self):
131
150
],
132
151
},
133
152
"config" : {
134
- "threshold_period " : 1 ,
135
- "detection_type " : AlertRuleDetectionType .STATIC .value ,
153
+ "thresholdPeriod " : 1 ,
154
+ "detectionType " : AlertRuleDetectionType .STATIC .value ,
136
155
},
137
156
}
138
157
139
- @mock .patch ("sentry.workflow_engine.endpoints.validators.base.detector.create_audit_entry" )
140
- def test_create_with_valid_data (self , mock_audit ):
141
- validator = MetricIssueDetectorValidator (
142
- data = self .valid_data ,
143
- context = self .context ,
144
- )
145
- assert validator .is_valid (), validator .errors
146
-
147
- with self .tasks ():
148
- detector = validator .save ()
149
-
150
- # Verify detector in DB
158
+ def assert_validated (self , detector ):
151
159
detector = Detector .objects .get (id = detector .id )
152
160
assert detector .name == "Test Detector"
153
161
assert detector .type == MetricIssue .slug
@@ -175,6 +183,19 @@ def test_create_with_valid_data(self, mock_audit):
175
183
assert snuba_query .environment == self .environment
176
184
assert snuba_query .event_types == [SnubaQueryEventType .EventType .ERROR ]
177
185
186
+ @mock .patch ("sentry.workflow_engine.endpoints.validators.base.detector.create_audit_entry" )
187
+ def test_create_with_valid_data (self , mock_audit ):
188
+ validator = MetricIssueDetectorValidator (
189
+ data = self .valid_data ,
190
+ context = self .context ,
191
+ )
192
+ assert validator .is_valid (), validator .errors
193
+
194
+ with self .tasks ():
195
+ detector = validator .save ()
196
+
197
+ # Verify detector in DB
198
+ self .assert_validated (detector )
178
199
# Verify condition group in DB
179
200
condition_group = DataConditionGroup .objects .get (id = detector .workflow_condition_group_id )
180
201
assert condition_group .logic_type == DataConditionGroup .Type .ANY
@@ -197,6 +218,102 @@ def test_create_with_valid_data(self, mock_audit):
197
218
data = detector .get_audit_log_data (),
198
219
)
199
220
221
+ @mock .patch ("sentry.workflow_engine.endpoints.validators.base.detector.create_audit_entry" )
222
+ def test_anomaly_detection (self , mock_audit ):
223
+ data = {
224
+ ** self .valid_data ,
225
+ "conditionGroup" : {
226
+ "id" : self .data_condition_group .id ,
227
+ "organizationId" : self .organization .id ,
228
+ "logicType" : self .data_condition_group .logic_type ,
229
+ "conditions" : [
230
+ {
231
+ "type" : Condition .ANOMALY_DETECTION ,
232
+ "comparison" : {
233
+ "sensitivity" : AnomalyDetectionSensitivity .HIGH ,
234
+ "seasonality" : AnomalyDetectionSeasonality .AUTO ,
235
+ "threshold_type" : AnomalyDetectionThresholdType .ABOVE_AND_BELOW ,
236
+ },
237
+ "conditionResult" : DetectorPriorityLevel .HIGH ,
238
+ "conditionGroupId" : self .data_condition_group .id ,
239
+ },
240
+ ],
241
+ },
242
+ "config" : {
243
+ "threshold_period" : 1 ,
244
+ "detection_type" : AlertRuleDetectionType .DYNAMIC .value ,
245
+ },
246
+ }
247
+ validator = MetricIssueDetectorValidator (
248
+ data = data ,
249
+ context = self .context ,
250
+ )
251
+ assert validator .is_valid (), validator .errors
252
+
253
+ with self .tasks ():
254
+ detector = validator .save ()
255
+
256
+ # Verify detector in DB
257
+ self .assert_validated (detector )
258
+
259
+ # Verify condition group in DB
260
+ condition_group = DataConditionGroup .objects .get (id = detector .workflow_condition_group_id )
261
+ assert condition_group .logic_type == DataConditionGroup .Type .ANY
262
+ assert condition_group .organization_id == self .project .organization_id
263
+
264
+ # Verify conditions in DB
265
+ conditions = list (DataCondition .objects .filter (condition_group = condition_group ))
266
+ assert len (conditions ) == 1
267
+
268
+ condition = conditions [0 ]
269
+ assert condition .type == Condition .ANOMALY_DETECTION
270
+ assert condition .comparison == {
271
+ "sensitivity" : AnomalyDetectionSensitivity .HIGH ,
272
+ "seasonality" : AnomalyDetectionSeasonality .AUTO ,
273
+ "threshold_type" : AnomalyDetectionThresholdType .ABOVE_AND_BELOW ,
274
+ }
275
+ assert condition .condition_result == DetectorPriorityLevel .HIGH
276
+
277
+ # Verify audit log
278
+ mock_audit .assert_called_once_with (
279
+ request = self .context ["request" ],
280
+ organization = self .project .organization ,
281
+ target_object = detector .id ,
282
+ event = audit_log .get_event_id ("DETECTOR_ADD" ),
283
+ data = detector .get_audit_log_data (),
284
+ )
285
+
286
+ def test_anomaly_detection__invalid_comparison (self ):
287
+ data = {
288
+ ** self .valid_data ,
289
+ "conditionGroup" : {
290
+ "id" : self .data_condition_group .id ,
291
+ "organizationId" : self .organization .id ,
292
+ "logicType" : self .data_condition_group .logic_type ,
293
+ "conditions" : [
294
+ {
295
+ "type" : Condition .ANOMALY_DETECTION ,
296
+ "comparison" : {
297
+ "sensitivity" : "super sensitive" ,
298
+ "seasonality" : AnomalyDetectionSeasonality .AUTO ,
299
+ "threshold_type" : AnomalyDetectionThresholdType .ABOVE_AND_BELOW ,
300
+ },
301
+ "conditionResult" : DetectorPriorityLevel .HIGH ,
302
+ "conditionGroupId" : self .data_condition_group .id ,
303
+ },
304
+ ],
305
+ },
306
+ "config" : {
307
+ "threshold_period" : 1 ,
308
+ "detection_type" : AlertRuleDetectionType .DYNAMIC .value ,
309
+ },
310
+ }
311
+ validator = MetricIssueDetectorValidator (
312
+ data = data ,
313
+ context = self .context ,
314
+ )
315
+ assert not validator .is_valid ()
316
+
200
317
def test_invalid_detector_type (self ):
201
318
data = {** self .valid_data , "type" : "invalid_type" }
202
319
validator = MetricIssueDetectorValidator (data = data , context = self .context )
0 commit comments