Skip to content

Commit 270022c

Browse files
authored
1 parent c349789 commit 270022c

File tree

5 files changed

+160
-11
lines changed

5 files changed

+160
-11
lines changed

src/sentry/search/eap/resolver.py

+26-5
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,9 @@ def resolve_term(
403403

404404
value = term.value.value
405405
if self.params.is_timeseries_request and context_definition is not None:
406-
resolved_column, value = self.map_context_to_original_column(term, context_definition)
406+
resolved_column, value = self.map_search_term_context_to_original_column(
407+
term, context_definition
408+
)
407409
context_definition = None
408410

409411
if not isinstance(resolved_column.proto_definition, AttributeKey):
@@ -503,9 +505,8 @@ def resolve_term(
503505

504506
def map_context_to_original_column(
505507
self,
506-
term: event_search.SearchFilter,
507508
context_definition: VirtualColumnDefinition,
508-
) -> tuple[ResolvedAttribute, str | int | list[str]]:
509+
) -> ResolvedAttribute:
509510
"""
510511
Time series request do not support virtual column contexts, so we have to remap the value back to the original column.
511512
(see https://github.com/getsentry/eap-planning/issues/236)
@@ -525,10 +526,30 @@ def map_context_to_original_column(
525526
if public_alias is None:
526527
raise InvalidSearchQuery(f"Cannot map {context.from_column_name} to a public alias")
527528

528-
value = term.value.value
529529
resolved_column, _ = self.resolve_column(public_alias)
530+
530531
if not isinstance(resolved_column.proto_definition, AttributeKey):
531-
raise ValueError(f"{term.key.name} is not valid search term")
532+
raise ValueError(f"{resolved_column.public_alias} is not valid search term")
533+
534+
return resolved_column
535+
536+
def map_search_term_context_to_original_column(
537+
self,
538+
term: event_search.SearchFilter,
539+
context_definition: VirtualColumnDefinition,
540+
) -> tuple[ResolvedAttribute, str | int | list[str]]:
541+
"""
542+
Time series request do not support virtual column contexts, so we have to remap the value back to the original column.
543+
(see https://github.com/getsentry/eap-planning/issues/236)
544+
"""
545+
context = context_definition.constructor(self.params)
546+
is_number_column = (
547+
context.from_column_name in SPANS_INTERNAL_TO_PUBLIC_ALIAS_MAPPINGS["number"]
548+
)
549+
550+
resolved_column = self.map_context_to_original_column(context_definition)
551+
552+
value = term.value.value
532553

533554
inverse_value_map: dict[str, list[str]] = {}
534555
for key, val in context.value_map.items():

src/sentry/snuba/rpc_dataset_common.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,17 @@ def get_timeseries_query(
114114
meta = search_resolver.resolve_meta(referrer=referrer, sampling_mode=sampling_mode)
115115
query, _, query_contexts = search_resolver.resolve_query(query_string)
116116
(functions, _) = search_resolver.resolve_functions(y_axes)
117-
(groupbys, _) = search_resolver.resolve_attributes(groupby)
117+
groupbys, groupby_contexts = search_resolver.resolve_attributes(groupby)
118+
119+
# Virtual context columns (VCCs) are currently only supported in TraceItemTable.
120+
# Since they are not supported here - we map them manually back to the original
121+
# column the virtual context column would have used.
122+
for i, groupby_definition in enumerate(zip(groupbys, groupby_contexts)):
123+
_, context = groupby_definition
124+
if context is not None:
125+
col = search_resolver.map_context_to_original_column(context)
126+
groupbys[i] = col
127+
118128
if extra_conditions is not None:
119129
if query is not None:
120130
query = TraceItemFilter(and_filter=AndFilter(filters=[query, extra_conditions]))

src/sentry/snuba/spans_rpc.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,20 @@ def run_top_events_timeseries_query(
207207
change this"""
208208
"""Make a table query first to get what we need to filter by"""
209209
rpc_dataset_common.validate_granularity(params)
210-
search_resolver = get_resolver(params=params, config=config)
210+
211+
# Virtual context columns (VCCs) are currently only supported in TraceItemTable.
212+
# For TopN queries, we want table and timeseries data to match.
213+
# Here, we want to run the table request the the VCCs. SnubaParams has
214+
# a property `is_timeseries_request` which resolves to true if granularity_secs is set.
215+
# `is_timeseries_request` is used to evaluate if VCCs should be used.
216+
# Unset granularity_secs, so this gets treated as a table request with
217+
# the correct VCC.
218+
table_query_params = params.copy()
219+
table_query_params.granularity_secs = None
220+
table_search_resolver = get_resolver(params=table_query_params, config=config)
221+
211222
top_events = run_table_query(
212-
params,
223+
table_query_params,
213224
query_string,
214225
raw_groupby + y_axes,
215226
orderby,
@@ -218,10 +229,12 @@ def run_top_events_timeseries_query(
218229
referrer,
219230
config,
220231
sampling_mode,
221-
search_resolver,
232+
table_search_resolver,
222233
)
223234
if len(top_events["data"]) == 0:
224235
return {}
236+
237+
search_resolver = get_resolver(params=params, config=config)
225238
# Need to change the project slug columns to project.id because timeseries requests don't take virtual_column_contexts
226239
groupby_columns = [col for col in raw_groupby if not is_function(col)]
227240
groupby_columns_without_project = [
@@ -275,8 +288,22 @@ def run_top_events_timeseries_query(
275288
int(groupby_attributes[resolved_groupby.internal_name])
276289
]
277290
else:
278-
resolved_groupby, _ = search_resolver.resolve_attribute(col)
279-
remapped_groupby[col] = groupby_attributes[resolved_groupby.internal_name]
291+
resolved_groupby, context = search_resolver.resolve_attribute(col)
292+
293+
# Virtual context columns (VCCs) are currently only supported in TraceItemTable.
294+
# Since timeseries run the query with the original column, we need to map
295+
# them correctly so they map the table result. We need to map both the column name
296+
# and the values.
297+
if context is not None:
298+
resolved_groupby = search_resolver.map_context_to_original_column(context)
299+
300+
groupby_value = groupby_attributes[resolved_groupby.internal_name]
301+
if context is not None:
302+
groupby_value = context.constructor(params).value_map[groupby_value]
303+
groupby_attributes[resolved_groupby.internal_name] = groupby_value
304+
305+
remapped_groupby[col] = groupby_value
306+
280307
result_key = create_result_key(remapped_groupby, groupby_columns, {})
281308
map_result_key_to_timeseries[result_key].append(timeseries)
282309
final_result = {}

tests/snuba/api/endpoints/test_organization_events_span_indexed.py

+26
Original file line numberDiff line numberDiff line change
@@ -2568,6 +2568,32 @@ def test_device_class_filter_unknown(self):
25682568
assert data[0]["device.class"] == "Unknown"
25692569
assert meta["dataset"] == self.dataset
25702570

2571+
def test_device_class_column(self):
2572+
self.store_spans(
2573+
[
2574+
self.create_span(
2575+
{"sentry_tags": {"device.class": "1"}}, start_ts=self.ten_mins_ago
2576+
),
2577+
],
2578+
is_eap=self.is_eap,
2579+
)
2580+
response = self.do_request(
2581+
{
2582+
"field": ["device.class", "count()"],
2583+
"query": "",
2584+
"orderby": "count()",
2585+
"project": self.project.id,
2586+
"dataset": self.dataset,
2587+
}
2588+
)
2589+
2590+
assert response.status_code == 200, response.content
2591+
data = response.data["data"]
2592+
meta = response.data["meta"]
2593+
assert len(data) == 1
2594+
assert data[0]["device.class"] == "low"
2595+
assert meta["dataset"] == self.dataset
2596+
25712597
def test_http_response_count(self):
25722598
self.store_spans(
25732599
[

tests/snuba/api/endpoints/test_organization_events_stats_span_indexed.py

+65
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from django.urls import reverse
55

6+
from sentry.search.utils import DEVICE_CLASS
67
from sentry.testutils.helpers.datetime import before_now
78
from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase
89
from tests.snuba.api.endpoints.test_organization_events_span_indexed import KNOWN_PREFLIGHT_ID
@@ -1319,6 +1320,70 @@ def test_device_class_filter(self):
13191320
for test in zip(event_counts, rows):
13201321
assert test[1][1][0]["count"] == test[0]
13211322

1323+
def test_device_class_top_events(self):
1324+
event_counts = [
1325+
("low", 6),
1326+
("medium", 0),
1327+
("low", 6),
1328+
("medium", 6),
1329+
("low", 0),
1330+
("medium", 3),
1331+
]
1332+
spans = []
1333+
for hour, count in enumerate(event_counts):
1334+
spans.extend(
1335+
[
1336+
self.create_span(
1337+
{
1338+
"description": "foo",
1339+
"sentry_tags": {
1340+
"status": "success",
1341+
"device.class": (
1342+
list(DEVICE_CLASS["low"])[0]
1343+
if count[0] == "low"
1344+
else list(DEVICE_CLASS["medium"])[0]
1345+
),
1346+
},
1347+
},
1348+
start_ts=self.day_ago + timedelta(hours=hour, minutes=minute),
1349+
)
1350+
for minute in range(count[1])
1351+
],
1352+
)
1353+
self.store_spans(spans, is_eap=self.is_eap)
1354+
1355+
response = self._do_request(
1356+
data={
1357+
"start": self.day_ago,
1358+
"end": self.day_ago + timedelta(hours=6),
1359+
"interval": "1h",
1360+
"yAxis": "count()",
1361+
"field": ["device.class", "count()"],
1362+
"topEvents": 5,
1363+
"query": "",
1364+
"project": self.project.id,
1365+
"dataset": self.dataset,
1366+
},
1367+
)
1368+
assert response.status_code == 200, response.content
1369+
low = response.data["low"]["data"]
1370+
assert len(low) == 6
1371+
1372+
rows = low[0:6]
1373+
for i, test in enumerate(zip(event_counts, rows)):
1374+
test_data, row = test
1375+
test_count = test_data[1] if test_data[0] == "low" else 0.0
1376+
assert row[1][0]["count"] == test_count
1377+
1378+
medium = response.data["medium"]["data"]
1379+
assert len(medium) == 6
1380+
1381+
rows = medium[0:6]
1382+
for i, test in enumerate(zip(event_counts, rows)):
1383+
test_data, row = test
1384+
test_count = test_data[1] if test_data[0] == "medium" else 0.0
1385+
assert row[1][0]["count"] == test_count
1386+
13221387
def test_top_events_filters_out_groupby_even_when_its_just_one_row(self):
13231388
self.store_spans(
13241389
[

0 commit comments

Comments
 (0)