From 64f9f771d4e24a1e78e27f6e07eafba21cf691d0 Mon Sep 17 00:00:00 2001 From: Charlie Luo Date: Mon, 5 Jan 2026 14:26:56 -0800 Subject: [PATCH] fix(issue-search): correctly parse negated contains filters --- src/sentry/search/events/filter.py | 16 +++++++++++++--- .../endpoints/test_group_event_details.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/sentry/search/events/filter.py b/src/sentry/search/events/filter.py index bb5f7792e2bd..cdaf3a2bf50e 100644 --- a/src/sentry/search/events/filter.py +++ b/src/sentry/search/events/filter.py @@ -669,9 +669,19 @@ def convert_search_filter_to_snuba_query( # together. Otherwise just return the raw condition, so that it can be # used correctly in aggregates. if is_null_condition: - return [is_null_condition, condition] - else: - return condition + return [is_null_condition, *_flatten_conditions(condition)] + return condition + + +def _flatten_conditions(cond: list[Any]) -> list[Any]: + """ + Flatten nested legacy conditions into a flat list. A legacy condition is + [lhs, op_string, rhs]. Wildcard processing can create nested lists that + snuba_sdk.legacy.parse_condition cannot handle. + """ + if len(cond) == 3 and isinstance(cond[1], str): + return [cond] + return [c for item in cond if isinstance(item, list) for c in _flatten_conditions(item)] def format_search_filter(term, params): diff --git a/tests/sentry/issues/endpoints/test_group_event_details.py b/tests/sentry/issues/endpoints/test_group_event_details.py index fc2812f0b400..8683944166aa 100644 --- a/tests/sentry/issues/endpoints/test_group_event_details.py +++ b/tests/sentry/issues/endpoints/test_group_event_details.py @@ -403,6 +403,23 @@ def test_query_title(self) -> None: assert response.data["previousEventID"] is None assert response.data["nextEventID"] is None + def test_query_title_not_in_with_wildcards(self) -> None: + event_e = self.store_event( + data={ + "event_id": "e" * 32, + "environment": "staging", + "timestamp": before_now(minutes=1).isoformat(), + "fingerprint": ["group-title-wildcard"], + "message": "some other title", + }, + project_id=self.project_1.id, + ) + + url = f"/api/0/issues/{event_e.group.id}/events/recommended/" + response = self.client.get(url, {"query": '!title:["*value1*", "*value2*"]'}, format="json") + + assert response.status_code == 200, response.content + def test_query_issue_platform_title(self) -> None: issue_title = "king of england" occurrence, group_info = self.process_occurrence(