1111from sentry .search .eap .types import SearchResolverConfig
1212from sentry .search .events .types import SnubaParams
1313from sentry .snuba .occurrences_rpc import Occurrences
14- from sentry .utils import metrics
1514
1615logger = logging .getLogger (__name__ )
1716
1817
1918# Filters that must be skipped because they have no EAP equivalent.
2019# These would silently become dynamic tag lookups in the EAP SearchResolver
21- # (resolver.py:1026-1060) and produce incorrect results.
20+ # and produce incorrect results.
2221# TODO: these are potentially gaps between existing issue feed search behavior and EAP search behavior. May need to adddress.
2322SKIP_FILTERS : frozenset [str ] = frozenset (
2423 {
6059def search_filters_to_query_string (
6160 search_filters : Sequence [SearchFilter ],
6261) -> str :
63- """Convert Snuba-relevant SearchFilter objects to an EAP query string.
62+ """
63+ Convert Snuba-relevant SearchFilter objects to an EAP query string.
6464
6565 Expects filters that have already been stripped of postgres-only fields
6666 (status, assigned_to, bookmarked_by, etc.) by the caller.
@@ -85,10 +85,6 @@ def _convert_single_filter(sf: SearchFilter) -> str | None:
8585 return _convert_aggregation_filter (sf )
8686
8787 if key in SKIP_FILTERS :
88- metrics .incr (
89- "eap.search_executor.filter_skipped" ,
90- tags = {"key" : key },
91- )
9288 return None
9389
9490 # error.unhandled requires special inversion logic.
@@ -126,17 +122,24 @@ def _convert_single_filter(sf: SearchFilter) -> str | None:
126122 return None
127123
128124
129- def _convert_error_unhandled (sf : SearchFilter ) -> str | None :
130- """Convert error.unhandled filter to the EAP error.handled attribute.
125+ def _convert_aggregation_filter (sf : SearchFilter ) -> str | None :
126+ eap_function = AGGREGATION_FIELD_TO_EAP_FUNCTION [sf .key .name ]
127+ formatted_value = _format_value (sf .value .raw_value )
131128
132- error.unhandled:1 (or true) → !error.handled:1
133- error.unhandled:0 (or false) → error.handled:1
134- !error.unhandled:1 → error.handled:1
135- """
129+ if sf .operator in (">" , ">=" , "<" , "<=" ):
130+ return f"{ eap_function } :{ sf .operator } { formatted_value } "
131+ elif sf .operator == "=" :
132+ return f"{ eap_function } :{ formatted_value } "
133+ elif sf .operator == "!=" :
134+ return f"!{ eap_function } :{ formatted_value } "
135+
136+ return None
137+
138+
139+ def _convert_error_unhandled (sf : SearchFilter ) -> str | None :
136140 raw_value = sf .value .raw_value
137141 op = sf .operator
138142
139- # Determine if the user is looking for unhandled errors
140143 is_looking_for_unhandled = (op == "=" and raw_value in ("1" , 1 , True , "true" )) or (
141144 op == "!=" and raw_value in ("0" , 0 , False , "false" )
142145 )
@@ -147,24 +150,38 @@ def _convert_error_unhandled(sf: SearchFilter) -> str | None:
147150 return "error.handled:1"
148151
149152
150- def _convert_aggregation_filter (sf : SearchFilter ) -> str | None :
151- """Convert a legacy aggregation field filter to EAP function syntax.
153+ def _format_value (
154+ raw_value : str | int | float | datetime | Sequence [str ] | Sequence [float ],
155+ ) -> str :
156+ if isinstance (raw_value , (list , tuple )):
157+ parts = ", " .join (_format_single_value (v ) for v in raw_value )
158+ return f"[{ parts } ]"
159+ if isinstance (raw_value , datetime ):
160+ return raw_value .isoformat ()
161+ if isinstance (raw_value , (int , float )):
162+ return str (raw_value )
163+ return _format_string_value (str (raw_value ))
152164
153- e.g. times_seen:>100 → count():>100
154- last_seen:>2024-01-01 → last_seen():>2024-01-01T00:00:00+00:00
155- user_count:>5 → count_unique(user):>5
156- """
157- eap_function = AGGREGATION_FIELD_TO_EAP_FUNCTION [sf .key .name ]
158- formatted_value = _format_value (sf .value .raw_value )
159165
160- if sf . operator in ( ">" , ">=" , "<" , "<=" ) :
161- return f" { eap_function } : { sf . operator } { formatted_value } "
162- elif sf . operator == "=" :
163- return f" { eap_function } : { formatted_value } "
164- elif sf . operator == "!=" :
165- return f"! { eap_function } : { formatted_value } "
166+ def _format_single_value ( value : str | int | float | datetime ) -> str :
167+ if isinstance ( value , datetime ):
168+ return value . isoformat ()
169+ if isinstance ( value , ( int , float )):
170+ return str ( value )
171+ return _format_string_value ( str ( value ))
166172
167- return None
173+
174+ def _format_string_value (s : str ) -> str :
175+ # Wildcard values pass through as-is for the SearchResolver to handle
176+ if "*" in s :
177+ return s
178+
179+ # Quote strings containing spaces or special characters
180+ if " " in s or '"' in s or "," in s or "(" in s or ")" in s :
181+ escaped = s .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
182+ return f'"{ escaped } "'
183+
184+ return s
168185
169186
170187# Maps legacy sort_field names (from PostgresSnubaQueryExecutor.sort_strategies values)
@@ -177,7 +194,7 @@ def _convert_aggregation_filter(sf: SearchFilter) -> str | None:
177194# "user" → "user_count" → uniq(tags[sentry:user])
178195# "trends" → "trends" → complex ClickHouse expression (not supported)
179196# "recommended" → "recommended" → complex ClickHouse expression (not supported)
180- # "inbox" → "" → Postgres only (not supported)
197+ # "inbox" → "" → Postgres only (not supported)
181198EAP_SORT_STRATEGIES : dict [str , tuple [list [str ], list [str ]]] = {
182199 "last_seen" : (["group_id" , "last_seen()" ], ["-last_seen()" ]),
183200 "times_seen" : (["group_id" , "count()" ], ["-count()" ]),
@@ -199,14 +216,12 @@ def run_eap_group_search(
199216 search_filters : Sequence [SearchFilter ] | None = None ,
200217 referrer : str = "" ,
201218) -> tuple [list [tuple [int , Any ]], int ]:
202- """EAP equivalent of PostgresSnubaQueryExecutor.snuba_search().
219+ """
220+ EAP equivalent of PostgresSnubaQueryExecutor.snuba_search().
203221
204222 Returns a tuple of:
205223 * a list of (group_id, sort_score) tuples,
206224 * total count (0 during double-reading; legacy provides the real total).
207-
208- This matches the return signature of snuba_search() so it can be used
209- as the experimental branch in check_and_choose().
210225 """
211226 if sort_field not in EAP_SORT_STRATEGIES :
212227 return ([], 0 )
@@ -269,41 +284,7 @@ def run_eap_group_search(
269284 if group_id is not None :
270285 tuples .append ((int (group_id ), score ))
271286
272- # The EAP RPC TraceItemTableResponse does not include a total count
287+ # TODO: the EAP RPC TraceItemTableResponse does not include a total count
273288 # (unlike Snuba's totals=True). During double-reading the legacy result
274289 # provides the real total, so we return 0 here.
275290 return (tuples , 0 )
276-
277-
278- def _format_value (
279- raw_value : str | int | float | datetime | Sequence [str ] | Sequence [float ],
280- ) -> str :
281- if isinstance (raw_value , (list , tuple )):
282- parts = ", " .join (_format_single_value (v ) for v in raw_value )
283- return f"[{ parts } ]"
284- if isinstance (raw_value , datetime ):
285- return raw_value .isoformat ()
286- if isinstance (raw_value , (int , float )):
287- return str (raw_value )
288- return _format_string_value (str (raw_value ))
289-
290-
291- def _format_single_value (value : str | int | float | datetime ) -> str :
292- if isinstance (value , datetime ):
293- return value .isoformat ()
294- if isinstance (value , (int , float )):
295- return str (value )
296- return _format_string_value (str (value ))
297-
298-
299- def _format_string_value (s : str ) -> str :
300- # Wildcard values pass through as-is for the SearchResolver to handle
301- if "*" in s :
302- return s
303-
304- # Quote strings containing spaces or special characters
305- if " " in s or '"' in s or "," in s or "(" in s or ")" in s :
306- escaped = s .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
307- return f'"{ escaped } "'
308-
309- return s
0 commit comments