Skip to content

Commit 7f656e6

Browse files
authored
feat(explore) Add equation support to stats queries (#93252)
- Follow up to #92354, add equation support to the timeseries endpoints
1 parent a09846b commit 7f656e6

File tree

7 files changed

+500
-17
lines changed

7 files changed

+500
-17
lines changed

src/sentry/api/bases/organization_events.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,12 @@ def get_query_columns(columns, rollup):
6363

6464

6565
def resolve_axis_column(
66-
column: str, index: int = 0, transform_alias_to_input_format: bool = False
66+
column: str,
67+
index: int = 0,
68+
transform_alias_to_input_format: bool = False,
69+
use_rpc: bool = False,
6770
) -> str:
68-
if is_equation(column):
71+
if is_equation(column) and not use_rpc:
6972
return f"equation[{index}]"
7073

7174
# Function columns on input have names like `"p95(duration)"`. By default, we convert them to their aliases like `"p95_duration"`. Here, we want to preserve the original name, so we return the column as-is
@@ -87,10 +90,14 @@ def has_feature(self, organization: Organization, request: Request) -> bool:
8790
)
8891
)
8992

90-
def get_equation_list(self, organization: Organization, request: Request) -> list[str]:
93+
def get_equation_list(
94+
self, organization: Organization, request: Request, param_name: str = "field"
95+
) -> list[str]:
9196
"""equations have a prefix so that they can be easily included alongside our existing fields"""
9297
return [
93-
strip_equation(field) for field in request.GET.getlist("field")[:] if is_equation(field)
98+
strip_equation(field)
99+
for field in request.GET.getlist(param_name)[:]
100+
if is_equation(field)
94101
]
95102

96103
def get_field_list(
@@ -546,14 +553,15 @@ def get_event_stats_data(
546553
zerofill_results=zerofill_results,
547554
dataset=dataset,
548555
transform_alias_to_input_format=transform_alias_to_input_format,
556+
use_rpc=use_rpc,
549557
)
550558
if request.query_params.get("useOnDemandMetrics") == "true":
551559
results[key]["isMetricsExtractedData"] = self._query_if_extracted_data(
552560
results, key, query_columns
553561
)
554562
else:
555563
column = resolve_axis_column(
556-
query_columns[0], 0, transform_alias_to_input_format
564+
query_columns[0], 0, transform_alias_to_input_format, use_rpc
557565
)
558566
results[key] = serializer.serialize(
559567
event_result,
@@ -586,14 +594,17 @@ def get_event_stats_data(
586594
zerofill_results=zerofill_results,
587595
dataset=dataset,
588596
transform_alias_to_input_format=transform_alias_to_input_format,
597+
use_rpc=use_rpc,
589598
)
590599
if top_events > 0 and isinstance(result, SnubaTSResult):
591600
serialized_result = {"": serialized_result}
592601
else:
593602
extra_columns = None
594603
if comparison_delta:
595604
extra_columns = ["comparisonCount"]
596-
column = resolve_axis_column(query_columns[0], 0, transform_alias_to_input_format)
605+
column = resolve_axis_column(
606+
query_columns[0], 0, transform_alias_to_input_format, use_rpc
607+
)
597608
serialized_result = serializer.serialize(
598609
result,
599610
column=column,
@@ -643,6 +654,7 @@ def serialize_multiple_axis(
643654
zerofill_results: bool = True,
644655
dataset: Any | None = None,
645656
transform_alias_to_input_format: bool = False,
657+
use_rpc: bool = False,
646658
) -> dict[str, Any]:
647659
# Return with requested yAxis as the key
648660
result = {}
@@ -658,7 +670,9 @@ def serialize_multiple_axis(
658670
for index, query_column in enumerate(query_columns):
659671
result[columns[index]] = serializer.serialize(
660672
event_result,
661-
resolve_axis_column(query_column, equations, transform_alias_to_input_format),
673+
resolve_axis_column(
674+
query_column, equations, transform_alias_to_input_format, use_rpc
675+
),
662676
order=index,
663677
allow_partial_buckets=allow_partial_buckets,
664678
zerofill_results=zerofill_results,

src/sentry/api/endpoints/organization_events_stats.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ def _get_event_stats(
226226
use_aggregate_conditions=True,
227227
),
228228
sampling_mode=snuba_params.sampling_mode,
229+
equations=self.get_equation_list(organization, request),
229230
)
230231
return scoped_dataset.top_events_timeseries(
231232
timeseries_columns=query_columns,

src/sentry/api/endpoints/organization_events_timeseries.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ def get_event_stats(
248248
use_aggregate_conditions=True,
249249
),
250250
sampling_mode=snuba_params.sampling_mode,
251+
equations=self.get_equation_list(organization, request, param_name="groupBy"),
251252
)
252253
return dataset.top_events_timeseries(
253254
timeseries_columns=query_columns,

src/sentry/snuba/rpc_dataset_common.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929

3030
from sentry.api.event_search import SearchFilter, SearchKey, SearchValue
31+
from sentry.discover import arithmetic
3132
from sentry.exceptions import InvalidSearchQuery
3233
from sentry.search.eap.columns import (
3334
ResolvedAggregate,
@@ -104,20 +105,22 @@ def categorize_column(
104105

105106

106107
def categorize_aggregate(
107-
column: ResolvedAggregate | ResolvedConditionalAggregate | ResolvedFormula,
108+
column: ResolvedAggregate | ResolvedConditionalAggregate | ResolvedFormula | ResolvedEquation,
108109
) -> Expression:
109-
if isinstance(column, ResolvedFormula):
110+
if isinstance(column, (ResolvedFormula, ResolvedEquation)):
110111
# TODO: Remove when https://github.com/getsentry/eap-planning/issues/206 is merged, since we can use formulas in both APIs at that point
111112
return Expression(
112113
formula=transform_binary_formula_to_expression(column.proto_definition),
113114
label=column.public_alias,
114115
)
115-
if isinstance(column, ResolvedAggregate):
116+
elif isinstance(column, ResolvedAggregate):
116117
return Expression(aggregation=column.proto_definition, label=column.public_alias)
117-
if isinstance(column, ResolvedConditionalAggregate):
118+
elif isinstance(column, ResolvedConditionalAggregate):
118119
return Expression(
119120
conditional_aggregation=column.proto_definition, label=column.public_alias
120121
)
122+
else:
123+
raise Exception(f"Unknown column type {type(column)}")
121124

122125

123126
def update_timestamps(
@@ -189,13 +192,15 @@ def get_timeseries_query(
189192
extra_conditions: TraceItemFilter | None = None,
190193
) -> tuple[
191194
TimeSeriesRequest,
192-
list[ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate],
195+
list[ResolvedFormula | ResolvedAggregate | ResolvedConditionalAggregate | ResolvedEquation],
193196
list[ResolvedAttribute],
194197
]:
195198
timeseries_filter, params = update_timestamps(params, search_resolver)
196199
meta = search_resolver.resolve_meta(referrer=referrer, sampling_mode=sampling_mode)
197200
query, _, query_contexts = search_resolver.resolve_query(query_string)
198-
(functions, _) = search_resolver.resolve_functions(y_axes)
201+
selected_equations, selected_axes = arithmetic.categorize_columns(y_axes)
202+
(functions, _) = search_resolver.resolve_functions(selected_axes)
203+
equations, _ = search_resolver.resolve_equations(selected_equations)
199204
groupbys, groupby_contexts = search_resolver.resolve_attributes(groupby)
200205

201206
# Virtual context columns (VCCs) are currently only supported in TraceItemTable.
@@ -223,15 +228,17 @@ def get_timeseries_query(
223228
TimeSeriesRequest(
224229
meta=meta,
225230
filter=query,
226-
expressions=[categorize_aggregate(fn) for fn in functions if fn.is_aggregate],
231+
expressions=[
232+
categorize_aggregate(fn) for fn in (functions + equations) if fn.is_aggregate
233+
],
227234
group_by=[
228235
groupby.proto_definition
229236
for groupby in groupbys
230237
if isinstance(groupby.proto_definition, AttributeKey)
231238
],
232239
granularity_secs=params.timeseries_granularity_secs,
233240
),
234-
functions,
241+
(functions + equations),
235242
groupbys,
236243
)
237244

@@ -489,6 +496,7 @@ def run_top_events_timeseries_query(
489496
referrer: str,
490497
config: SearchResolverConfig,
491498
sampling_mode: SAMPLING_MODES | None,
499+
equations: list[str] | None = None,
492500
) -> Any:
493501
"""We intentionally duplicate run_timeseries_query code here to reduce the complexity of needing multiple helper
494502
functions that both would call
@@ -508,17 +516,18 @@ def run_top_events_timeseries_query(
508516
table_search_resolver = get_resolver(table_query_params, config)
509517

510518
# Make a table query first to get what we need to filter by
519+
_, non_equation_axes = arithmetic.categorize_columns(y_axes)
511520
top_events = run_table_query(
512521
TableQuery(
513522
query_string=query_string,
514-
selected_columns=raw_groupby + y_axes,
523+
selected_columns=raw_groupby + non_equation_axes,
515524
orderby=orderby,
516525
offset=0,
517526
limit=limit,
518527
referrer=referrer,
519528
sampling_mode=sampling_mode,
520529
resolver=table_search_resolver,
521-
equations=[],
530+
equations=equations,
522531
)
523532
)
524533
if len(top_events["data"]) == 0:

src/sentry/snuba/spans_rpc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def run_top_events_timeseries_query(
159159
referrer: str,
160160
config: SearchResolverConfig,
161161
sampling_mode: SAMPLING_MODES | None,
162+
equations: list[str] | None = None,
162163
) -> Any:
163164
return rpc_dataset_common.run_top_events_timeseries_query(
164165
get_resolver=get_resolver,
@@ -171,6 +172,7 @@ def run_top_events_timeseries_query(
171172
referrer=referrer,
172173
config=config,
173174
sampling_mode=sampling_mode,
175+
equations=equations,
174176
)
175177

176178

0 commit comments

Comments
 (0)