Skip to content

Conversation

@Zylphrex
Copy link
Member

This parses the metric from aggregations instead of relying on the top level metric name/type. The top level ones still take precedence for better compatibility. It has the limitation that it only allows a single metric across all the aggregations.

This parses the metric from aggregations instead of relying on the top level
metric name/type. The top level ones still take precendence for better
compatibility. It has the limitation that it only allows a single metric across
all the aggregations.
@Zylphrex Zylphrex requested a review from a team as a code owner November 10, 2025 22:06
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Nov 10, 2025
)
selected_metrics.add(metric)

if not selected_metrics:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Equations Ignore Metric Aggregations

The _extra_conditions_from_columns method accepts an equations parameter but never processes it. When metric aggregates like sum(value,foo,counter,-) are used inside equations, the metric information won't be extracted, causing the query to miss the required metric filter conditions. This breaks the feature's core functionality of parsing metrics from aggregations when they appear in equations.

Fix in Cursor Fix in Web

@codecov
Copy link

codecov bot commented Nov 10, 2025

❌ 6 Tests Failed:

Tests completed Failed Passed Skipped
4280 6 4274 46
View the top 3 failed test(s) by shortest run time
tests.sentry.search.eap.test_ourlogs.SearchResolverQueryTest::test_in_numeric_filter
Stack Traces | 0.037s run time
#x1B[1m#x1B[.../search/eap/test_ourlogs.py#x1B[0m:89: in test_in_numeric_filter
    assert where == TraceItemFilter(
#x1B[1m#x1B[31mE   assert and_filter {\n  filters {\n    comparison_filter {\n      key {\n        type: TYPE_INT\n        name: "sentry.severity_num...       val_int_array {\n          values: 123\n          values: 456\n          values: 789\n        }\n      }\n    }\n  }\n}\n == comparison_filter {\n  key {\n    type: TYPE_INT\n    name: "sentry.severity_number"\n  }\n  op: OP_IN\n  value {\n    val_int_array {\n      values: 123\n      values: 456\n      values: 789\n    }\n  }\n}\n#x1B[0m
#x1B[1m#x1B[31mE    +  where comparison_filter {\n  key {\n    type: TYPE_INT\n    name: "sentry.severity_number"\n  }\n  op: OP_IN\n  value {\n    val_int_array {\n      values: 123\n      values: 456\n      values: 789\n    }\n  }\n}\n = TraceItemFilter(comparison_filter=key {\n  type: TYPE_INT\n  name: "sentry.severity_number"\n}\nop: OP_IN\nvalue {\n  val_int_array {\n    values: 123\n    values: 456\n    values: 789\n  }\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +    where key {\n  type: TYPE_INT\n  name: "sentry.severity_number"\n}\nop: OP_IN\nvalue {\n  val_int_array {\n    values: 123\n    values: 456\n    values: 789\n  }\n}\n = ComparisonFilter(key=type: TYPE_INT\nname: "sentry.severity_number"\n, op=9, value=val_int_array {\n  values: 123\n  values: 456\n  values: 789\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +      where type: TYPE_INT\nname: "sentry.severity_number"\n = AttributeKey(name='sentry.severity_number', type=4)#x1B[0m
#x1B[1m#x1B[31mE    +        where 4 = <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f0f580ee150>.TYPE_INT#x1B[0m
#x1B[1m#x1B[31mE    +          where <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f0f580ee150> = AttributeKey.Type#x1B[0m
#x1B[1m#x1B[31mE    +      and   9 = ComparisonFilter.OP_IN#x1B[0m
#x1B[1m#x1B[31mE    +      and   val_int_array {\n  values: 123\n  values: 456\n  values: 789\n}\n = AttributeValue(val_int_array=values: 123\nvalues: 456\nvalues: 789\n)#x1B[0m
#x1B[1m#x1B[31mE    +        where values: 123\nvalues: 456\nvalues: 789\n = IntArray(values=[123, 456, 789])#x1B[0m
tests.sentry.search.eap.test_uptime_results.SearchResolverQueryTest::test_negation
Stack Traces | 0.037s run time
#x1B[1m#x1B[.../search/eap/test_uptime_results.py#x1B[0m:43: in test_negation
    assert where == TraceItemFilter(
#x1B[1m#x1B[31mE   assert and_filter {\n  filters {\n    comparison_filter {\n      key {\n        type: TYPE_STRING\n        name: "check_status"\n      }\n      op: OP_NOT_EQUALS\n      value {\n        val_str: "success"\n      }\n    }\n  }\n}\n == comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "check_status"\n  }\n  op: OP_NOT_EQUALS\n  value {\n    val_str: "success"\n  }\n}\n#x1B[0m
#x1B[1m#x1B[31mE    +  where comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "check_status"\n  }\n  op: OP_NOT_EQUALS\n  value {\n    val_str: "success"\n  }\n}\n = TraceItemFilter(comparison_filter=key {\n  type: TYPE_STRING\n  name: "check_status"\n}\nop: OP_NOT_EQUALS\nvalue {\n  val_str: "success"\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +    where key {\n  type: TYPE_STRING\n  name: "check_status"\n}\nop: OP_NOT_EQUALS\nvalue {\n  val_str: "success"\n}\n = ComparisonFilter(key=type: TYPE_STRING\nname: "check_status"\n, op=6, value=val_str: "success"\n)#x1B[0m
#x1B[1m#x1B[31mE    +      where type: TYPE_STRING\nname: "check_status"\n = AttributeKey(name='check_status', type=1)#x1B[0m
#x1B[1m#x1B[31mE    +        where 1 = <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7fddc71ea150>.TYPE_STRING#x1B[0m
#x1B[1m#x1B[31mE    +          where <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7fddc71ea150> = AttributeKey.Type#x1B[0m
#x1B[1m#x1B[31mE    +      and   6 = ComparisonFilter.OP_NOT_EQUALS#x1B[0m
#x1B[1m#x1B[31mE    +      and   val_str: "success"\n = AttributeValue(val_str='success')#x1B[0m
tests.sentry.search.eap.test_ourlogs::test_attribute_search[test_case1]
Stack Traces | 0.041s run time
#x1B[1m#x1B[.../search/eap/test_ourlogs.py#x1B[0m:319: in test_attribute_search
    assert where == TraceItemFilter(
#x1B[1m#x1B[31mE   assert and_filter {\n  filters {\n    comparison_filter {\n      key {\n        type: TYPE_STRING\n        name: "sentry.observed_timestamp_nanos"\n      }\n      op: OP_EQUALS\n      value {\n        val_str: "1111111111.0"\n      }\n    }\n  }\n}\n == comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.observed_timestamp_nanos"\n  }\n  op: OP_EQUALS\n  value {\n    val_str: "1111111111.0"\n  }\n}\n#x1B[0m
#x1B[1m#x1B[31mE    +  where comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.observed_timestamp_nanos"\n  }\n  op: OP_EQUALS\n  value {\n    val_str: "1111111111.0"\n  }\n}\n = TraceItemFilter(comparison_filter=key {\n  type: TYPE_STRING\n  name: "sentry.observed_timestamp_nanos"\n}\nop: OP_EQUALS\nvalue {\n  val_str: "1111111111.0"\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +    where key {\n  type: TYPE_STRING\n  name: "sentry.observed_timestamp_nanos"\n}\nop: OP_EQUALS\nvalue {\n  val_str: "1111111111.0"\n}\n = ComparisonFilter(key=type: TYPE_STRING\nname: "sentry.observed_timestamp_nanos"\n, op=5, value=val_str: "1111111111.0"\n)#x1B[0m
#x1B[1m#x1B[31mE    +      where type: TYPE_STRING\nname: "sentry.observed_timestamp_nanos"\n = AttributeKey(name='sentry.observed_timestamp_nanos', type=1)#x1B[0m
#x1B[1m#x1B[31mE    +        where 'sentry.observed_timestamp_nanos' = ResolvedAttribute(public_alias='observed_timestamp', search_type='number', internal_type=1, processor=None, validator=...l_name='sentry.observed_timestamp_nanos', is_aggregate=False, private=False, replacement=None, deprecation_status=None).internal_name#x1B[0m
#x1B[1m#x1B[31mE    +      and   5 = ComparisonFilter.OP_EQUALS#x1B[0m
tests.sentry.search.eap.test_spans.SearchResolverQueryTest::test_negation
Stack Traces | 0.073s run time
#x1B[1m#x1B[.../search/eap/test_spans.py#x1B[0m:58: in test_negation
    assert where == TraceItemFilter(
#x1B[1m#x1B[31mE   assert and_filter {\n  filters {\n    comparison_filter {\n      key {\n        type: TYPE_STRING\n        name: "sentry.raw_description"\n      }\n      op: OP_NOT_EQUALS\n      value {\n        val_str: "foo"\n      }\n    }\n  }\n}\n == comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.raw_description"\n  }\n  op: OP_NOT_EQUALS\n  value {\n    val_str: "foo"\n  }\n}\n#x1B[0m
#x1B[1m#x1B[31mE    +  where comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.raw_description"\n  }\n  op: OP_NOT_EQUALS\n  value {\n    val_str: "foo"\n  }\n}\n = TraceItemFilter(comparison_filter=key {\n  type: TYPE_STRING\n  name: "sentry.raw_description"\n}\nop: OP_NOT_EQUALS\nvalue {\n  val_str: "foo"\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +    where key {\n  type: TYPE_STRING\n  name: "sentry.raw_description"\n}\nop: OP_NOT_EQUALS\nvalue {\n  val_str: "foo"\n}\n = ComparisonFilter(key=type: TYPE_STRING\nname: "sentry.raw_description"\n, op=6, value=val_str: "foo"\n)#x1B[0m
#x1B[1m#x1B[31mE    +      where type: TYPE_STRING\nname: "sentry.raw_description"\n = AttributeKey(name='sentry.raw_description', type=1)#x1B[0m
#x1B[1m#x1B[31mE    +        where 1 = <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f0f580ee150>.TYPE_STRING#x1B[0m
#x1B[1m#x1B[31mE    +          where <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7f0f580ee150> = AttributeKey.Type#x1B[0m
#x1B[1m#x1B[31mE    +      and   6 = ComparisonFilter.OP_NOT_EQUALS#x1B[0m
#x1B[1m#x1B[31mE    +      and   val_str: "foo"\n = AttributeValue(val_str='foo')#x1B[0m
tests.sentry.search.eap.test_spans.SearchResolverQueryTest::test_not_in_filter
Stack Traces | 0.083s run time
#x1B[1m#x1B[.../search/eap/test_spans.py#x1B[0m:106: in test_not_in_filter
    assert where == TraceItemFilter(
#x1B[1m#x1B[31mE   assert and_filter {\n  filters {\n    comparison_filter {\n      key {\n        type: TYPE_STRING\n        name: "sentry.raw_descr... val_str_array {\n          values: "foo"\n          values: "bar"\n          values: "baz"\n        }\n      }\n    }\n  }\n}\n == comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.raw_description"\n  }\n  op: OP_NOT_IN\n  value {\n    val_str_array {\n      values: "foo"\n      values: "bar"\n      values: "baz"\n    }\n  }\n}\n#x1B[0m
#x1B[1m#x1B[31mE    +  where comparison_filter {\n  key {\n    type: TYPE_STRING\n    name: "sentry.raw_description"\n  }\n  op: OP_NOT_IN\n  value {\n    val_str_array {\n      values: "foo"\n      values: "bar"\n      values: "baz"\n    }\n  }\n}\n = TraceItemFilter(comparison_filter=key {\n  type: TYPE_STRING\n  name: "sentry.raw_description"\n}\nop: OP_NOT_IN\nvalue {\n  val_str_array {\n    values: "foo"\n    values: "bar"\n    values: "baz"\n  }\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +    where key {\n  type: TYPE_STRING\n  name: "sentry.raw_description"\n}\nop: OP_NOT_IN\nvalue {\n  val_str_array {\n    values: "foo"\n    values: "bar"\n    values: "baz"\n  }\n}\n = ComparisonFilter(key=type: TYPE_STRING\nname: "sentry.raw_description"\n, op=10, value=val_str_array {\n  values: "foo"\n  values: "bar"\n  values: "baz"\n}\n)#x1B[0m
#x1B[1m#x1B[31mE    +      where type: TYPE_STRING\nname: "sentry.raw_description"\n = AttributeKey(name='sentry.raw_description', type=1)#x1B[0m
#x1B[1m#x1B[31mE    +        where 1 = <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7fddc71ea150>.TYPE_STRING#x1B[0m
#x1B[1m#x1B[31mE    +          where <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7fddc71ea150> = AttributeKey.Type#x1B[0m
#x1B[1m#x1B[31mE    +      and   10 = ComparisonFilter.OP_NOT_IN#x1B[0m
#x1B[1m#x1B[31mE    +      and   val_str_array {\n  values: "foo"\n  values: "bar"\n  values: "baz"\n}\n = AttributeValue(val_str_array=values: "foo"\nvalues: "bar"\nvalues: "baz"\n)#x1B[0m
#x1B[1m#x1B[31mE    +        where values: "foo"\nvalues: "bar"\nvalues: "baz"\n = StrArray(values=['foo', 'bar', 'baz'])#x1B[0m
tests.sentry.snuba.test_entity_subscriptions.EntitySubscriptionTestCase::test_get_entity_subscription_for_eap_rpc_query
Stack Traces | 1.88s run time
#x1B[1m#x1B[.../sentry/snuba/test_entity_subscriptions.py#x1B[0m:457: in test_get_entity_subscription_for_eap_rpc_query
    assert rpc_timeseries_request.filter.comparison_filter.value.val_str == "http.client"
#x1B[1m#x1B[31mE   AssertionError: assert '' == 'http.client'#x1B[0m
#x1B[1m#x1B[31mE     #x1B[0m
#x1B[1m#x1B[31mE     - http.client#x1B[0m

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

equations: list[str] | None,
) -> TraceItemFilter | None:
# use the metric from the config first if it exists
if extra_conditions := self._extra_conditions_from_metric(search_resolver):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config is populated from the extra request params right? Is the long term plan to eventually stop supporting it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Trace metrics uses it own TraceMetricsSearchResolverConfig that inherits from SearchResolverConfig. This currently has top level configs to specify 1 metric. Long term, we'll remove that and only rely on the metric specified in the aggregation.

Comment on lines +77 to +78
if len(selected_metrics) > 1:
raise InvalidSearchQuery("Cannot aggregate multiple metrics in 1 query.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two questions

  1. if you can't select multiple metrics in one query why do we need it in the aggregates?
  2. shouldn't we dedupe the selected_metrics list if there's multiple aggregates on the same metric?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. We cant select multiple metrics today because EAP does not have support to allow us to express it yet. See EAP-313
  2. selected_metrics is a set of tuples so it already dedupes, meaning it lets you select multiple aggregates on the same metric

@Zylphrex Zylphrex merged commit f4fcb4a into master Nov 12, 2025
65 checks passed
@Zylphrex Zylphrex deleted the txiao/feat/parse-metric-from-aggregations branch November 12, 2025 17:01
@sentry
Copy link

sentry bot commented Nov 12, 2025

Issues attributed to commits in this pull request

This pull request was merged and Sentry observed the following issues:

andrewshie-sentry pushed a commit that referenced this pull request Nov 13, 2025
This parses the metric from aggregations instead of relying on the top
level metric name/type. The top level ones still take precedence for
better compatibility. It has the limitation that it only allows a single
metric across all the aggregations.
Zylphrex added a commit that referenced this pull request Nov 13, 2025
Same as #103102 but for formulas so per_second and per_minute will work as well.
Zylphrex added a commit that referenced this pull request Nov 13, 2025
Same as #103102 but for formulas so per_second and per_minute will work
as well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants