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

feat(discover): Add SnQL support for error.unhandled (#27330)

Add support for error.handled and error.unhandled
in SnQL. Fields and Filter.
Shruthi 3 лет назад
Родитель
Сommit
d8ea561031

+ 13 - 1
src/sentry/search/events/base.py

@@ -7,6 +7,7 @@ from snuba_sdk.orderby import OrderBy
 
 
 from sentry.models import Project
 from sentry.models import Project
 from sentry.search.events.constants import (
 from sentry.search.events.constants import (
+    ERROR_HANDLED_ALIAS,
     ERROR_UNHANDLED_ALIAS,
     ERROR_UNHANDLED_ALIAS,
     ISSUE_ALIAS,
     ISSUE_ALIAS,
     ISSUE_ID_ALIAS,
     ISSUE_ID_ALIAS,
@@ -53,8 +54,9 @@ class QueryBase:
             TIMESTAMP_TO_DAY_ALIAS: self._resolve_timestamp_to_day_alias,
             TIMESTAMP_TO_DAY_ALIAS: self._resolve_timestamp_to_day_alias,
             USER_DISPLAY_ALIAS: self._resolve_user_display_alias,
             USER_DISPLAY_ALIAS: self._resolve_user_display_alias,
             TRANSACTION_STATUS_ALIAS: self._resolve_transaction_status,
             TRANSACTION_STATUS_ALIAS: self._resolve_transaction_status,
+            ERROR_UNHANDLED_ALIAS: self._resolve_error_unhandled_alias,
+            ERROR_HANDLED_ALIAS: self._resolve_error_handled_alias,
             # TODO: implement these
             # TODO: implement these
-            ERROR_UNHANDLED_ALIAS: self._resolve_unimplemented_alias,
             KEY_TRANSACTION_ALIAS: self._resolve_unimplemented_alias,
             KEY_TRANSACTION_ALIAS: self._resolve_unimplemented_alias,
             TEAM_KEY_TRANSACTION_ALIAS: self._resolve_unimplemented_alias,
             TEAM_KEY_TRANSACTION_ALIAS: self._resolve_unimplemented_alias,
             PROJECT_THRESHOLD_CONFIG_ALIAS: self._resolve_unimplemented_alias,
             PROJECT_THRESHOLD_CONFIG_ALIAS: self._resolve_unimplemented_alias,
@@ -131,6 +133,16 @@ class QueryBase:
             "toUInt8", [self.column(TRANSACTION_STATUS_ALIAS)], TRANSACTION_STATUS_ALIAS
             "toUInt8", [self.column(TRANSACTION_STATUS_ALIAS)], TRANSACTION_STATUS_ALIAS
         )
         )
 
 
+    def _resolve_error_unhandled_alias(self, _: str) -> SelectType:
+        return Function("notHandled", [], ERROR_UNHANDLED_ALIAS)
+
+    def _resolve_error_handled_alias(self, _: str) -> SelectType:
+        # Columns in snuba doesn't support aliasing right now like Function does.
+        # Adding a no-op here to get the alias.
+        return Function(
+            "cast", [self.column("error.handled"), "Array(Nullable(UInt8))"], ERROR_HANDLED_ALIAS
+        )
+
     def _resolve_unimplemented_alias(self, alias: str) -> SelectType:
     def _resolve_unimplemented_alias(self, alias: str) -> SelectType:
         """Used in the interim as a stub for ones that have not be implemented in SnQL yet.
         """Used in the interim as a stub for ones that have not be implemented in SnQL yet.
         Can be deleted once all field aliases have been implemented.
         Can be deleted once all field aliases have been implemented.

+ 2 - 0
src/sentry/search/events/constants.py

@@ -9,6 +9,7 @@ PROJECT_THRESHOLD_OVERRIDE_CONFIG_INDEX_ALIAS = "project_threshold_override_conf
 PROJECT_THRESHOLD_CONFIG_ALIAS = "project_threshold_config"
 PROJECT_THRESHOLD_CONFIG_ALIAS = "project_threshold_config"
 TEAM_KEY_TRANSACTION_ALIAS = "team_key_transaction"
 TEAM_KEY_TRANSACTION_ALIAS = "team_key_transaction"
 ERROR_UNHANDLED_ALIAS = "error.unhandled"
 ERROR_UNHANDLED_ALIAS = "error.unhandled"
+ERROR_HANDLED_ALIAS = "error.handled"
 USER_DISPLAY_ALIAS = "user.display"
 USER_DISPLAY_ALIAS = "user.display"
 PROJECT_ALIAS = "project"
 PROJECT_ALIAS = "project"
 PROJECT_NAME_ALIAS = "project.name"
 PROJECT_NAME_ALIAS = "project.name"
@@ -97,6 +98,7 @@ SNQL_FIELD_ALLOWLIST = {
     TIMESTAMP_TO_HOUR_ALIAS,
     TIMESTAMP_TO_HOUR_ALIAS,
     TIMESTAMP_TO_DAY_ALIAS,
     TIMESTAMP_TO_DAY_ALIAS,
     TRANSACTION_STATUS_ALIAS,
     TRANSACTION_STATUS_ALIAS,
+    ERROR_UNHANDLED_ALIAS,
 }
 }
 
 
 OPERATOR_NEGATION_MAP = {
 OPERATOR_NEGATION_MAP = {

+ 37 - 0
src/sentry/search/events/filter.py

@@ -25,6 +25,7 @@ from sentry.search.events.base import QueryBase
 from sentry.search.events.constants import (
 from sentry.search.events.constants import (
     ARRAY_FIELDS,
     ARRAY_FIELDS,
     EQUALITY_OPERATORS,
     EQUALITY_OPERATORS,
+    ERROR_HANDLED_ALIAS,
     ERROR_UNHANDLED_ALIAS,
     ERROR_UNHANDLED_ALIAS,
     ISSUE_ALIAS,
     ISSUE_ALIAS,
     ISSUE_ID_ALIAS,
     ISSUE_ID_ALIAS,
@@ -1021,6 +1022,8 @@ class QueryFilter(QueryBase):
             ISSUE_ALIAS: self._issue_filter_converter,
             ISSUE_ALIAS: self._issue_filter_converter,
             TRANSACTION_STATUS_ALIAS: self._transaction_status_filter_converter,
             TRANSACTION_STATUS_ALIAS: self._transaction_status_filter_converter,
             ISSUE_ID_ALIAS: self._issue_id_filter_converter,
             ISSUE_ID_ALIAS: self._issue_id_filter_converter,
+            ERROR_HANDLED_ALIAS: self._error_handled_filter_converter,
+            ERROR_UNHANDLED_ALIAS: self._error_unhandled_filter_converter,
         }
         }
 
 
     def resolve_where(self, query: Optional[str]) -> List[WhereType]:
     def resolve_where(self, query: Optional[str]) -> List[WhereType]:
@@ -1270,3 +1273,37 @@ class QueryFilter(QueryBase):
         # Skip isNull check on group_id value as we want to
         # Skip isNull check on group_id value as we want to
         # allow snuba's prewhere optimizer to find this condition.
         # allow snuba's prewhere optimizer to find this condition.
         return Condition(lhs, Op(search_filter.operator), rhs)
         return Condition(lhs, Op(search_filter.operator), rhs)
+
+    def _error_unhandled_filter_converter(
+        self,
+        search_filter: SearchFilter,
+    ) -> Optional[WhereType]:
+        value = search_filter.value.value
+        # Treat has filter as equivalent to handled
+        if search_filter.value.raw_value == "":
+            output = 0 if search_filter.operator == "!=" else 1
+            return Condition(Function("isHandled", []), Op.EQ, output)
+        if value in ("1", 1):
+            return Condition(Function("notHandled", []), Op.EQ, 1)
+        if value in ("0", 0):
+            return Condition(Function("isHandled", []), Op.EQ, 1)
+        raise InvalidSearchQuery(
+            "Invalid value for error.unhandled condition. Accepted values are 1, 0"
+        )
+
+    def _error_handled_filter_converter(
+        self,
+        search_filter: SearchFilter,
+    ) -> Optional[WhereType]:
+        value = search_filter.value.value
+        # Treat has filter as equivalent to handled
+        if search_filter.value.raw_value == "":
+            output = 1 if search_filter.operator == "!=" else 0
+            return Condition(Function("isHandled", []), Op.EQ, output)
+        if value in ("1", 1):
+            return Condition(Function("isHandled", []), Op.EQ, 1)
+        if value in ("0", 0):
+            return Condition(Function("notHandled", []), Op.EQ, 1)
+        raise InvalidSearchQuery(
+            "Invalid value for error.handled condition. Accepted values are 1, 0"
+        )

+ 86 - 0
tests/sentry/snuba/test_discover.py

@@ -497,6 +497,92 @@ class QueryIntegrationTest(SnubaTestCase, TestCase):
         )
         )
         run_query("!has:transaction.status", [], "status nonexistant")
         run_query("!has:transaction.status", [], "status nonexistant")
 
 
+    def test_error_handled_alias(self):
+        data = load_data("android-ndk", timestamp=before_now(minutes=10))
+        events = (
+            ("a" * 32, "not handled", False),
+            ("b" * 32, "is handled", True),
+            ("c" * 32, "undefined", None),
+        )
+        for event in events:
+            data["event_id"] = event[0]
+            data["message"] = event[1]
+            data["exception"]["values"][0]["value"] = event[1]
+            data["exception"]["values"][0]["mechanism"]["handled"] = event[2]
+            self.store_event(data=data, project_id=self.project.id)
+
+        queries = [
+            ("", [[0], [1], [None]]),
+            ("error.handled:true", [[1], [None]]),
+            ("!error.handled:true", [[0]]),
+            ("has:error.handled", [[1], [None]]),
+            ("has:error.handled error.handled:true", [[1], [None]]),
+            ("error.handled:false", [[0]]),
+            ("has:error.handled error.handled:false", []),
+        ]
+
+        for query, expected_data in queries:
+            for query_fn in [discover.query, discover.wip_snql_query]:
+                result = query_fn(
+                    selected_columns=["error.handled"],
+                    query=query,
+                    params={
+                        "organization_id": self.organization.id,
+                        "project_id": [self.project.id],
+                        "start": before_now(minutes=12),
+                        "end": before_now(minutes=8),
+                    },
+                )
+
+                data = result["data"]
+                data = sorted(
+                    data, key=lambda k: (k["error.handled"][0] is None, k["error.handled"][0])
+                )
+
+                assert len(data) == len(expected_data), query_fn
+                assert [item["error.handled"] for item in data] == expected_data
+
+    def test_error_unhandled_alias(self):
+        data = load_data("android-ndk", timestamp=before_now(minutes=10))
+        events = (
+            ("a" * 32, "not handled", False),
+            ("b" * 32, "is handled", True),
+            ("c" * 32, "undefined", None),
+        )
+        for event in events:
+            data["event_id"] = event[0]
+            data["message"] = event[1]
+            data["exception"]["values"][0]["value"] = event[1]
+            data["exception"]["values"][0]["mechanism"]["handled"] = event[2]
+            self.store_event(data=data, project_id=self.project.id)
+
+        queries = [
+            ("error.unhandled:true", ["a" * 32], [1]),
+            ("!error.unhandled:true", ["b" * 32, "c" * 32], [0, 0]),
+            ("has:error.unhandled", ["a" * 32], [1]),
+            ("!has:error.unhandled", ["b" * 32, "c" * 32], [0, 0]),
+            ("has:error.unhandled error.unhandled:true", ["a" * 32], [1]),
+            ("error.unhandled:false", ["b" * 32, "c" * 32], [0, 0]),
+            ("has:error.unhandled error.unhandled:false", [], []),
+        ]
+
+        for query, expected_events, error_handled in queries:
+            for query_fn in [discover.query, discover.wip_snql_query]:
+                result = query_fn(
+                    selected_columns=["error.unhandled"],
+                    query=query,
+                    params={
+                        "organization_id": self.organization.id,
+                        "project_id": [self.project.id],
+                        "start": before_now(minutes=12),
+                        "end": before_now(minutes=8),
+                    },
+                )
+                data = result["data"]
+
+                assert len(data) == len(expected_events), query_fn
+                assert [item["error.unhandled"] for item in data] == error_handled
+
     def test_field_aliasing_in_selected_columns(self):
     def test_field_aliasing_in_selected_columns(self):
         result = discover.query(
         result = discover.query(
             selected_columns=["project.id", "user", "release", "timestamp.to_hour"],
             selected_columns=["project.id", "user", "release", "timestamp.to_hour"],

+ 1 - 0
tests/snuba/api/endpoints/test_organization_events_stats.py

@@ -1494,6 +1494,7 @@ class OrganizationEventsStatsTopNEvents(APITestCase, SnubaTestCase):
 
 
         assert response.status_code == 200, response.content
         assert response.status_code == 200, response.content
         data = response.data
         data = response.data
+
         assert len(data) == 3
         assert len(data) == 3
 
 
         results = data[""]
         results = data[""]