feat(eap): Implement gen_ai attribute transformations#6201
feat(eap): Implement gen_ai attribute transformations#6201constantinius wants to merge 6 commits into
Conversation
Implement the two attribute transformations introduced in sentry-conventions PR #465: `gen_ai_request_messages_to_input_messages` and `gen_ai_response_to_output_messages`. These go beyond simple key renaming — they reshape attribute values to match the new schema. `gen_ai.request.messages` with a `content` field on each message is converted to `gen_ai.input.messages` with a `parts` array. `gen_ai.response.text` (plain string, JSON string array, or objects with content) and `gen_ai.response.tool_calls` are combined into a single `gen_ai.output.messages` assistant message with typed parts. The transformations run inside `normalize_ai` as part of AI span normalization (processing mode only). Attributes with the new `"transform"` deprecation status produce `WriteBehavior::CurrentName` so `normalize_attribute_names` leaves them alone — the dedicated transformation code handles the full move-and-reshape. The `gen_ai.request.messages`, `gen_ai.response.text`, and `gen_ai.response.tool_calls` legacy aliases are removed from SpanV1 SpanData so these deprecated keys survive into SpanV2 attributes where the transformation can process them. Key changes: - Add `Transform` variant to `DeprecationStatus` in the build script, producing `WriteBehavior::CurrentName` instead of `NewName` - New `gen_ai_transform` module with the two transformation functions - `normalize_ai` calls `transform_gen_ai` as its first step - Remove legacy aliases for the three deprecated attributes from SpanData - Update sentry-conventions submodule to PR #465 branch - 21 unit tests, 4 new integration tests covering SpanV1 and SpanV2
Clippy lint fix for the Transform arm in format_write_behavior.
The legacy standalone spans and transaction extraction paths convert SpanV1 to SpanV2 but skip normalize_ai. With the legacy aliases removed, deprecated keys like gen_ai.response.text survive untransformed. Call transform_gen_ai directly after span_v1_to_span_v2 on both paths so the deprecated attributes get reshaped regardless of pipeline. Also fix clippy collapsible_if lints in gen_ai_transform.
When gen_ai.response.tool_calls is not valid JSON (e.g., "some_tool_calls"), the transformation cannot parse it, so the deprecated key stays as-is. Update the test expectation to match this behavior.
| let mut span_v2 = span.map_value(|span| relay_spans::span_v1_to_span_v2(span, true)); | ||
| if let Some(span_v2) = span_v2.value_mut() | ||
| && let Some(attributes) = span_v2.attributes.value_mut() | ||
| { | ||
| eap::transform_gen_ai(attributes); | ||
| } |
There was a problem hiding this comment.
Gonna have to talk to the team about this, I understand the desire of adding it in here. As it stands right now, the right approach is to implement the normalization independent of the span format and then add it to the proper normalization paths.
Solutions which come to mind:
- Add an abstraction (maybe something like
Getterjust attribute based) which can be backed by a V1 and V2 span - Introduce a proper "post transaction" normalization, and figure out what the implications of this are
There was a problem hiding this comment.
@constantinius will get back to you, this shouldn't be something you have to figure out and do, this is more a general platform problem.
- Always remove deprecated keys even when canonical key already exists or when no parts could be extracted from the input values - Remove transform_gen_ai calls from legacy_spans/store.rs and transactions/spans.rs — transformation only runs in the EAP pipeline via normalize_ai; other paths are a platform concern - Use module import instead of super:: reference in ai.rs - Narrow visibility of transform_gen_ai to pub(crate)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aa94a95. Configure here.
| attributes.remove(GEN_AI__RESPONSE__TOOL_CALLS); | ||
|
|
||
| if parts.is_empty() { | ||
| return; |
There was a problem hiding this comment.
Response attrs dropped when empty
High Severity
When gen_ai.response.text and/or gen_ai.response.tool_calls are present but parsing yields no output parts (e.g. non-JSON tool calls like some_tool_calls, or JSON text with no usable content), the code still removes both deprecated keys and writes nothing to gen_ai.output.messages, so response data is lost instead of being moved or preserved like invalid gen_ai.request.messages.
Reviewed by Cursor Bugbot for commit aa94a95. Configure here.
| return; | ||
| } | ||
|
|
||
| gen_ai_transform::transform_gen_ai(attributes); | ||
| normalize_model(attributes); | ||
| normalize_ai_type(attributes); | ||
| normalize_total_tokens(attributes); |
There was a problem hiding this comment.
Bug: Deprecated AI attributes are not transformed if the span lacks an operation attribute, as the normalize_ai function is skipped, leading to data loss.
Severity: LOW
Suggested Fix
The transformation logic should be moved to a place where it can handle these legacy attributes unconditionally, or the is_ai_item() guard should be updated to also check for the presence of deprecated AI attributes to ensure they are always processed.
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: relay-event-normalization/src/eap/ai.rs#L35-L41
Potential issue: The removal of `legacy_alias` for keys like `gen_ai.request.messages`
means they are no longer automatically remapped. The transformation now occurs in
`normalize_ai`, which is guarded by the `is_ai_item()` check. This check requires an
operation attribute like `gen_ai.operation.name` or a specific `sentry.op`. If a span
contains only deprecated AI attributes without these operation indicators,
`is_ai_item()` will return `false`, `normalize_ai` will be skipped, and the data under
the deprecated keys will be lost as it is never converted to its canonical form.


Summary
Implements the two gen_ai attribute transformations introduced in sentry-conventions PR #465:
gen_ai_request_messages_to_input_messages— transformsgen_ai.request.messages(withcontentfield per message) intogen_ai.input.messages(withpartsarray)gen_ai_response_to_output_messages— combinesgen_ai.response.textandgen_ai.response.tool_callsinto a singlegen_ai.output.messagesassistant message with typed partsThese go beyond simple key renaming — they reshape attribute values to match the new schema.
How it works
Attributes with
_status: "transform"in sentry-conventions produceWriteBehavior::CurrentName, sonormalize_attribute_namesleaves them alone. The dedicated transformation code in the newgen_ai_transformmodule handles the full move-and-reshape, called fromnormalize_aias part of AI span normalization (processing mode only).The
gen_ai.request.messages,gen_ai.response.text, andgen_ai.response.tool_callslegacy aliases are removed from SpanV1SpanDataso these deprecated keys survive into SpanV2 attributes where the transformation can process them.gen_ai.request.messages→gen_ai.input.messagesFor each message object, the
contentfield is converted to apartsarray:"hello"→[{"type": "text", "content": "hello"}]textcopied tocontenton each part if missinggen_ai.response.text+gen_ai.response.tool_calls→gen_ai.output.messagesgen_ai.response.texthandles multiple formats:content, JSON array of objects withcontentTool calls get
"type": "tool_call"added. Both are combined into one assistant message:[{"role": "assistant", "parts": [...]}].Changes
relay-conventions/build/attributes.rs— addTransformvariant toDeprecationStatus, producingWriteBehavior::CurrentNamerelay-event-normalization/src/eap/gen_ai_transform.rs— new module with transformation logicrelay-event-normalization/src/eap/ai.rs—normalize_aicallstransform_gen_aias its first steprelay-event-schema/src/protocol/span.rs— remove legacy aliases for the three deprecated attributesrelay-conventions/sentry-conventions— update submodule to PR fix(server): Allow multipart requests without trailing newline #465 branchTests
test_ai_spans_example_transactionexpectationsContributes to TET-2587