diff --git a/src/sentry/search/eap/columns.py b/src/sentry/search/eap/columns.py index afd6b1ebb405c8..a3a07fd5bca1ec 100644 --- a/src/sentry/search/eap/columns.py +++ b/src/sentry/search/eap/columns.py @@ -1,7 +1,7 @@ from collections.abc import Callable, Iterable, Mapping from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Literal, TypeAlias, TypedDict +from typing import Any, Literal, TypeAlias, TypedDict, cast from dateutil.tz import tz from sentry_protos.snuba.v1.attribute_conditional_aggregation_pb2 import ( @@ -22,7 +22,7 @@ from sentry.api.event_search import SearchFilter from sentry.exceptions import InvalidSearchQuery from sentry.search.eap import constants -from sentry.search.eap.types import EAPResponse, SearchResolverConfig +from sentry.search.eap.types import EAPResponse, MetricType, SearchResolverConfig from sentry.search.events.types import SnubaParams ResolvedArgument: TypeAlias = AttributeKey | str | int | float @@ -221,6 +221,13 @@ def proto_definition(self) -> AttributeAggregation: ) +@dataclass(frozen=True, kw_only=True) +class ResolvedMetricAggregate(ResolvedAggregate): + metric_name: str | None + metric_type: MetricType | None + metric_unit: str | None + + @dataclass(frozen=True, kw_only=True) class ResolvedConditionalAggregate(ResolvedFunction): # The internal rpc alias for this column @@ -389,10 +396,18 @@ def resolve( if self.attribute_resolver is not None: resolved_attribute = self.attribute_resolver(resolved_attribute) - if all(resolved_argument != "" for resolved_argument in resolved_arguments[1:]): + metric_name = None + metric_type = None + metric_unit = None + + if all( + isinstance(resolved_argument, str) and resolved_argument != "" + for resolved_argument in resolved_arguments[1:] + ): # a metric was passed - # TODO: we need to put it into the top level query conditions - pass + metric_name = cast(str, resolved_arguments[1]) + metric_type = cast(MetricType, resolved_arguments[2]) + metric_unit = None if resolved_arguments[3] == "-" else cast(str, resolved_arguments[3]) elif all(resolved_argument == "" for resolved_argument in resolved_arguments[1:]): # no metrics were specified, assume we query all metrics pass @@ -401,7 +416,7 @@ def resolve( f"Trace metric aggregates expect the full metric to be specified, got name:{resolved_arguments[1]} type:{resolved_arguments[2]} unit:{resolved_arguments[3]}" ) - return ResolvedAggregate( + return ResolvedMetricAggregate( public_alias=alias, internal_name=self.internal_function, search_type=search_type, @@ -411,6 +426,9 @@ def resolve( self.extrapolation if not search_config.disable_aggregate_extrapolation else False ), argument=resolved_attribute, + metric_name=metric_name, + metric_type=metric_type, + metric_unit=metric_unit, ) diff --git a/src/sentry/search/eap/resolver.py b/src/sentry/search/eap/resolver.py index 374d938a4c878b..03ca0158249dd7 100644 --- a/src/sentry/search/eap/resolver.py +++ b/src/sentry/search/eap/resolver.py @@ -56,6 +56,7 @@ ValueArgumentDefinition, VirtualColumnDefinition, ) +from sentry.search.eap.rpc_utils import and_trace_item_filters from sentry.search.eap.sampling import validate_sampling from sentry.search.eap.spans.attributes import SPANS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS from sentry.search.eap.types import EAPResponse, SearchResolverConfig @@ -139,31 +140,34 @@ def resolve_query(self, querystring: str | None) -> tuple[ span.set_tag("SearchResolver.resolved_query", where) span.set_tag("SearchResolver.environment_query", environment_query) - # The RPC request meta does not contain the environment. - # So we have to inject it as a query condition. - # - # To do so, we want to AND it with the query. - # So if either one is not defined, we just use the other. - # But if both are defined, we AND them together. + where = and_trace_item_filters( + where, + # The RPC request meta does not contain the environment. + # So we have to inject it as a query condition. + environment_query, + ) - if not environment_query: - return where, having, contexts + return where, having, contexts - if not where: - return environment_query, having, [] + @sentry_sdk.trace + def resolve_query_with_columns( + self, + querystring: str | None, + selected_columns: list[str] | None, + equations: list[str] | None, + ) -> tuple[ + TraceItemFilter | None, + AggregationFilter | None, + list[VirtualColumnDefinition | None], + ]: + where, having, contexts = self.resolve_query(querystring) - return ( - TraceItemFilter( - and_filter=AndFilter( - filters=[ - environment_query, - where, - ] - ) - ), - having, - contexts, - ) + # Some datasets like trace metrics require we inject additional + # conditions in the top level. + dataset_conditions = self.resolve_dataset_conditions(selected_columns, equations) + where = and_trace_item_filters(where, dataset_conditions) + + return where, having, contexts def __resolve_environment_query(self) -> TraceItemFilter | None: resolved_column, _ = self.resolve_column("environment") @@ -1174,3 +1178,12 @@ def _resolve_operation(self, operation: arithmetic.OperandType) -> tuple[ return Column(conditional_aggregation=col.proto_definition), contexts elif isinstance(col, ResolvedFormula): return Column(formula=col.proto_definition), contexts + + def resolve_dataset_conditions( + self, + selected_columns: list[str] | None, + equations: list[str] | None, + ) -> TraceItemFilter | None: + extra_conditions = self.config.extra_conditions(self, selected_columns, equations) + + return and_trace_item_filters(extra_conditions) diff --git a/src/sentry/search/eap/rpc_utils.py b/src/sentry/search/eap/rpc_utils.py new file mode 100644 index 00000000000000..2fb2465d41fc4e --- /dev/null +++ b/src/sentry/search/eap/rpc_utils.py @@ -0,0 +1,14 @@ +from sentry_protos.snuba.v1.trace_item_filter_pb2 import AndFilter, TraceItemFilter + + +def and_trace_item_filters( + *trace_item_filters: TraceItemFilter | None, +) -> TraceItemFilter | None: + filters: list[TraceItemFilter] = [f for f in trace_item_filters if f is not None] + if not filters: + return None + + if len(filters) == 1: + return filters[0] + + return TraceItemFilter(and_filter=AndFilter(filters=filters)) diff --git a/src/sentry/search/eap/trace_metrics/config.py b/src/sentry/search/eap/trace_metrics/config.py index ada9251bcf8db8..241171a99471bf 100644 --- a/src/sentry/search/eap/trace_metrics/config.py +++ b/src/sentry/search/eap/trace_metrics/config.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Literal, cast +from typing import cast from rest_framework.request import Request from sentry_protos.snuba.v1.trace_item_attribute_pb2 import AttributeKey, AttributeValue @@ -9,10 +9,18 @@ TraceItemFilter, ) +from sentry.exceptions import InvalidSearchQuery +from sentry.search.eap.columns import ResolvedMetricAggregate from sentry.search.eap.resolver import SearchResolver -from sentry.search.eap.types import SearchResolverConfig +from sentry.search.eap.types import MetricType, SearchResolverConfig +from sentry.search.events import fields -MetricType = Literal["counter", "gauge", "distribution"] + +@dataclass(frozen=True, kw_only=True) +class Metric: + metric_name: str + metric_type: MetricType + metric_unit: str | None @dataclass(frozen=True, kw_only=True) @@ -21,50 +29,123 @@ class TraceMetricsSearchResolverConfig(SearchResolverConfig): metric_type: MetricType | None metric_unit: str | None - def extra_conditions(self, search_resolver: SearchResolver) -> TraceItemFilter | None: - if not self.metric_name or not self.metric_type: + def extra_conditions( + self, + search_resolver: SearchResolver, + selected_columns: list[str] | None, + 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): + return extra_conditions + + # then try to parse metric from the aggregations + if extra_conditions := self._extra_conditions_from_columns( + search_resolver, selected_columns, equations + ): + return extra_conditions + + return None + + def _extra_conditions_from_columns( + self, + search_resolver: SearchResolver, + selected_columns: list[str] | None, + equations: list[str] | None, + ) -> TraceItemFilter | None: + selected_metrics: set[Metric] = set() + + if selected_columns: + stripped_columns = [column.strip() for column in selected_columns] + for column in stripped_columns: + match = fields.is_function(column) + if not match: + continue + + resolved_function, _ = search_resolver.resolve_function(column) + if not isinstance(resolved_function, ResolvedMetricAggregate): + continue + + if not resolved_function.metric_name or not resolved_function.metric_type: + continue + + metric = Metric( + metric_name=resolved_function.metric_name, + metric_type=resolved_function.metric_type, + metric_unit=resolved_function.metric_unit, + ) + selected_metrics.add(metric) + + if not selected_metrics: return None - metric_name, _ = search_resolver.resolve_column("metric.name") - if not isinstance(metric_name.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.name") + if len(selected_metrics) > 1: + raise InvalidSearchQuery("Cannot aggregate multiple metrics in 1 query.") - metric_type, _ = search_resolver.resolve_column("metric.type") - if not isinstance(metric_type.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.type") + selected_metric = selected_metrics.pop() - filters = [ - TraceItemFilter( - comparison_filter=ComparisonFilter( - key=metric_name.proto_definition, - op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=self.metric_name), - ) - ), + return get_metric_filter(search_resolver, selected_metric) + + def _extra_conditions_from_metric( + self, + search_resolver: SearchResolver, + ) -> TraceItemFilter | None: + if not self.metric_name or not self.metric_type: + return None + + metric = Metric( + metric_name=self.metric_name, + metric_type=self.metric_type, + metric_unit=self.metric_unit, + ) + + return get_metric_filter(search_resolver, metric) + + +def get_metric_filter( + search_resolver: SearchResolver, + metric: Metric, +) -> TraceItemFilter: + metric_name, _ = search_resolver.resolve_column("metric.name") + if not isinstance(metric_name.proto_definition, AttributeKey): + raise ValueError("Unable to resolve metric.name") + + metric_type, _ = search_resolver.resolve_column("metric.type") + if not isinstance(metric_type.proto_definition, AttributeKey): + raise ValueError("Unable to resolve metric.type") + + filters = [ + TraceItemFilter( + comparison_filter=ComparisonFilter( + key=metric_name.proto_definition, + op=ComparisonFilter.OP_EQUALS, + value=AttributeValue(val_str=metric.metric_name), + ) + ), + TraceItemFilter( + comparison_filter=ComparisonFilter( + key=metric_type.proto_definition, + op=ComparisonFilter.OP_EQUALS, + value=AttributeValue(val_str=metric.metric_type), + ) + ), + ] + + if metric.metric_unit: + metric_unit, _ = search_resolver.resolve_column("metric.unit") + if not isinstance(metric_unit.proto_definition, AttributeKey): + raise ValueError("Unable to resolve metric.unit") + filters.append( TraceItemFilter( comparison_filter=ComparisonFilter( - key=metric_type.proto_definition, + key=metric_unit.proto_definition, op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=self.metric_type), - ) - ), - ] - - if self.metric_unit: - metric_unit, _ = search_resolver.resolve_column("metric.unit") - if not isinstance(metric_unit.proto_definition, AttributeKey): - raise ValueError("Unable to resolve metric.unit") - filters.append( - TraceItemFilter( - comparison_filter=ComparisonFilter( - key=metric_unit.proto_definition, - op=ComparisonFilter.OP_EQUALS, - value=AttributeValue(val_str=self.metric_unit), - ) + value=AttributeValue(val_str=metric.metric_unit), ) ) + ) - return TraceItemFilter(and_filter=AndFilter(filters=filters)) + return TraceItemFilter(and_filter=AndFilter(filters=filters)) ALLOWED_METRIC_TYPES: list[MetricType] = ["counter", "gauge", "distribution"] diff --git a/src/sentry/search/eap/types.py b/src/sentry/search/eap/types.py index dbce61609cb7fc..bc97b73be9fa16 100644 --- a/src/sentry/search/eap/types.py +++ b/src/sentry/search/eap/types.py @@ -34,7 +34,12 @@ class SearchResolverConfig: # Whether to set the timestamp granularities to stable buckets stable_timestamp_quantization: bool = True - def extra_conditions(self, search_resolver: "SearchResolver") -> TraceItemFilter | None: + def extra_conditions( + self, + search_resolver: "SearchResolver", + selected_columns: list[str] | None, + equations: list[str] | None, + ) -> TraceItemFilter | None: return None @@ -81,3 +86,6 @@ class AdditionalQueries: span: list[str] | None log: list[str] | None metric: list[str] | None + + +MetricType = Literal["counter", "gauge", "distribution"] diff --git a/src/sentry/snuba/rpc_dataset_common.py b/src/sentry/snuba/rpc_dataset_common.py index 3ed2c43b7a1794..8deac61c0aa40d 100644 --- a/src/sentry/snuba/rpc_dataset_common.py +++ b/src/sentry/snuba/rpc_dataset_common.py @@ -48,6 +48,7 @@ ) from sentry.search.eap.constants import DOUBLE, MAX_ROLLUP_POINTS, VALID_GRANULARITIES from sentry.search.eap.resolver import SearchResolver +from sentry.search.eap.rpc_utils import and_trace_item_filters from sentry.search.eap.sampling import events_meta_from_rpc_request_meta from sentry.search.eap.types import ( CONFIDENCES, @@ -214,7 +215,11 @@ def get_table_rpc_request(cls, query: TableQuery) -> TableRequest: resolver = query.resolver sentry_sdk.set_tag("query.sampling_mode", query.sampling_mode) meta = resolver.resolve_meta(referrer=query.referrer, sampling_mode=query.sampling_mode) - where, having, query_contexts = resolver.resolve_query(query.query_string) + where, having, query_contexts = resolver.resolve_query_with_columns( + query.query_string, + query.selected_columns, + query.equations, + ) # if there are additional conditions to be added, make sure to merge them with the where = and_trace_item_filters(where, query.extra_conditions) @@ -556,9 +561,18 @@ def get_timeseries_query( list[AnyResolved], list[ResolvedAttribute], ]: + selected_equations, selected_axes = arithmetic.categorize_columns(y_axes) + (functions, _) = search_resolver.resolve_functions(selected_axes) + equations, _ = search_resolver.resolve_equations(selected_equations) + groupbys, groupby_contexts = search_resolver.resolve_attributes(groupby) + timeseries_filter, params = cls.update_timestamps(params, search_resolver) meta = search_resolver.resolve_meta(referrer=referrer, sampling_mode=sampling_mode) - query, _, query_contexts = search_resolver.resolve_query(query_string) + query, _, _ = search_resolver.resolve_query_with_columns( + query_string, + selected_axes, + selected_equations, + ) trace_column, _ = search_resolver.resolve_column("trace") if ( @@ -572,11 +586,6 @@ def get_timeseries_query( # incomplete traces. meta.downsampled_storage_config.mode = DownsampledStorageConfig.MODE_HIGHEST_ACCURACY - selected_equations, selected_axes = arithmetic.categorize_columns(y_axes) - (functions, _) = search_resolver.resolve_functions(selected_axes) - equations, _ = search_resolver.resolve_equations(selected_equations) - groupbys, groupby_contexts = search_resolver.resolve_attributes(groupby) - # Virtual context columns (VCCs) are currently only supported in TraceItemTable. # Since they are not supported here - we map them manually back to the original # column the virtual context column would have used. @@ -706,8 +715,6 @@ def run_top_events_timeseries_query( table_query_params.granularity_secs = None table_search_resolver = cls.get_resolver(table_query_params, config) - extra_conditions = config.extra_conditions(table_search_resolver) - # Make a table query first to get what we need to filter by _, non_equation_axes = arithmetic.categorize_columns(y_axes) top_events = cls._run_table_query( @@ -721,7 +728,6 @@ def run_top_events_timeseries_query( sampling_mode=sampling_mode, resolver=table_search_resolver, equations=equations, - extra_conditions=extra_conditions, ) ) # There aren't any top events, just return an empty dict and save a query @@ -738,7 +744,6 @@ def run_top_events_timeseries_query( top_conditions, other_conditions = cls.build_top_event_conditions( search_resolver, top_events, groupby_columns_without_project ) - extra_conditions = config.extra_conditions(search_resolver) """Make the queries""" rpc_request, aggregates, groupbys = cls.get_timeseries_query( @@ -749,7 +754,7 @@ def run_top_events_timeseries_query( groupby=groupby_columns_without_project, referrer=f"{referrer}.topn", sampling_mode=sampling_mode, - extra_conditions=and_trace_item_filters(top_conditions, extra_conditions), + extra_conditions=top_conditions, ) requests = [rpc_request] if include_other: @@ -761,7 +766,7 @@ def run_top_events_timeseries_query( groupby=[], # in the other series, we want eveything in a single group, so the group by is empty referrer=f"{referrer}.query-other", sampling_mode=sampling_mode, - extra_conditions=and_trace_item_filters(other_conditions, extra_conditions), + extra_conditions=other_conditions, ) requests.append(other_request) @@ -966,19 +971,3 @@ def transform_column_to_expression(column: Column) -> Expression: label=column.label, literal=column.literal, ) - - -def and_trace_item_filters( - *trace_item_filters: TraceItemFilter | None, -) -> TraceItemFilter | None: - trace_item_filter: TraceItemFilter | None = None - - for f in trace_item_filters: - if trace_item_filter is None: - trace_item_filter = f - elif f is not None: - trace_item_filter = TraceItemFilter( - and_filter=AndFilter(filters=[trace_item_filter, f]) - ) - - return trace_item_filter diff --git a/src/sentry/snuba/trace_metrics.py b/src/sentry/snuba/trace_metrics.py index 3b278c5f6d806e..0356b98ff3fa79 100644 --- a/src/sentry/snuba/trace_metrics.py +++ b/src/sentry/snuba/trace_metrics.py @@ -56,8 +56,6 @@ def run_table_query( search_resolver = search_resolver or cls.get_resolver(params=params, config=config) - extra_conditions = config.extra_conditions(search_resolver) - return cls._run_table_query( rpc_dataset_common.TableQuery( query_string=query_string, @@ -70,7 +68,6 @@ def run_table_query( resolver=search_resolver, page_token=page_token, additional_queries=additional_queries, - extra_conditions=extra_conditions, ), debug=debug, ) @@ -91,8 +88,6 @@ def run_timeseries_query( cls.validate_granularity(params) search_resolver = cls.get_resolver(params, config) - extra_conditions = config.extra_conditions(search_resolver) - rpc_request, aggregates, groupbys = cls.get_timeseries_query( search_resolver=search_resolver, params=params, @@ -101,7 +96,6 @@ def run_timeseries_query( groupby=[], referrer=referrer, sampling_mode=sampling_mode, - extra_conditions=extra_conditions, ) """Run the query""" diff --git a/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py b/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py index 2c052e1d7628e7..1647103ad454f2 100644 --- a/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py +++ b/tests/snuba/api/endpoints/test_organization_events_trace_metrics.py @@ -1,6 +1,7 @@ from unittest import mock import pytest +from rest_framework.exceptions import ErrorDetail from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase @@ -140,7 +141,7 @@ def test_sum_with_counter_metric_type(self): { "metricName": "request_count", "metricType": "counter", - "field": ["sum(value,request_count,counter,none)"], + "field": ["sum(value,request_count,counter,-)"], "project": self.project.id, "dataset": self.dataset, "statsPeriod": "10m", @@ -150,8 +151,8 @@ def test_sum_with_counter_metric_type(self): data = response.data["data"] meta = response.data["meta"] assert len(data) == 1 - assert data[0]["sum(value,request_count,counter,none)"] == 8 - assert meta["fields"]["sum(value,request_count,counter,none)"] == "number" + assert data[0]["sum(value,request_count,counter,-)"] == 8 + assert meta["fields"]["sum(value,request_count,counter,-)"] == "number" assert meta["dataset"] == "tracemetrics" def test_sum_with_distribution_metric_type(self): @@ -166,7 +167,7 @@ def test_sum_with_distribution_metric_type(self): "metricName": "request_duration", "metricType": "distribution", "field": [ - "sum(value, request_duration, distribution, none)" + "sum(value, request_duration, distribution, -)" ], # Trying space in the formula here to make sure it works. "project": self.project.id, "dataset": self.dataset, @@ -176,7 +177,7 @@ def test_sum_with_distribution_metric_type(self): assert response.status_code == 200, response.content data = response.data["data"] assert data[0] == { - "sum(value, request_duration, distribution, none)": 155, + "sum(value, request_duration, distribution, -)": 155, } def test_per_minute_formula(self) -> None: @@ -240,7 +241,7 @@ def test_per_second_formula_with_counter_metric_type(self) -> None: { "metricName": "request_count", "metricType": "counter", - "field": ["per_second(value,request_count,counter,none)"], + "field": ["per_second(value,request_count,counter,-)"], "project": self.project.id, "dataset": self.dataset, "statsPeriod": "10m", @@ -249,7 +250,7 @@ def test_per_second_formula_with_counter_metric_type(self) -> None: assert response.status_code == 200, response.content data = response.data["data"] assert data[0] == { - "per_second(value,request_count,counter,none)": pytest.approx(8 / 600, abs=0.001) + "per_second(value,request_count,counter,-)": pytest.approx(8 / 600, abs=0.001) } def test_per_second_formula_with_gauge_metric_type(self) -> None: @@ -264,7 +265,7 @@ def test_per_second_formula_with_gauge_metric_type(self) -> None: "metricName": "cpu_usage", "metricType": "gauge", "field": [ - "per_second(value, cpu_usage, gauge, none)" + "per_second(value, cpu_usage, gauge, -)" ], # Trying space in the formula here to make sure it works. "project": self.project.id, "dataset": self.dataset, @@ -274,7 +275,7 @@ def test_per_second_formula_with_gauge_metric_type(self) -> None: assert response.status_code == 200, response.content data = response.data["data"] assert data[0] == { - "per_second(value, cpu_usage, gauge, none)": pytest.approx(2 / 600, abs=0.001) + "per_second(value, cpu_usage, gauge, -)": pytest.approx(2 / 600, abs=0.001) } def test_per_second_formula_with_gauge_metric_type_without_top_level_metric_type(self) -> None: @@ -287,7 +288,7 @@ def test_per_second_formula_with_gauge_metric_type_without_top_level_metric_type response = self.do_request( { "field": [ - "per_second(value, cpu_usage, gauge, none)" + "per_second(value, cpu_usage, gauge, -)" ], # Trying space in the formula here to make sure it works. "query": "metric.name:cpu_usage", "project": self.project.id, @@ -298,7 +299,7 @@ def test_per_second_formula_with_gauge_metric_type_without_top_level_metric_type assert response.status_code == 200, response.content data = response.data["data"] assert data[0] == { - "per_second(value, cpu_usage, gauge, none)": pytest.approx(2 / 600, abs=0.001) + "per_second(value, cpu_usage, gauge, -)": pytest.approx(2 / 600, abs=0.001) } def test_list_metrics(self): @@ -345,3 +346,65 @@ def test_list_metrics(self): "count(metric.name)": 4, }, ] + + def test_aggregation_embedded_metric_name(self): + trace_metrics = [ + self.create_trace_metric("foo", 1, "counter"), + self.create_trace_metric("foo", 1, "counter"), + self.create_trace_metric("bar", 2, "counter"), + ] + self.store_trace_metrics(trace_metrics) + + response = self.do_request( + { + "field": ["count(value,foo,counter,-)"], + "dataset": self.dataset, + } + ) + assert response.status_code == 200, response.content + assert response.data["data"] == [ + {"count(value,foo,counter,-)": 2}, + ] + + def test_aggregation_multiple_embedded_same_metric_name(self): + trace_metrics = [ + self.create_trace_metric("foo", 1, "distribution"), + self.create_trace_metric("foo", 2, "distribution"), + self.create_trace_metric("bar", 2, "counter"), + ] + self.store_trace_metrics(trace_metrics) + + response = self.do_request( + { + "field": [ + "min(value,foo,distribution,-)", + "max(value,foo,distribution,-)", + ], + "dataset": self.dataset, + } + ) + assert response.status_code == 200, response.content + assert response.data["data"] == [ + { + "min(value,foo,distribution,-)": 1, + "max(value,foo,distribution,-)": 2, + }, + ] + + def test_aggregation_multiple_embedded_different_metric_name(self): + response = self.do_request( + { + "field": [ + "count(value,foo,counter,-)", + "count(value,bar,counter,-)", + ], + "dataset": self.dataset, + "project": self.project.id, + } + ) + assert response.status_code == 400, response.content + assert response.data == { + "detail": ErrorDetail( + "Cannot aggregate multiple metrics in 1 query.", code="parse_error" + ) + }