Skip to content

feat(eap): Implement gen_ai attribute transformations#6201

Open
constantinius wants to merge 6 commits into
masterfrom
constantinius/feat/eap/gen-ai-attribute-transformations
Open

feat(eap): Implement gen_ai attribute transformations#6201
constantinius wants to merge 6 commits into
masterfrom
constantinius/feat/eap/gen-ai-attribute-transformations

Conversation

@constantinius

Copy link
Copy Markdown
Contributor

Summary

Implements the two gen_ai attribute transformations introduced in sentry-conventions PR #465:

  • gen_ai_request_messages_to_input_messages — transforms gen_ai.request.messages (with content field per message) into gen_ai.input.messages (with parts array)
  • gen_ai_response_to_output_messages — combines gen_ai.response.text and gen_ai.response.tool_calls into a single gen_ai.output.messages assistant message with typed parts

These go beyond simple key renaming — they reshape attribute values to match the new schema.

How it works

Attributes with _status: "transform" in sentry-conventions produce WriteBehavior::CurrentName, so normalize_attribute_names leaves them alone. The dedicated transformation code in the new gen_ai_transform module handles the full move-and-reshape, called from normalize_ai as part of AI span normalization (processing mode only).

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.

gen_ai.request.messagesgen_ai.input.messages

For each message object, the content field is converted to a parts array:

  • String content "hello"[{"type": "text", "content": "hello"}]
  • Array content (already parts-like) → kept, with text copied to content on each part if missing
  • Other content types (e.g., tool message objects) → left unchanged
  • Non-JSON values → moved as-is (key rename only)

gen_ai.response.text + gen_ai.response.tool_callsgen_ai.output.messages

gen_ai.response.text handles multiple formats:

  • Plain string, JSON string, JSON array of strings, JSON object with content, JSON array of objects with content

Tool calls get "type": "tool_call" added. Both are combined into one assistant message: [{"role": "assistant", "parts": [...]}].

Changes

  • relay-conventions/build/attributes.rs — add Transform variant to DeprecationStatus, producing WriteBehavior::CurrentName
  • relay-event-normalization/src/eap/gen_ai_transform.rs — new module with transformation logic
  • relay-event-normalization/src/eap/ai.rsnormalize_ai calls transform_gen_ai as its first step
  • relay-event-schema/src/protocol/span.rs — remove legacy aliases for the three deprecated attributes
  • relay-conventions/sentry-conventions — update submodule to PR fix(server): Allow multipart requests without trailing newline #465 branch

Tests

  • 21 Rust unit tests covering all input formats, edge cases, and idempotency
  • 4 Python integration tests covering both SpanV1 (transaction) and SpanV2 (direct ingestion) paths
  • Updated existing test_ai_spans_example_transaction expectations

Contributes to TET-2587

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
@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

TET-2587

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.
@constantinius constantinius requested a review from a team July 10, 2026 09:51
@constantinius constantinius marked this pull request as ready for review July 10, 2026 09:51
@constantinius constantinius requested a review from a team as a code owner July 10, 2026 09:51
Comment thread relay-event-normalization/src/eap/gen_ai_transform.rs
Comment thread relay-event-normalization/src/eap/gen_ai_transform.rs
Comment thread relay-event-normalization/src/eap/gen_ai_transform.rs
@constantinius constantinius changed the title feat(eap): implement gen_ai attribute transformations feat(eap): Implement gen_ai attribute transformations Jul 10, 2026
Comment thread relay-server/src/processing/legacy_spans/store.rs Outdated
Comment thread relay-event-normalization/src/eap/ai.rs Outdated
Comment on lines +119 to +124
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);
}

@Dav1dde Dav1dde Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 Getter just 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@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.

Comment thread relay-event-normalization/src/eap/ai.rs Outdated
Comment thread relay-event-normalization/src/eap/gen_ai_transform.rs
Comment thread relay-event-normalization/src/eap/gen_ai_transform.rs
- 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)

@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 1 potential issue.

Fix All in Cursor

❌ 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit aa94a95. Configure here.

Comment on lines 35 to 41
return;
}

gen_ai_transform::transform_gen_ai(attributes);
normalize_model(attributes);
normalize_ai_type(attributes);
normalize_total_tokens(attributes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

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.

2 participants