Skip to content

feat(eap): Resolve session_id and gen_ai.conversation.id to their indexed columns#8221

Open
pbhandari wants to merge 7 commits into
masterfrom
ref/use-indexed-columns
Open

feat(eap): Resolve session_id and gen_ai.conversation.id to their indexed columns#8221
pbhandari wants to merge 7 commits into
masterfrom
ref/use-indexed-columns

Conversation

@pbhandari

@pbhandari pbhandari commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This was previously implemented in - #8185, and had to be reverted due to CI failures. This also updates our CI to include the tests that failed in that run. Fixes for those tests are in getsentry/sentry#120608.

Description

sentry.session_id and gen_ai.conversation.id now resolve to their dedicated top-level columns (session_id, ai_conversation_id) instead of the attributes_string[...] map, so = / IN <literal> filters on them prune
granules through the bf_session_id / bf_ai_conversation_id bloom-filter skip indexes.

The change is in the shared attribute_key_to_expression resolver, so every EAP endpoint (/events/ TraceItemTable, TimeSeries, TraceItemStats, GetTraces, GetTrace, TraceItemDetails, ExportTraceItems) gets it automatically.

To allow this, we refactored NORMALIZED_COLUMNS_EAP_ITEMS so that it now maps an attribute name to an explicit NormalizedColumn(column_name, types) named-tuple rather than deriving the column by stripping the sentry. prefix. That is to facilitategen_ai.conversation.id to map to ai_conversation_id.

We also removed the COLUMN_PREFIX constant, as it's no longer serving it's original purpose. The lone sentry.timestamp check that used it is inlined.

Review focus — absence semantics: a missing value for used to read as NULL (map miss) and now reads as the column default ('' for the String ai_conversation_id, the zero UUID for session_id). This matches how existing normalized columns such as sentry.trace_id already behave. We expect the columns to already have been populated where appropriate.

tests/web/rpc/v1/test_indexed_columns.py runs each filter through a real TraceItemTable query and asserts via EXPLAIN indexes = 1 that the expected skip index is applied, and that the returned rows are correct.

Related PRs

Refs: https://linear.app/getsentry/issue/EAP-620

🤖 Generated with Claude Code

@pbhandari
pbhandari requested review from a team as code owners July 24, 2026 20:47
@linear-code

linear-code Bot commented Jul 24, 2026

Copy link
Copy Markdown

EAP-620

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 74431a1. Configure here.

base = field.parameters[0]
if isinstance(base, Column) and base.column_name in EMPTY_STRING_DEFAULT_COLUMNS:
return f.notEmpty(field)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Session exists filter always matches

Medium Severity

Registering sentry.session_id as a normalized column makes exists filters use isNotNull on a cast of the non-nullable session_id column, which is always true. Spans without the attribute in attributes_string used to fail an existence check; they now pass it even though the ingested column value is an auto-generated UUID.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 74431a1. Configure here.

# so a bound equal to the mandatory range is byte-identical to it and gets
# collapsed by dedupe_timestamp_conditions.
if k.name == f"{COLUMN_PREFIX}timestamp" and value_type in (
if k.name == sentry_column("timestamp") and value_type in (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Null filters break conversation id

High Severity

Moving gen_ai.conversation.id off the map-backed filter path leaves null comparisons on the generic normalized-column logic. = null no longer matches spans whose unset ai_conversation_id is '', and != null adds an xor(isNull(...)) arm that is true for every row because the cast column is never NULL.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 74431a1. Configure here.

Comment thread snuba/protos/common.py
# ai_conversation_id when a TraceItem has no conversation_id. Because the column is never NULL,
# existence must be checked with notEmpty(...) instead of isNotNull(...), which would otherwise
# match every row (see get_field_existence_expression).
EMPTY_STRING_DEFAULT_COLUMNS: tuple[str, ...] = ("ai_conversation_id",)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The existence check for sentry.session_id incorrectly uses isNotNull. Since the column defaults to a zero-UUID instead of NULL, the check always returns true, matching all rows.
Severity: MEDIUM

Suggested Fix

The existence check for session_id should not use isNotNull. Instead, it should compare the column's value against the zero-UUID. A possible implementation is to generate a condition like notEquals(session_id, toUUID('00000000-0000-0000-0000-000000000000')). This requires modifying get_field_existence_expression to handle UUID columns with zero-UUID defaults specifically.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: snuba/protos/common.py#L198

Potential issue: The existence check for `sentry.session_id` is implemented using
`isNotNull(CAST(session_id, 'String'))`. However, the `session_id` column in the
database is a UUID type that defaults to the zero-UUID
(`'00000000-0000-0000-0000-000000000000'`) and is never NULL. Consequently, the
`isNotNull` check will always evaluate to true. This causes queries using
`exists(sentry.session_id)` to incorrectly return all rows, rather than only those with
a meaningful, non-default `session_id` value. The logic fails to account for the
zero-UUID default, unlike the handling for similar string columns.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment thread .github/workflows/ci.yml
Comment on lines +500 to +501
tests/sentry/api/endpoints/test_organization_ai_conversations.py \
tests/sentry/api/endpoints/test_organization_ai_conversation_details.py \

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

New change: Adding the 2 failing CI tests on our CI.

Comment on lines 116 to +120
return AnyValue(string_value=value)
if isinstance(value, int):
return AnyValue(int_value=value)
if isinstance(value, bool):
return AnyValue(bool_value=value)
if isinstance(value, int):
return AnyValue(int_value=value)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

new change. isintance(True, int) => True; so the bool case was never being exercised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant