diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 552fefe..e723663 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ { "name": "claude-code-ai-observability", "description": "Realtime Sentry traces with per-turn tokens, cost, subagent and error instrumentation, and rich auto-tagging (session / git / host). Forked from sergical/claude-code-sentry-monitor (MIT).", - "version": "0.2.8", + "version": "0.2.9", "author": { "name": "Joshkop" }, diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5af903e..b58829a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "claude-code-ai-observability", "description": "Comprehensive AI Agent Observability plugin for Claude Code — realtime Sentry traces with per-turn tokens, cost, subagent and error instrumentation, and rich auto-tagging (session / git / host). Forked from sergical/claude-code-sentry-monitor (MIT).", - "version": "0.2.8", + "version": "0.2.9", "author": { "name": "Joshkop" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 17652d2..ce2d49e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.2.9] - 2026-05-21 + +### Fixed + +- **Sentry AI Conversations view actually populates now (fourth-and-final).** 0.2.8 mirrored the prompt onto the chat child as `gen_ai.request.messages` plus `gen_ai.response.text`, but the view stayed empty because `@sentry/node` 10.53.1 follows the current OTel gen_ai semantic convention which renamed those attributes. The SDK silently dropped the legacy keys and initialized `gen_ai.input.messages` to an empty array — visible directly in the span detail panel as `gen_ai.input.messages: []`. Switched all input/output message attributes to the new names: + - `gen_ai.request.messages` → `gen_ai.input.messages` (on both the `gen_ai.invoke_agent` agent span and the `gen_ai.chat` child) + - `gen_ai.response.text` → `gen_ai.output.messages` on the chat child, now wrapped as `[{role: "assistant", content: response}]` to match the array shape Sentry expects + - Same rename applied to the synthesized subagent invoke_agent span (`src/subagent.ts`) + ## [0.2.8] - 2026-05-21 ### Fixed diff --git a/package.json b/package.json index e12f36a..23594f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-ai-observability", - "version": "0.2.8", + "version": "0.2.9", "description": "Comprehensive AI Agent Observability plugin for Claude Code — realtime Sentry traces with per-turn tokens, cost, subagent and error instrumentation, and rich auto-tagging (session / git / host).", "type": "module", "scripts": { diff --git a/scripts/spans.js b/scripts/spans.js index 009c195..201ec43 100644 --- a/scripts/spans.js +++ b/scripts/spans.js @@ -38,7 +38,7 @@ export function openTurnTransaction(sentry, sessionId, turnIndex, prompt, tags, })); if (config.recordInputs && prompt) { const messages = serialize([{ role: "user", content: prompt }], config.maxAttributeLength); - agent.setAttribute("gen_ai.request.messages", messages); + agent.setAttribute("gen_ai.input.messages", messages); } return { root, agent }; } @@ -85,10 +85,10 @@ export function closeTurnSpan(sentry, turnSpans, input, config, endTime) { } } if (config.recordInputs && prompt) { - chatSpan.setAttribute("gen_ai.request.messages", serialize([{ role: "user", content: prompt }], config.maxAttributeLength)); + chatSpan.setAttribute("gen_ai.input.messages", serialize([{ role: "user", content: prompt }], config.maxAttributeLength)); } if (config.recordOutputs && response) { - chatSpan.setAttribute("gen_ai.response.text", serialize(response, config.maxAttributeLength)); + chatSpan.setAttribute("gen_ai.output.messages", serialize([{ role: "assistant", content: response }], config.maxAttributeLength)); } if (tokenExtractionStatus) { chatSpan.setAttribute("claude_code.token_extraction.status", tokenExtractionStatus); diff --git a/scripts/subagent.js b/scripts/subagent.js index cd42e61..b369ef6 100644 --- a/scripts/subagent.js +++ b/scripts/subagent.js @@ -47,7 +47,7 @@ export function createSubagentSpan(sentry, event, options = {}) { if (description) attributes["gen_ai.agent.description"] = scrubString(truncate(description, maxAttrLen)); if (prompt) - attributes["gen_ai.request.messages"] = scrubString(truncate(prompt, maxAttrLen)); + attributes["gen_ai.input.messages"] = scrubString(truncate(prompt, maxAttrLen)); if (event.tool_use_id) attributes["gen_ai.tool.call.id"] = event.tool_use_id; // Dual-namespaced on purpose: gen_ai.agent.* for generic OTel AI tooling, diff --git a/src/spans.ts b/src/spans.ts index e469bbe..9fd134b 100644 --- a/src/spans.ts +++ b/src/spans.ts @@ -71,7 +71,7 @@ export function openTurnTransaction( [{ role: "user", content: prompt }], config.maxAttributeLength, ); - agent.setAttribute("gen_ai.request.messages", messages); + agent.setAttribute("gen_ai.input.messages", messages); } return { root, agent }; } @@ -165,7 +165,7 @@ export function closeTurnSpan( } if (config.recordInputs && prompt) { chatSpan.setAttribute( - "gen_ai.request.messages", + "gen_ai.input.messages", serialize( [{ role: "user", content: prompt }], config.maxAttributeLength, @@ -174,8 +174,11 @@ export function closeTurnSpan( } if (config.recordOutputs && response) { chatSpan.setAttribute( - "gen_ai.response.text", - serialize(response, config.maxAttributeLength), + "gen_ai.output.messages", + serialize( + [{ role: "assistant", content: response }], + config.maxAttributeLength, + ), ); } if (tokenExtractionStatus) { diff --git a/src/subagent.ts b/src/subagent.ts index 398ce53..a342306 100644 --- a/src/subagent.ts +++ b/src/subagent.ts @@ -90,7 +90,7 @@ export function createSubagentSpan( }; if (subagentType) attributes["gen_ai.agent.name"] = subagentType; if (description) attributes["gen_ai.agent.description"] = scrubString(truncate(description, maxAttrLen)); - if (prompt) attributes["gen_ai.request.messages"] = scrubString(truncate(prompt, maxAttrLen)); + if (prompt) attributes["gen_ai.input.messages"] = scrubString(truncate(prompt, maxAttrLen)); if (event.tool_use_id) attributes["gen_ai.tool.call.id"] = event.tool_use_id; // Dual-namespaced on purpose: gen_ai.agent.* for generic OTel AI tooling, diff --git a/tests/spans.test.ts b/tests/spans.test.ts index 539c6af..3a8dce9 100644 --- a/tests/spans.test.ts +++ b/tests/spans.test.ts @@ -130,19 +130,19 @@ describe("openTurnTransaction attribute contract", () => { expect(root.attrs["process.pid"]).toBe(1234); }); - it("attaches gen_ai.request.messages to the agent span when recordInputs is true and prompt provided", () => { + it("attaches gen_ai.input.messages to the agent span when recordInputs is true and prompt provided", () => { const sentry = makeFakeSentry(); const cfg: ResolvedPluginConfig = { ...baseConfig, recordInputs: true }; const turn = openTurnTransaction(sentry as never, "sess-1", 0, "hello world", baseTags, cfg); const agent = turn.agent as unknown as ReturnType; - expect(agent.attrs["gen_ai.request.messages"]).toBeDefined(); + expect(agent.attrs["gen_ai.input.messages"]).toBeDefined(); }); - it("does not attach gen_ai.request.messages when recordInputs is false", () => { + it("does not attach gen_ai.input.messages when recordInputs is false", () => { const sentry = makeFakeSentry(); const turn = openTurnTransaction(sentry as never, "sess-1", 0, "hello world", baseTags, baseConfig); const agent = turn.agent as unknown as ReturnType; - expect(agent.attrs["gen_ai.request.messages"]).toBeUndefined(); + expect(agent.attrs["gen_ai.input.messages"]).toBeUndefined(); }); }); @@ -329,7 +329,7 @@ describe("closeTurnSpan attribute contract", () => { expect(chat!.attrs["claude_code.token_extraction.status"]).toBe("ok|matched_after_retry"); }); - it("mirrors gen_ai.request.messages onto the chat child when prompt + recordInputs (Sentry Conversations base filter requires input AND output on the same span)", () => { + it("mirrors gen_ai.input.messages onto the chat child when prompt + recordInputs (Sentry Conversations base filter requires input AND output on the same span)", () => { const sentry = makeFakeSentry(); const cfg: ResolvedPluginConfig = { ...baseConfig, recordInputs: true, recordOutputs: true }; const turn = openTurnTransaction(sentry as never, "sess-conv", 0, "hello world", baseTags, cfg); @@ -341,12 +341,12 @@ describe("closeTurnSpan attribute contract", () => { }, cfg); const chat = sentry.spans.find(s => s.attrs["gen_ai.operation.name"] === "chat"); expect(chat).toBeDefined(); - expect(chat!.attrs["gen_ai.request.messages"]).toBeDefined(); - expect(chat!.attrs["gen_ai.response.text"]).toBeDefined(); + expect(chat!.attrs["gen_ai.input.messages"]).toBeDefined(); + expect(chat!.attrs["gen_ai.output.messages"]).toBeDefined(); expect(chat!.attrs["gen_ai.conversation.id"]).toBe("sess-conv"); }); - it("omits gen_ai.request.messages on the chat child when recordInputs is false", () => { + it("omits gen_ai.input.messages on the chat child when recordInputs is false", () => { const sentry = makeFakeSentry(); const turn = openTurnTransaction(sentry as never, "sess-1", 0, null, baseTags, baseConfig); closeTurnSpan(sentry as never, turn as never, { @@ -355,7 +355,7 @@ describe("closeTurnSpan attribute contract", () => { prompt: "hello world", }, baseConfig); const chat = sentry.spans.find(s => s.attrs["gen_ai.operation.name"] === "chat"); - expect(chat!.attrs["gen_ai.request.messages"]).toBeUndefined(); + expect(chat!.attrs["gen_ai.input.messages"]).toBeUndefined(); }); it("omits claude_code.token_extraction.status when undefined", () => {