Browse Source

feat(severity): Create a new "high priority" alert (#61186)

Adds a new alert condition, HIghPriorityIssueCondition, which evaluated
to true if:
- an issue is high severity (> 0.1 severity score)
- an archived issue escalatings
- a new issue (< 1 day old) is detected as escalating 

<img width="977" alt="image"
src="https://github.com/getsentry/sentry/assets/16563948/d8b81123-c326-42db-995b-a641c7aff5c4">


<img width="337" alt="image"
src="https://github.com/getsentry/sentry/assets/16563948/f8b115f3-c9bb-487c-9869-97de9e48a496">

Up next - further testing in a personal project, checking the email
formatting and reason

Fixes https://github.com/getsentry/sentry/issues/59872
Snigdha Sharma 1 year ago
parent
commit
4922319ccf

+ 7 - 0
src/sentry/api/endpoints/project_rules_configuration.py

@@ -32,6 +32,7 @@ class ProjectRulesConfigurationEndpoint(ProjectEndpoint):
             "organizations:integrations-ticket-rules", project.organization
         )
         has_issue_severity_alerts = features.has("projects:first-event-severity-alerting", project)
+        has_high_priority_issue_alert = features.has("projects:high-priority-alerts", project)
 
         # TODO: conditions need to be based on actions
         for rule_type, rule_cls in rules:
@@ -72,6 +73,12 @@ class ProjectRulesConfigurationEndpoint(ProjectEndpoint):
                 continue
 
             if rule_type.startswith("condition/"):
+                if (
+                    context["id"]
+                    == "sentry.rules.conditions.high_priority_issue.HighPriorityIssueCondition"
+                    and not has_high_priority_issue_alert
+                ):
+                    continue
                 condition_list.append(context)
             elif rule_type.startswith("filter/"):
                 if (

+ 2 - 0
src/sentry/conf/server.py

@@ -1919,6 +1919,8 @@ SENTRY_FEATURES: dict[str, bool | None] = {
     "projects:first-event-severity-calculation": False,
     # Enable escalation detection for new issues
     "projects:first-event-severity-new-escalation": False,
+    # Enable severity alerts for new issues based on severity and escalation
+    "projects:high-priority-alerts": False,
     # Enable functionality for attaching  minidumps to events and displaying
     # then in the group UI.
     "projects:minidump": True,

+ 1 - 0
src/sentry/constants.py

@@ -263,6 +263,7 @@ _SENTRY_RULES = (
     "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition",
     "sentry.rules.conditions.regression_event.RegressionEventCondition",
     "sentry.rules.conditions.reappeared_event.ReappearedEventCondition",
+    "sentry.rules.conditions.high_priority_issue.HighPriorityIssueCondition",
     "sentry.rules.conditions.tagged_event.TaggedEventCondition",
     "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
     "sentry.rules.conditions.event_frequency.EventUniqueUserFrequencyCondition",

+ 1 - 0
src/sentry/features/__init__.py

@@ -285,6 +285,7 @@ default_manager.add("projects:alert-filters", ProjectFeature, FeatureHandlerStra
 default_manager.add("projects:first-event-severity-alerting", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("projects:first-event-severity-calculation", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("projects:first-event-severity-new-escalation", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
+default_manager.add("projects:high-priority-alerts", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("projects:minidump", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("projects:race-free-group-creation", ProjectFeature, FeatureHandlerStrategy.INTERNAL)
 default_manager.add("projects:similarity-indexing", ProjectFeature, FeatureHandlerStrategy.INTERNAL)

+ 62 - 0
src/sentry/rules/conditions/high_priority_issue.py

@@ -0,0 +1,62 @@
+from datetime import datetime
+from typing import Optional, Sequence
+
+from sentry import features
+from sentry.eventstore.models import GroupEvent
+from sentry.models.activity import Activity
+from sentry.models.group import Group
+from sentry.rules import EventState
+from sentry.rules.conditions.base import EventCondition
+from sentry.types.activity import ActivityType
+from sentry.types.condition_activity import ConditionActivity, ConditionActivityType
+
+HIGH_SEVERITY_THRESHOLD = 0.1
+
+
+class HighPriorityIssueCondition(EventCondition):
+    id = "sentry.rules.conditions.high_priority_issue.HighPriorityIssueCondition"
+    label = "Sentry marks an issue as high priority"
+
+    def is_high_severity(self, state: EventState, group: Optional[Group]) -> bool:
+        if not group:
+            return False
+
+        try:
+            severity = float(group.get_event_metadata().get("severity", ""))
+        except (KeyError, TypeError, ValueError):
+            return False
+
+        return severity >= HIGH_SEVERITY_THRESHOLD
+
+    def passes(self, event: GroupEvent, state: EventState) -> bool:
+        has_issue_priority_alerts = features.has("projects:high-priority-alerts", self.project)
+        if not has_issue_priority_alerts:
+            return False
+
+        is_high_severity = self.is_high_severity(state, event.group)
+        is_escalating = state.has_reappeared or state.has_escalated
+
+        return is_high_severity or is_escalating
+
+    def get_activity(
+        self, start: datetime, end: datetime, limit: int
+    ) -> Sequence[ConditionActivity]:
+        # reappearances are recorded as SET_UNRESOLVED with no user
+        activities = (
+            Activity.objects.filter(
+                project=self.project,
+                datetime__gte=start,
+                datetime__lt=end,
+                type__in=[ActivityType.SET_UNRESOLVED.value, ActivityType.SET_ESCALATING.value],
+                user_id=None,
+            )
+            .order_by("-datetime")[:limit]
+            .values_list("group", "datetime", "data")
+        )
+
+        return [
+            ConditionActivity(
+                group_id=a[0], type=ConditionActivityType.REAPPEARED, timestamp=a[1], data=a[2]
+            )
+            for a in activities
+        ]

+ 1 - 1
tests/sentry/api/endpoints/test_project_agnostic_rule_conditions.py

@@ -13,4 +13,4 @@ class ProjectAgnosticRuleConditionsTest(APITestCase):
         response = self.client.get(url, format="json")
 
         assert response.status_code == 200, response.content
-        assert len(response.data) == 10
+        assert len(response.data) == 11

+ 15 - 0
tests/sentry/api/endpoints/test_project_rules_configuration.py

@@ -198,3 +198,18 @@ class ProjectRuleConfigurationTest(APITestCase):
             assert "sentry.rules.filters.issue_severity.IssueSeverityFilter" in [
                 filter["id"] for filter in response.data["filters"]
             ]
+
+    def test_high_priority_issue_condition_feature(self):
+        # Hide the high priority issue condition when high-priority-alerts is off
+        with self.feature({"projects:high-priority-alerts": False}):
+            response = self.get_success_response(self.organization.slug, self.project.slug)
+            assert "sentry.rules.conditions.high_priority_issue.HighPriorityIssueCondition" not in [
+                filter["id"] for filter in response.data["conditions"]
+            ]
+
+        # Show the high priority issue condition when high-priority-alerts is on
+        with self.feature({"projects:high-priority-alerts": True}):
+            response = self.get_success_response(self.organization.slug, self.project.slug)
+            assert "sentry.rules.conditions.high_priority_issue.HighPriorityIssueCondition" in [
+                filter["id"] for filter in response.data["conditions"]
+            ]

+ 28 - 0
tests/sentry/rules/conditions/test_high_severity_issue.py

@@ -0,0 +1,28 @@
+from sentry.rules.conditions.high_priority_issue import HighPriorityIssueCondition
+from sentry.testutils.cases import RuleTestCase
+from sentry.testutils.helpers.features import with_feature
+from sentry.testutils.silo import region_silo_test
+from sentry.testutils.skips import requires_snuba
+
+pytestmark = [requires_snuba]
+
+
+@region_silo_test
+class HighPriorityIssueConditionTest(RuleTestCase):
+    rule_cls = HighPriorityIssueCondition
+
+    @with_feature("projects:high-priority-alerts")
+    def test_applies_correctly(self):
+        rule = self.get_rule()
+        event = self.get_event()
+
+        self.assertPasses(rule, event, has_reappeared=True, has_escalated=False)
+        self.assertPasses(rule, event, has_reappeared=False, has_escalated=True)
+        self.assertDoesNotPass(rule, event, has_reappeared=False, has_escalated=False)
+
+        event.group.data["metadata"] = {"severity": "0.7"}
+        self.assertPasses(rule, event, has_reappeared=False, has_escalated=False)
+
+        event.group.data["metadata"] = {"severity": "0.0"}
+        self.assertPasses(rule, event, has_reappeared=False, has_escalated=True)
+        self.assertDoesNotPass(rule, event, has_reappeared=False, has_escalated=False)