Skip to content

Commit adb432f

Browse files
authored
feat(aci milestone 3): issue priority de-escalation condition handler (#89832)
We need this condition handler to fire actions when a metric issue's priority de-escalates. For example, if we go from MEDIUM to HIGH and then back to MEDIUM, we also want to fire the HIGH actions informing users of the status de-escalation. To do so, pass in a comparison priority and the highest-seen priority. The condition evaluates to TRUE if A) the current priority is < the comparison priority and B) the comparison priority is <= the highest-seen priority, so we've fired the action. If we decide to change the behavior of alert firing in the future (if we decide not to fire warning actions when going straight to high), then this logic could break. It could also break if we decide to add in another priority level, like low (because then if we go from high -> low -> medium, the condition evaluates to TRUE on the last transition but we aren't de-escalating).
1 parent 7b6d491 commit adb432f

File tree

4 files changed

+137
-0
lines changed

4 files changed

+137
-0
lines changed

src/sentry/workflow_engine/handlers/condition/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"IssueOccurrencesConditionHandler",
1313
"IssuePriorityCondition",
1414
"IssuePriorityGreaterOrEqualConditionHandler",
15+
"IssuePriorityDeescalatingConditionHandler",
1516
"IssueResolutionConditionHandler",
1617
"LatestAdoptedReleaseConditionHandler",
1718
"LatestReleaseConditionHandler",
@@ -34,6 +35,7 @@
3435
from .first_seen_event_handler import FirstSeenEventConditionHandler
3536
from .issue_category_handler import IssueCategoryConditionHandler
3637
from .issue_occurrences_handler import IssueOccurrencesConditionHandler
38+
from .issue_priority_deescalating_handler import IssuePriorityDeescalatingConditionHandler
3739
from .issue_priority_equals import IssuePriorityCondition
3840
from .issue_priority_greater_or_equal_handler import IssuePriorityGreaterOrEqualConditionHandler
3941
from .issue_resolution_condition_handler import IssueResolutionConditionHandler
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from typing import Any
2+
3+
from sentry.models.group import GroupStatus
4+
from sentry.models.groupopenperiod import GroupOpenPeriod
5+
from sentry.workflow_engine.models.data_condition import Condition
6+
from sentry.workflow_engine.registry import condition_handler_registry
7+
from sentry.workflow_engine.types import DataConditionHandler, WorkflowEventData
8+
9+
10+
@condition_handler_registry.register(Condition.ISSUE_PRIORITY_DEESCALATING)
11+
class IssuePriorityDeescalatingConditionHandler(DataConditionHandler[WorkflowEventData]):
12+
group = DataConditionHandler.Group.ACTION_FILTER
13+
subgroup = DataConditionHandler.Subgroup.ISSUE_ATTRIBUTES
14+
15+
comparison_json_schema = {
16+
"type": "object",
17+
"properties": {
18+
"priority": {"type": "integer", "minimum": 0},
19+
},
20+
"required": ["priority"],
21+
"additionalProperties": False,
22+
}
23+
24+
@staticmethod
25+
def evaluate_value(event_data: WorkflowEventData, comparison: Any) -> bool:
26+
group = event_data.event.group
27+
28+
# we will fire actions on de-escalation if the priority seen is >= the threshold
29+
# priority specified in the comparison
30+
comparison_priority = comparison.get("priority")
31+
current_priority = group.priority
32+
open_period = GroupOpenPeriod.objects.filter(group=group).order_by("-date_started").first()
33+
if open_period is None:
34+
raise Exception("No open period found")
35+
# use this to determine if we've breached the comparison priority before
36+
highest_seen_priority = open_period.data.get("highest_seen_priority", current_priority)
37+
38+
return comparison_priority <= highest_seen_priority and (
39+
current_priority < comparison_priority or group.status == GroupStatus.RESOLVED
40+
)

src/sentry/workflow_engine/models/data_condition.py

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Condition(StrEnum):
4848
TAGGED_EVENT = "tagged_event"
4949
ISSUE_PRIORITY_EQUALS = "issue_priority_equals"
5050
ISSUE_PRIORITY_GREATER_OR_EQUAL = "issue_priority_greater_or_equal"
51+
ISSUE_PRIORITY_DEESCALATING = "issue_priority_deescalating"
5152
ISSUE_RESOLUTION_CHANGE = "issue_resolution_change"
5253

5354
# Event frequency conditions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from sentry.models.group import GroupStatus
2+
from sentry.models.groupopenperiod import GroupOpenPeriod
3+
from sentry.types.group import PriorityLevel
4+
from sentry.users.services.user.service import user_service
5+
from sentry.workflow_engine.migration_helpers.alert_rule import (
6+
migrate_alert_rule,
7+
migrate_metric_data_conditions,
8+
)
9+
from sentry.workflow_engine.models.data_condition import Condition
10+
from sentry.workflow_engine.types import DetectorPriorityLevel, WorkflowEventData
11+
from tests.sentry.workflow_engine.handlers.condition.test_base import ConditionTestCase
12+
13+
14+
class TestIssuePriorityGreaterOrEqualCondition(ConditionTestCase):
15+
condition = Condition.ISSUE_PRIORITY_DEESCALATING
16+
17+
def setUp(self):
18+
super().setUp()
19+
self.event_data = WorkflowEventData(event=self.group_event)
20+
self.metric_alert = self.create_alert_rule()
21+
self.alert_rule_trigger_warning = self.create_alert_rule_trigger(
22+
alert_rule=self.metric_alert, label="warning"
23+
)
24+
self.alert_rule_trigger_critical = self.create_alert_rule_trigger(
25+
alert_rule=self.metric_alert, label="critical"
26+
)
27+
self.rpc_user = user_service.get_user(user_id=self.user.id)
28+
migrate_alert_rule(self.metric_alert, self.rpc_user)
29+
30+
data_condition_warning_tuple = migrate_metric_data_conditions(
31+
self.alert_rule_trigger_warning
32+
)
33+
data_condition_critical_tuple = migrate_metric_data_conditions(
34+
self.alert_rule_trigger_critical
35+
)
36+
37+
assert data_condition_warning_tuple is not None
38+
assert data_condition_critical_tuple is not None
39+
dc_warning = data_condition_warning_tuple[1]
40+
dc_critical = data_condition_critical_tuple[1]
41+
42+
self.deescalating_dc_warning = self.create_data_condition(
43+
comparison={"priority": DetectorPriorityLevel.MEDIUM.value},
44+
type=self.condition,
45+
condition_result=True,
46+
condition_group=dc_warning.condition_group,
47+
)
48+
49+
self.deescalating_dc_critical = self.create_data_condition(
50+
comparison={"priority": DetectorPriorityLevel.HIGH.value},
51+
type=self.condition,
52+
condition_result=True,
53+
condition_group=dc_critical.condition_group,
54+
)
55+
56+
def update_group_and_open_period(self, priority: PriorityLevel) -> None:
57+
self.group.update(priority=priority)
58+
open_period = (
59+
GroupOpenPeriod.objects.filter(group=self.group).order_by("-date_started").first()
60+
)
61+
assert open_period is not None
62+
highest_seen_priority = open_period.data.get("highest_seen_priority", priority)
63+
open_period.data["highest_seen_priority"] = max(highest_seen_priority, priority)
64+
open_period.save()
65+
66+
def test_warning(self):
67+
self.update_group_and_open_period(priority=PriorityLevel.MEDIUM)
68+
self.assert_does_not_pass(self.deescalating_dc_warning, self.event_data)
69+
70+
self.update_group_and_open_period(priority=PriorityLevel.HIGH)
71+
self.assert_does_not_pass(self.deescalating_dc_warning, self.event_data)
72+
73+
self.group.update(status=GroupStatus.RESOLVED)
74+
self.assert_passes(self.deescalating_dc_warning, self.event_data)
75+
76+
def test_critical_threshold_not_breached(self):
77+
self.update_group_and_open_period(priority=PriorityLevel.MEDIUM)
78+
self.assert_does_not_pass(self.deescalating_dc_critical, self.event_data)
79+
80+
self.group.update(status=GroupStatus.RESOLVED)
81+
self.assert_does_not_pass(self.deescalating_dc_critical, self.event_data)
82+
83+
def test_critical(self):
84+
self.update_group_and_open_period(priority=PriorityLevel.MEDIUM)
85+
self.assert_does_not_pass(self.deescalating_dc_critical, self.event_data)
86+
87+
self.update_group_and_open_period(priority=PriorityLevel.HIGH)
88+
self.assert_does_not_pass(self.deescalating_dc_critical, self.event_data)
89+
90+
self.update_group_and_open_period(priority=PriorityLevel.MEDIUM)
91+
self.assert_passes(self.deescalating_dc_critical, self.event_data)
92+
93+
self.group.update(status=GroupStatus.RESOLVED)
94+
self.assert_passes(self.deescalating_dc_critical, self.event_data)

0 commit comments

Comments
 (0)