Просмотр исходного кода

feat(issue-alerts): preview for attributes filter (#41226)

Adds support for attributes filter to preview.
Andrew Xue 2 лет назад
Родитель
Сommit
335372b10b

+ 67 - 36
src/sentry/rules/conditions/event_attribute.py

@@ -1,38 +1,41 @@
-from typing import Any, Sequence
+from typing import Any, Dict, Sequence
 
 from django import forms
 
 from sentry.eventstore.models import GroupEvent
 from sentry.rules import MATCH_CHOICES, EventState, MatchType
 from sentry.rules.conditions.base import EventCondition
-
-ATTR_CHOICES = [
-    "message",
-    "platform",
-    "environment",
-    "type",
-    "error.handled",
-    "error.unhandled",
-    "exception.type",
-    "exception.value",
-    "user.id",
-    "user.email",
-    "user.username",
-    "user.ip_address",
-    "http.method",
-    "http.url",
-    "http.status_code",
-    "sdk.name",
-    "stacktrace.code",
-    "stacktrace.module",
-    "stacktrace.filename",
-    "stacktrace.abs_path",
-    "stacktrace.package",
-]
+from sentry.snuba.events import Columns
+from sentry.types.condition_activity import ConditionActivity
+
+# Maps attributes to snuba columns
+ATTR_CHOICES = {
+    "message": Columns.MESSAGE,
+    "platform": Columns.PLATFORM,
+    "environment": Columns.MESSAGE,
+    "type": Columns.TYPE,
+    "error.handled": Columns.ERROR_HANDLED,
+    "error.unhandled": Columns.ERROR_HANDLED,
+    "exception.type": Columns.ERROR_TYPE,
+    "exception.value": Columns.ERROR_VALUE,
+    "user.id": Columns.USER_ID,
+    "user.email": Columns.USER_EMAIL,
+    "user.username": Columns.USER_USERNAME,
+    "user.ip_address": Columns.USER_IP_ADDRESS,
+    "http.method": Columns.HTTP_METHOD,
+    "http.url": Columns.HTTP_URL,
+    "http.status_code": Columns.HTTP_STATUS_CODE,
+    "sdk.name": Columns.SDK_NAME,
+    "stacktrace.code": None,
+    "stacktrace.module": Columns.STACK_MODULE,
+    "stacktrace.filename": Columns.STACK_FILENAME,
+    "stacktrace.abs_path": Columns.STACK_ABS_PATH,
+    "stacktrace.package": Columns.STACK_PACKAGE,
+}
 
 
 class EventAttributeForm(forms.Form):  # type: ignore
-    attribute = forms.ChoiceField(choices=[(a, a) for a in ATTR_CHOICES])
+    attribute = forms.ChoiceField(choices=[(a, a) for a in ATTR_CHOICES.keys()])
     match = forms.ChoiceField(choices=list(MATCH_CHOICES.items()))
     value = forms.CharField(widget=forms.TextInput(), required=False)
 
@@ -60,7 +63,7 @@ class EventAttributeCondition(EventCondition):
         "attribute": {
             "type": "choice",
             "placeholder": "i.e. exception.type",
-            "choices": [[a, a] for a in ATTR_CHOICES],
+            "choices": [[a, a] for a in ATTR_CHOICES.keys()],
         },
         "match": {"type": "choice", "choices": list(MATCH_CHOICES.items())},
         "value": {"type": "string", "placeholder": "value"},
@@ -189,21 +192,14 @@ class EventAttributeCondition(EventCondition):
         }
         return self.label.format(**data)
 
-    def passes(self, event: GroupEvent, state: EventState, **kwargs: Any) -> bool:
-        attr = self.get_option("attribute")
+    def _passes(self, attribute_values: Sequence[Any]) -> bool:
         match = self.get_option("match")
         value = self.get_option("value")
 
-        if not (attr and match and value):
+        if not ((match and value) or (match in (MatchType.IS_SET, MatchType.NOT_SET))):
             return False
 
         value = value.lower()
-        attr = attr.lower()
-
-        try:
-            attribute_values = self._get_attribute_values(event, attr)
-        except KeyError:
-            attribute_values = []
 
         attribute_values = [str(v).lower() for v in attribute_values if v is not None]
 
@@ -262,3 +258,38 @@ class EventAttributeCondition(EventCondition):
             return not attribute_values
 
         raise RuntimeError("Invalid Match")
+
+    def passes(self, event: GroupEvent, state: EventState, **kwargs: Any) -> bool:
+        attr = self.get_option("attribute", "")
+        try:
+            attribute_values = self._get_attribute_values(event, attr.lower())
+        except KeyError:
+            attribute_values = []
+
+        return self._passes(attribute_values)
+
+    def passes_activity(
+        self, condition_activity: ConditionActivity, event_map: Dict[str, Any]
+    ) -> bool:
+        try:
+            attr = self.get_option("attribute").lower()
+            column = ATTR_CHOICES[attr].value.event_name
+            attribute_values = event_map[condition_activity.data["event_id"]][column]
+
+            if isinstance(attribute_values, str):
+                attribute_values = [attribute_values]
+
+            # flip values, since the queried column is "error.handled"
+            if attr == "error.unhandled":
+                attribute_values = [not value for value in attribute_values]
+
+            return self._passes(attribute_values)
+        except (TypeError, KeyError):
+            return False
+
+    def get_event_columns(self) -> Sequence[str]:
+        attr = self.get_option("attribute")
+        column = ATTR_CHOICES[attr]
+        if column is None:
+            raise NotImplementedError
+        return [column.value.event_name]

+ 4 - 1
src/sentry/rules/history/preview.py

@@ -105,7 +105,10 @@ def get_filters(
             raise PreviewException
         filter_object = filter_cls(project, data=filter)
         filter_objects.append(filter_object)
-        event_columns.update(filter_object.get_event_columns())
+        try:
+            event_columns.update(filter_object.get_event_columns())
+        except NotImplementedError:
+            raise PreviewException
 
     filter_func = get_match_function(filter_match)
     if filter_func is None:

+ 19 - 0
tests/sentry/rules/history/test_preview.py

@@ -212,6 +212,25 @@ class ProjectRulePreviewTest(TestCase):
         results = preview(self.project, conditions, filters, "all", "all", 0)
         assert event.group not in results
 
+    def test_event_attribute(self):
+        event = self._set_up_event({"message": "hello world"})
+        conditions = [{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}]
+        filters = [
+            {
+                "id": "sentry.rules.filters.event_attribute.EventAttributeFilter",
+                "attribute": "message",
+                "match": "eq",
+                "value": "hello world",
+            }
+        ]
+
+        results = preview(self.project, conditions, filters, "all", "all", 0)
+        assert event.group in results
+
+        filters[0]["value"] = "goodbye world"
+        results = preview(self.project, conditions, filters, "all", "all", 0)
+        assert event.group not in results
+
     def test_unsupported_conditions(self):
         self._set_up_first_seen()
         # conditions with no immediate plan to support