From 44574fa3c7a90f07216b15eb8473938f1ff8bead Mon Sep 17 00:00:00 2001 From: xurui-c Date: Thu, 2 Apr 2026 13:34:39 -0700 Subject: [PATCH 1/6] c --- pyproject.toml | 2 +- .../R_eap_items/resolver_trace_item_table.py | 20 +++- .../test_endpoint_trace_item_table.py | 101 +++++++++++++++++- uv.lock | 8 +- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 53e87af681c..74829ef3bd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "sentry-arroyo>=2.38.7", "sentry-conventions>=0.3.0", "sentry-kafka-schemas>=2.1.24", - "sentry-protos>=0.7.0", + "sentry-protos>=0.8.9", "sentry-redis-tools>=0.5.1", "sentry-relay>=0.9.25", "sentry-sdk>=2.35.0", diff --git a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py index d736582553f..1d8e73d22b7 100644 --- a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py +++ b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py @@ -145,16 +145,28 @@ def transform_expressions(expression: Expression) -> Expression: return expression context = mapped_column_to_context.get(str(expression.key.value)) if context: + source_type = ( + context.from_column_type + or NORMALIZED_COLUMNS_EAP_ITEMS.get( + context.from_column_name, [AttributeKey.TYPE_STRING] + )[0] + ) attribute_expression = attribute_key_to_expression( AttributeKey( name=context.from_column_name, - type=NORMALIZED_COLUMNS_EAP_ITEMS.get( - context.from_column_name, [AttributeKey.TYPE_STRING] - )[0], + type=source_type, ) ) + # ifNull default must match the numeric/string type of the source column; + # mismatching types (e.g. Int64 vs '') causes a ClickHouse type error. + numeric_types = ( + AttributeKey.TYPE_INT, + AttributeKey.TYPE_DOUBLE, + AttributeKey.TYPE_BOOLEAN, + ) + ifnull_default = literal(0) if source_type in numeric_types else literal("") return f.transform( - f.CAST(f.ifNull(attribute_expression, literal("")), "String"), + f.CAST(f.ifNull(attribute_expression, ifnull_default), "String"), literals_array(None, [literal(k) for k in context.value_map.keys()]), literals_array(None, [literal(v) for v in context.value_map.values()]), literal(context.default_value if context.default_value != "" else "unknown"), diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py index 94e7fae6863..568725cc564 100644 --- a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py +++ b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py @@ -1,6 +1,6 @@ import random import re -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from math import isclose from typing import Any from unittest.mock import MagicMock, call, patch @@ -3471,6 +3471,105 @@ def test_multiply_attribute_aggregation(self) -> None: assert len(res.results) == 1 assert isclose(res.results[0].val_double, expected_avg) + def test_occurrence_virtual_column_mapping(self) -> None: + """ + Reproduces the request shape sent by api.organization-events for the issues + view: OCCURRENCE items with virtual column contexts that remap group_id -> + issue and sentry.project_id -> project / project.name. + """ + org_id = 4557819828305920 + project_id = 4557819828633600 + item_ts = datetime.fromtimestamp(1773929000, tz=timezone.utc) + + items_storage = get_storage(StorageKey("eap_items")) + write_raw_unprocessed_events( + items_storage, # type: ignore + [ + gen_item_message( + start_timestamp=item_ts, + type=TraceItemType.TRACE_ITEM_TYPE_OCCURRENCE, + attributes={"group_id": AnyValue(int_value=1)}, + project_id=project_id, + organization_id=org_id, + remove_default_attributes=True, + ) + ], + ) + + message = TraceItemTableRequest( + meta=RequestMeta( + organization_id=org_id, + referrer="api.organization-events", + project_ids=[project_id], + start_timestamp=Timestamp(seconds=1773925780), + end_timestamp=Timestamp(seconds=1773933040), + trace_item_type=TraceItemType.TRACE_ITEM_TYPE_OCCURRENCE, + downsampled_storage_config=DownsampledStorageConfig( + mode=DownsampledStorageConfig.MODE_NORMAL, + ), + ), + columns=[ + Column( + key=AttributeKey(type=AttributeKey.TYPE_STRING, name="issue"), + label="issue", + ), + Column( + key=AttributeKey(type=AttributeKey.TYPE_INT, name="group_id"), + label="group_id", + ), + Column( + key=AttributeKey(type=AttributeKey.TYPE_STRING, name="project"), + label="project", + ), + Column( + key=AttributeKey(type=AttributeKey.TYPE_STRING, name="sentry.item_id"), + label="id", + ), + Column( + key=AttributeKey(type=AttributeKey.TYPE_STRING, name="project.name"), + label="project.name", + ), + ], + limit=101, + page_token=PageToken(offset=0), + virtual_column_contexts=[ + VirtualColumnContext( + from_column_name="group_id", + to_column_name="issue", + value_map={"1": "BAR-1"}, + from_column_type=AttributeKey.TYPE_INT, + ), + VirtualColumnContext( + from_column_name="sentry.project_id", + to_column_name="project", + value_map={str(project_id): "bar"}, + ), + VirtualColumnContext( + from_column_name="sentry.project_id", + to_column_name="project.name", + value_map={str(project_id): "bar"}, + ), + ], + ) + + response = EndpointTraceItemTable().execute(message) + + assert [c.attribute_name for c in response.column_values] == [ + "issue", + "group_id", + "project", + "id", + "project.name", + ] + + col = {c.attribute_name: c for c in response.column_values} + assert col["issue"].results == [AttributeValue(val_str="BAR-1")] + assert col["group_id"].results == [AttributeValue(val_int=1)] + assert col["project"].results == [AttributeValue(val_str="bar")] + assert col["project.name"].results == [AttributeValue(val_str="bar")] + assert len(col["id"].results) == 1 + assert col["id"].results[0].val_str != "" + def _str_array(*values: str) -> AnyValue: return AnyValue(array_value=ArrayValue(values=[AnyValue(string_value=v) for v in values])) diff --git a/uv.lock b/uv.lock index 46350808da2..09ac9664fd8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" resolution-markers = [ "sys_platform == 'darwin'", @@ -950,7 +950,7 @@ wheels = [ [[package]] name = "sentry-protos" -version = "0.7.0" +version = "0.8.9" source = { registry = "https://pypi.devinfra.sentry.io/simple" } dependencies = [ { name = "grpc-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -958,7 +958,7 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] wheels = [ - { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.7.0-py3-none-any.whl", hash = "sha256:08fd8c88b50c14c2b95b6f23ea0ea2b4afec1e82b49484a95c914d8daf94a2d5" }, + { url = "https://pypi.devinfra.sentry.io/wheels/sentry_protos-0.8.9-py3-none-any.whl", hash = "sha256:047142d719f8fd4f533a6b3813f444871e6ab61c7ec7dd19a16c5fa70d916238" }, ] [[package]] @@ -1158,7 +1158,7 @@ requires-dist = [ { name = "sentry-arroyo", specifier = ">=2.38.7" }, { name = "sentry-conventions", specifier = ">=0.3.0" }, { name = "sentry-kafka-schemas", specifier = ">=2.1.24" }, - { name = "sentry-protos", specifier = ">=0.7.0" }, + { name = "sentry-protos", specifier = ">=0.8.9" }, { name = "sentry-redis-tools", specifier = ">=0.5.1" }, { name = "sentry-relay", specifier = ">=0.9.25" }, { name = "sentry-sdk", specifier = ">=2.35.0" }, From dfe9faca2d00dbdf33865f77e80a75eb6a66abc1 Mon Sep 17 00:00:00 2001 From: xurui-c Date: Sun, 5 Apr 2026 22:04:24 -0700 Subject: [PATCH 2/6] simplify --- .../R_eap_items/resolver_trace_item_table.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py index 1d8e73d22b7..13a14050ff3 100644 --- a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py +++ b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py @@ -157,16 +157,14 @@ def transform_expressions(expression: Expression) -> Expression: type=source_type, ) ) - # ifNull default must match the numeric/string type of the source column; - # mismatching types (e.g. Int64 vs '') causes a ClickHouse type error. - numeric_types = ( - AttributeKey.TYPE_INT, - AttributeKey.TYPE_DOUBLE, - AttributeKey.TYPE_BOOLEAN, - ) - ifnull_default = literal(0) if source_type in numeric_types else literal("") return f.transform( - f.CAST(f.ifNull(attribute_expression, ifnull_default), "String"), + f.CAST( + f.ifNull( + attribute_expression, + literal("") if source_type == AttributeKey.TYPE_STRING else literal(0), + ), + "String", + ), literals_array(None, [literal(k) for k in context.value_map.keys()]), literals_array(None, [literal(v) for v in context.value_map.values()]), literal(context.default_value if context.default_value != "" else "unknown"), From a0daa9bc6e93caf75e50fdcfbc447c7855050991 Mon Sep 17 00:00:00 2001 From: xurui-c Date: Mon, 6 Apr 2026 10:01:04 -0700 Subject: [PATCH 3/6] dummy values --- .../test_endpoint_trace_item_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py index 568725cc564..562e1ccc13e 100644 --- a/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py +++ b/tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py @@ -3477,8 +3477,8 @@ def test_occurrence_virtual_column_mapping(self) -> None: view: OCCURRENCE items with virtual column contexts that remap group_id -> issue and sentry.project_id -> project / project.name. """ - org_id = 4557819828305920 - project_id = 4557819828633600 + org_id = 1 + project_id = 1 item_ts = datetime.fromtimestamp(1773929000, tz=timezone.utc) items_storage = get_storage(StorageKey("eap_items")) From 4652fd4596d5a62228aa038d9dabcffa9fb55073 Mon Sep 17 00:00:00 2001 From: xurui-c Date: Mon, 6 Apr 2026 16:04:05 -0700 Subject: [PATCH 4/6] update --- .../R_eap_items/resolver_trace_item_table.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py index 13a14050ff3..f6d1e14d824 100644 --- a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py +++ b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py @@ -158,12 +158,10 @@ def transform_expressions(expression: Expression) -> Expression: ) ) return f.transform( - f.CAST( - f.ifNull( - attribute_expression, - literal("") if source_type == AttributeKey.TYPE_STRING else literal(0), - ), - "String", + if_cond( + f.isNull(attribute_expression), + literal(""), + f.toString(attribute_expression), ), literals_array(None, [literal(k) for k in context.value_map.keys()]), literals_array(None, [literal(v) for v in context.value_map.values()]), @@ -618,7 +616,10 @@ def _get_page_token( # the routing strategy will properly truncate the time window of the next request return FlexibleTimeWindowPageWithFilters.create( request, - TimeWindow(original_time_window.start_timestamp, time_window.start_timestamp), + TimeWindow( + original_time_window.start_timestamp, + time_window.start_timestamp, + ), response, ).page_token else: @@ -687,10 +688,15 @@ def resolve( except Exception as e: sentry_sdk.capture_message(f"Error merging clickhouse settings: {e}") original_time_window = TimeWindow( - start_timestamp=in_msg.meta.start_timestamp, end_timestamp=in_msg.meta.end_timestamp + start_timestamp=in_msg.meta.start_timestamp, + end_timestamp=in_msg.meta.end_timestamp, ) snuba_request = _build_snuba_request( - in_msg, query_settings, routing_decision.time_window, routing_decision.tier, self._timer + in_msg, + query_settings, + routing_decision.time_window, + routing_decision.tier, + self._timer, ) res = run_query( dataset=PluggableDataset(name="eap", all_entities=[]), From 7a1209bd9b9c60a3b838f7af78e4775357f10ae6 Mon Sep 17 00:00:00 2001 From: xurui-c Date: Tue, 7 Apr 2026 11:20:03 -0700 Subject: [PATCH 5/6] type enforcement --- .../rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py index f6d1e14d824..0bb944ef8b2 100644 --- a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py +++ b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py @@ -151,6 +151,10 @@ def transform_expressions(expression: Expression) -> Expression: context.from_column_name, [AttributeKey.TYPE_STRING] )[0] ) + assert source_type in ( + AttributeKey.Type.TYPE_STRING, + AttributeKey.Type.TYPE_INT, + ), "VCC can only map string or int attributes" attribute_expression = attribute_key_to_expression( AttributeKey( name=context.from_column_name, From 4243058cc7a1161c43924dfdda8d672d0e2723f4 Mon Sep 17 00:00:00 2001 From: xurui-c Date: Tue, 7 Apr 2026 11:29:46 -0700 Subject: [PATCH 6/6] type enforcement --- .../v1/resolvers/R_eap_items/resolver_trace_item_table.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py index 0bb944ef8b2..f6ed48cd4d6 100644 --- a/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py +++ b/snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py @@ -31,7 +31,7 @@ from snuba.datasets.entities.factory import get_entity from snuba.datasets.pluggable_dataset import PluggableDataset from snuba.downsampled_storage_tiers import Tier -from snuba.protos.common import NORMALIZED_COLUMNS_EAP_ITEMS +from snuba.protos.common import NORMALIZED_COLUMNS_EAP_ITEMS, MalformedAttributeException from snuba.query import OrderBy, OrderByDirection, SelectedExpression from snuba.query.data_source.simple import Entity from snuba.query.dsl import Functions as f @@ -151,10 +151,11 @@ def transform_expressions(expression: Expression) -> Expression: context.from_column_name, [AttributeKey.TYPE_STRING] )[0] ) - assert source_type in ( + if source_type not in ( AttributeKey.Type.TYPE_STRING, AttributeKey.Type.TYPE_INT, - ), "VCC can only map string or int attributes" + ): + raise MalformedAttributeException("VCC can only map string or int attributes") attribute_expression = attribute_key_to_expression( AttributeKey( name=context.from_column_name,