Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions scripts/spans.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion scripts/subagent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions src/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 9 additions & 9 deletions tests/spans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof makeFakeSpan>;
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<typeof makeFakeSpan>;
expect(agent.attrs["gen_ai.request.messages"]).toBeUndefined();
expect(agent.attrs["gen_ai.input.messages"]).toBeUndefined();
});
});

Expand Down Expand Up @@ -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);
Expand All @@ -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, {
Expand All @@ -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", () => {
Expand Down
Loading