Skip to content

Commit 175ac5c

Browse files
author
Riccardo Busetti
authored
fix(on-demand): Implement system for transforming the parsed query (#59777)
1 parent e63933d commit 175ac5c

File tree

2 files changed

+67
-12
lines changed

2 files changed

+67
-12
lines changed

src/sentry/snuba/metrics/extraction.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,53 @@ class MetricSpec(TypedDict):
313313
tags: NotRequired[Sequence[TagSpec]]
314314

315315

316+
def _transform_search_filter(search_filter: SearchFilter) -> SearchFilter:
317+
# If we have `message:something` we convert it to `message:*something*` since we want to perform `contains` matching
318+
# exactly how discover does it.
319+
if search_filter.key.name == "message":
320+
return SearchFilter(
321+
key=SearchKey(name=search_filter.key.name),
322+
operator=search_filter.operator,
323+
value=SearchValue(raw_value=f"*{search_filter.value.raw_value}*"),
324+
)
325+
326+
# If we have `transaction.status:unknown_error` we convert it to `transaction.status:unknown` since we need to be
327+
# backward compatible.
328+
if (
329+
search_filter.key.name == "transaction.status"
330+
and search_filter.value.raw_value == "unknown_error"
331+
):
332+
return SearchFilter(
333+
key=SearchKey(name=search_filter.key.name),
334+
operator=search_filter.operator,
335+
value=SearchValue(raw_value="unknown"),
336+
)
337+
338+
return search_filter
339+
340+
341+
def _transform_search_query(query: Sequence[QueryToken]) -> Sequence[QueryToken]:
342+
transformed_query: List[QueryToken] = []
343+
344+
for token in query:
345+
if isinstance(token, SearchFilter):
346+
transformed_query.append(_transform_search_filter(token))
347+
elif isinstance(token, ParenExpression):
348+
transformed_query.append(ParenExpression(_transform_search_query(token.children)))
349+
else:
350+
transformed_query.append(token)
351+
352+
return transformed_query
353+
354+
355+
def _parse_search_query(query: Optional[str]) -> Sequence[QueryToken]:
356+
"""
357+
Parses a search query with the discover grammar and performs some transformations on the AST in order to account for
358+
edge cases.
359+
"""
360+
return _transform_search_query(event_search.parse_search_query(query))
361+
362+
316363
@dataclass(frozen=True)
317364
class SupportedBy:
318365
"""Result of a check for standard and on-demand metric support."""
@@ -457,7 +504,7 @@ def _get_groupbys_support(groupbys: Sequence[str]) -> SupportedBy:
457504

458505
def _get_query_supported_by(query: Optional[str]) -> SupportedBy:
459506
try:
460-
parsed_query = event_search.parse_search_query(query)
507+
parsed_query = _parse_search_query(query)
461508

462509
standard_metrics = _is_standard_metrics_query(parsed_query)
463510
on_demand_metrics = _is_on_demand_supported_query(parsed_query)
@@ -575,7 +622,7 @@ def to_standard_metrics_query(query: str) -> str:
575622
"transaction.duration:>=1s AND browser.version:1" -> ""
576623
"""
577624
try:
578-
tokens = event_search.parse_search_query(query)
625+
tokens = _parse_search_query(query)
579626
except InvalidSearchQuery:
580627
logger.error(f"Failed to parse search query: {query}", exc_info=True)
581628
raise
@@ -1153,7 +1200,7 @@ def _parse_field(value: str) -> FieldParsingResult:
11531200
def _parse_query(value: str) -> QueryParsingResult:
11541201
"""Parse query string into our internal AST format."""
11551202
try:
1156-
conditions = event_search.parse_search_query(value)
1203+
conditions = _parse_search_query(value)
11571204
clean_conditions = cleanup_query(_remove_event_type_and_project_filter(conditions))
11581205
return QueryParsingResult(conditions=clean_conditions)
11591206
except InvalidSearchQuery as e:
@@ -1327,15 +1374,6 @@ def _filter(self, token: SearchFilter) -> RuleCondition:
13271374
if not operator:
13281375
raise ValueError(f"Unsupported operator {token.operator}")
13291376

1330-
# If we have a `message` field, we want to convert it to a glob matching, since in the UI `message` will perform
1331-
# a contains style match.
1332-
if token.key.name == "message":
1333-
token = SearchFilter(
1334-
key=SearchKey(name=token.key.name),
1335-
operator=token.operator,
1336-
value=SearchValue(raw_value=f"*{token.value.raw_value}*"),
1337-
)
1338-
13391377
# We propagate the filter in order to give as output a better error message with more context.
13401378
key: str = token.key.name
13411379
value: Any = token.value.raw_value

tests/sentry/snuba/test_extraction.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,23 @@ def test_spec_with_message():
447447
}
448448

449449

450+
def test_spec_with_unknown_error_status():
451+
spec = OnDemandMetricSpec(
452+
"avg(measurements.lcp)", "transaction.status:unknown_error OR transaction.status:unknown"
453+
)
454+
455+
assert spec._metric_type == "d"
456+
assert spec.field_to_extract == "event.measurements.lcp.value"
457+
assert spec.op == "avg"
458+
assert spec.condition == {
459+
"inner": [
460+
{"name": "event.contexts.trace.status", "op": "eq", "value": "unknown"},
461+
{"name": "event.contexts.trace.status", "op": "eq", "value": "unknown"},
462+
],
463+
"op": "or",
464+
}
465+
466+
450467
def test_spec_ignore_fields():
451468
with_ignored_field = OnDemandMetricSpec("count()", "transaction.duration:>=1 project:sentry")
452469
without_ignored_field = OnDemandMetricSpec("count()", "transaction.duration:>=1")

0 commit comments

Comments
 (0)