diff --git a/pyproject.toml b/pyproject.toml index 53e87af681..74829ef3bd 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 d736582553..f6ed48cd4d 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 @@ -145,16 +145,29 @@ 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] + ) + if source_type not in ( + AttributeKey.Type.TYPE_STRING, + AttributeKey.Type.TYPE_INT, + ): + raise MalformedAttributeException("VCC can only map string or int attributes") 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, ) ) return f.transform( - f.CAST(f.ifNull(attribute_expression, literal("")), "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()]), literal(context.default_value if context.default_value != "" else "unknown"), @@ -608,7 +621,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: @@ -677,10 +693,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=[]), 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 94e7fae686..562e1ccc13 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 = 1 + project_id = 1 + 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 46350808da..09ac9664fd 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" },