Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,26 @@ 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,
)
)
return f.transform(
f.CAST(f.ifNull(attribute_expression, literal("")), "String"),
f.CAST(
f.ifNull(
attribute_expression,
literal("") if source_type == AttributeKey.TYPE_STRING else literal(0),
),
"String",
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null-coalescing fallback for non-string source types uses 0, which can change semantics vs the previous empty-string fallback: a missing value becomes "0" after the cast and could incorrectly match a value_map entry for "0". It also doesn’t handle TYPE_BOOLEAN cleanly (falls into the 0 branch). Consider casting attribute_expression to String first and then applying ifNull(..., "") (or otherwise choosing a fallback literal that matches source_type, e.g. False for booleans) so missing values reliably map to the default_value/"unknown" instead of potentially mapping to a real key.

Suggested change
f.CAST(
f.ifNull(
attribute_expression,
literal("") if source_type == AttributeKey.TYPE_STRING else literal(0),
),
"String",
f.ifNull(
f.CAST(
attribute_expression,
"String",
),
literal(""),

Copilot uses AI. Check for mistakes.
),
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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]))
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading