Browse Source

feat(discover): Add SnQL support to filter arrays (#27585)

A lot of the array fields are aliased, just
added filter support for now without
aliasing these fields.
Shruthi 3 years ago
parent
commit
c427daa9ad

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

@@ -105,6 +105,18 @@ SNQL_FIELD_ALLOWLIST = {
     TRANSACTION_STATUS_ALIAS,
     ERROR_UNHANDLED_ALIAS,
     TEAM_KEY_TRANSACTION_ALIAS,
+    "error.mechanism",
+    "error.type",
+    "error.value",
+    "stack.abs_path",
+    "stack.colno",
+    "stack.filename",
+    "stack.function",
+    "stack.in_app",
+    "stack.lineno",
+    "stack.module",
+    "stack.package",
+    "stack.stack_level",
 }
 
 OPERATOR_NEGATION_MAP = {

+ 28 - 4
src/sentry/search/events/filter.py

@@ -1174,15 +1174,39 @@ class QueryFilter(QueryFields):
 
         lhs = self.resolve_field_alias(name) if self.is_field_alias(name) else self.column(name)
 
+        if name in ARRAY_FIELDS:
+            if search_filter.value.is_wildcard():
+                condition = Condition(
+                    lhs,
+                    Op.LIKE if search_filter.operator == "=" else Op.NOT_LIKE,
+                    search_filter.value.raw_value.replace("%", "\\%")
+                    .replace("_", "\\_")
+                    .replace("*", "%"),
+                )
+            elif name in ARRAY_FIELDS and search_filter.is_in_filter:
+                condition = Condition(
+                    Function("hasAny", [self.column(name), value]),
+                    Op.EQ if search_filter.operator == "IN" else Op.NEQ,
+                    1,
+                )
+            elif name in ARRAY_FIELDS and search_filter.value.raw_value == "":
+                condition = Condition(
+                    Function("hasAny", [self.column(name), []]),
+                    Op.EQ if search_filter.operator == "=" else Op.NEQ,
+                    1,
+                )
+            else:
+                condition = Condition(lhs, Op(search_filter.operator), value)
+
         # Handle checks for existence
-        if search_filter.operator in ("=", "!=") and search_filter.value.value == "":
+        elif search_filter.operator in ("=", "!=") and search_filter.value.value == "":
             if search_filter.key.is_tag:
-                return Condition(lhs, Op(search_filter.operator), value)
+                condition = Condition(lhs, Op(search_filter.operator), value)
             else:
                 # If not a tag, we can just check that the column is null.
-                return Condition(Function("isNull", [lhs]), Op(search_filter.operator), 1)
+                condition = Condition(Function("isNull", [lhs]), Op(search_filter.operator), 1)
 
-        if search_filter.value.is_wildcard():
+        elif search_filter.value.is_wildcard():
             condition = Condition(
                 Function("match", [lhs, f"'(?i){value}'"]),
                 Op(search_filter.operator),

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

@@ -810,6 +810,65 @@ class QueryIntegrationTest(SnubaTestCase, TestCase):
                 assert len(data) == len(expected_events), query_fn
                 assert [item["error.unhandled"] for item in data] == error_handled
 
+    def test_array_fields(self):
+        data = load_data("javascript")
+        data["timestamp"] = iso_format(before_now(minutes=10))
+        self.store_event(data=data, project_id=self.project.id)
+
+        expected_filenames = [
+            "../../sentry/scripts/views.js",
+            "../../sentry/scripts/views.js",
+            "../../sentry/scripts/views.js",
+            "raven.js",
+        ]
+
+        queries = [
+            ("", 1),
+            ("stack.filename:*.js", 1),
+            ("stack.filename:*.py", 0),
+            ("has:stack.filename", 1),
+            ("!has:stack.filename", 0),
+        ]
+
+        for query, expected_len in queries:
+            for query_fn, expected_alias in [
+                (discover.query, "stack.filename"),
+                (discover.wip_snql_query, "exception_frames.filename"),
+            ]:
+                result = query_fn(
+                    selected_columns=["stack.filename"],
+                    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) == expected_len
+                if len(data) == 0:
+                    continue
+                assert len(data[0][expected_alias]) == len(expected_filenames)
+                assert sorted(data[0][expected_alias]) == expected_filenames
+
+        result = discover.wip_snql_query(
+            selected_columns=["stack.filename"],
+            query="stack.filename:[raven.js]",
+            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) == 1
+        assert len(data[0]["exception_frames.filename"]) == len(expected_filenames)
+        assert sorted(data[0]["exception_frames.filename"]) == expected_filenames
+
     def test_field_aliasing_in_selected_columns(self):
         result = discover.query(
             selected_columns=["project.id", "user", "release", "timestamp.to_hour"],