Skip to content

Commit 32814e0

Browse files
authored
feat(upsampling) - Organization Events API error upsampling support (#95473)
Support projects with error upsampling in Organization Events API - supporting count(), eps() and epm() columns. Required some manipulation on results to add the "()" back to the alias on the results from Snuba, because our SnQL parser doesnt allow them on aliases, but we want to convert back to the original column name after changing it under the hood.
1 parent 5d4ed06 commit 32814e0

File tree

6 files changed

+336
-8
lines changed

6 files changed

+336
-8
lines changed

src/sentry/api/bases/organization_events.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.api.base import CURSOR_LINK_HEADER
1919
from sentry.api.bases import NoProjects
2020
from sentry.api.bases.organization import FilterParamsDateNotNull, OrganizationEndpoint
21+
from sentry.api.helpers.error_upsampling import are_all_projects_error_upsampled
2122
from sentry.api.helpers.mobile import get_readable_device_name
2223
from sentry.api.helpers.teams import get_teams
2324
from sentry.api.serializers.snuba import SnubaTSResultSerializer
@@ -340,6 +341,8 @@ def handle_results_with_meta(
340341
meta = results.get("meta", {})
341342
fields_meta = meta.get("fields", {})
342343

344+
self.handle_error_upsampling(project_ids, results)
345+
343346
if standard_meta:
344347
isMetricsData = meta.pop("isMetricsData", False)
345348
isMetricsExtractedData = meta.pop("isMetricsExtractedData", False)
@@ -422,6 +425,37 @@ def handle_data(
422425

423426
return results
424427

428+
def handle_error_upsampling(self, project_ids: Sequence[int], results: dict[str, Any]):
429+
"""
430+
If the query is for error upsampled projects, we need to rename the fields to include the ()
431+
and update the meta fields to reflect the new field names. This works around a limitation in
432+
how aliases are handled in the SnQL parser.
433+
"""
434+
if are_all_projects_error_upsampled(project_ids):
435+
data = results.get("data", [])
436+
fields_meta = results.get("meta", {}).get("fields", {})
437+
438+
for result in data:
439+
if "count" in result:
440+
result["count()"] = result["count"]
441+
del result["count"]
442+
if "eps" in result:
443+
result["eps()"] = result["eps"]
444+
del result["eps"]
445+
if "epm" in result:
446+
result["epm()"] = result["epm"]
447+
del result["epm"]
448+
449+
if "count" in fields_meta:
450+
fields_meta["count()"] = fields_meta["count"]
451+
del fields_meta["count"]
452+
if "eps" in fields_meta:
453+
fields_meta["eps()"] = fields_meta["eps"]
454+
del fields_meta["eps"]
455+
if "epm" in fields_meta:
456+
fields_meta["epm()"] = fields_meta["epm"]
457+
del fields_meta["epm"]
458+
425459
def handle_issues(
426460
self, results: Sequence[Any], project_ids: Sequence[int], organization: Organization
427461
) -> None:

src/sentry/api/endpoints/organization_events.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from sentry.api.api_publish_status import ApiPublishStatus
1414
from sentry.api.base import region_silo_endpoint
1515
from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase
16+
from sentry.api.helpers.error_upsampling import (
17+
is_errors_query_for_error_upsampled_projects,
18+
transform_query_columns_for_error_upsampling,
19+
)
1620
from sentry.api.paginator import GenericOffsetPaginator
1721
from sentry.api.utils import handle_query_errors
1822
from sentry.apidocs import constants as api_constants
@@ -315,9 +319,16 @@ def _data_fn(
315319
limit: int,
316320
query: str | None,
317321
):
322+
transform_alias_to_input_format = True
323+
selected_columns = self.get_field_list(organization, request)
324+
if is_errors_query_for_error_upsampled_projects(
325+
snuba_params, organization, dataset, request
326+
):
327+
selected_columns = transform_query_columns_for_error_upsampling(selected_columns)
328+
transform_alias_to_input_format = False
318329
query_source = self.get_request_source(request)
319330
return dataset_query(
320-
selected_columns=self.get_field_list(organization, request),
331+
selected_columns=selected_columns,
321332
query=query or "",
322333
snuba_params=snuba_params,
323334
equations=self.get_equation_list(organization, request),
@@ -329,7 +340,7 @@ def _data_fn(
329340
auto_aggregations=True,
330341
allow_metric_aggregates=allow_metric_aggregates,
331342
use_aggregate_conditions=use_aggregate_conditions,
332-
transform_alias_to_input_format=True,
343+
transform_alias_to_input_format=transform_alias_to_input_format,
333344
# Whether the flag is enabled or not, regardless of the referrer
334345
has_metrics=use_metrics,
335346
use_metrics_layer=batch_features.get("organizations:use-metrics-layer", False),

src/sentry/api/helpers/error_upsampling.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,23 @@ def are_all_projects_error_upsampled(project_ids: Sequence[int]) -> bool:
4444
return result
4545

4646

47-
def transform_query_columns_for_error_upsampling(
48-
query_columns: Sequence[str],
49-
) -> list[str]:
47+
def transform_query_columns_for_error_upsampling(query_columns: Sequence[str]) -> list[str]:
5048
"""
5149
Transform aggregation functions to use sum(sample_weight) instead of count()
52-
for error upsampling. Only called when all projects are allowlisted.
50+
for error upsampling.
5351
"""
5452
transformed_columns = []
5553
for column in query_columns:
5654
column_lower = column.lower().strip()
5755

5856
if column_lower == "count()":
59-
# Simple count becomes sum of sample weights
6057
transformed_columns.append("upsampled_count() as count")
6158

59+
elif column_lower == "eps()":
60+
transformed_columns.append("upsampled_eps() as eps")
61+
62+
elif column_lower == "epm()":
63+
transformed_columns.append("upsampled_epm() as epm")
6264
else:
6365
transformed_columns.append(column)
6466

src/sentry/search/events/datasets/discover.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,23 @@ def function_converter(self) -> Mapping[str, SnQLFunction]:
10461046
[Function("sum", [Function("ifNull", [Column("sample_weight"), 1])])],
10471047
alias,
10481048
),
1049-
default_result_type="number",
1049+
default_result_type="integer",
1050+
),
1051+
SnQLFunction(
1052+
"upsampled_eps",
1053+
snql_aggregate=lambda args, alias: function_aliases.resolve_upsampled_eps(
1054+
args, alias, self.builder
1055+
),
1056+
optional_args=[IntervalDefault("interval", 1, None)],
1057+
default_result_type="rate",
1058+
),
1059+
SnQLFunction(
1060+
"upsampled_epm",
1061+
snql_aggregate=lambda args, alias: function_aliases.resolve_upsampled_epm(
1062+
args, alias, self.builder
1063+
),
1064+
optional_args=[IntervalDefault("interval", 1, None)],
1065+
default_result_type="rate",
10501066
),
10511067
]
10521068
}

src/sentry/search/events/datasets/function_aliases.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,22 @@ def resolve_eps(
379379
return Function("divide", [Function("count", []), interval], alias)
380380

381381

382+
def resolve_upsampled_eps(
383+
args: Mapping[str, str | Column | SelectType | int | float],
384+
alias: str,
385+
builder: BaseQueryBuilder,
386+
) -> SelectType:
387+
if hasattr(builder, "interval"):
388+
interval = builder.interval
389+
else:
390+
interval = args["interval"]
391+
return Function(
392+
"divide",
393+
[Function("sum", [Function("ifNull", [Column("sample_weight"), 1])]), interval],
394+
alias,
395+
)
396+
397+
382398
def resolve_epm(
383399
args: Mapping[str, str | Column | SelectType | int | float],
384400
alias: str,
@@ -393,3 +409,22 @@ def resolve_epm(
393409
[Function("count", []), Function("divide", [interval, 60])],
394410
alias,
395411
)
412+
413+
414+
def resolve_upsampled_epm(
415+
args: Mapping[str, str | Column | SelectType | int | float],
416+
alias: str,
417+
builder: BaseQueryBuilder,
418+
) -> SelectType:
419+
if hasattr(builder, "interval"):
420+
interval = builder.interval
421+
else:
422+
interval = args["interval"]
423+
return Function(
424+
"divide",
425+
[
426+
Function("sum", [Function("ifNull", [Column("sample_weight"), 1])]),
427+
Function("divide", [interval, 60]),
428+
],
429+
alias,
430+
)

0 commit comments

Comments
 (0)