Skip to content

Conversation

@codyde
Copy link
Contributor

@codyde codyde commented Oct 1, 2025

Summary

Adds Sentry tracing instrumentation for the @anthropic-ai/claude-agent-sdk (Claude Code Agent SDK) following OpenTelemetry Semantic Conventions for Generative AI.

This integration enables AI monitoring for Claude Code agents with comprehensive telemetry:

  • Agent invocation spans (gen_ai.invoke_agent)
  • LLM chat spans (gen_ai.chat)
  • Tool execution spans (gen_ai.execute_tool)
  • Token usage tracking (including cache metrics)
  • Model info and session tracking
  • Optional input/output recording (respects sendDefaultPii)

Implementation

Uses automatic OpenTelemetry instrumentation via import-in-the-middle hooks - the same pattern as other AI integrations (Anthropic, OpenAI, Vercel AI, etc.). When the integration is added, it automatically patches the query function from @anthropic-ai/claude-agent-sdk.

Important: Sentry must be initialized before importing @anthropic-ai/claude-agent-sdk for auto-instrumentation to work.

Usage

// Initialize Sentry FIRST
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: 'your-dsn',
  integrations: [
    Sentry.claudeCodeAgentSdkIntegration({
      recordInputs: true,
      recordOutputs: true,
      agentName: 'my-coding-assistant', // optional
    }),
  ],
});

// THEN import the SDK - it will be automatically instrumented!
import { query } from '@anthropic-ai/claude-agent-sdk';

// Use query as normal - spans are created automatically
for await (const message of query({
  prompt: 'Hello',
  options: { model: 'claude-sonnet-4-20250514' },
})) {
  console.log(message);
}

Options

Option Type Default Description
recordInputs boolean sendDefaultPii Whether to record prompt messages
recordOutputs boolean sendDefaultPii Whether to record response text, tool calls, and outputs
agentName string 'claude-code' Custom agent name for span identification

Captured Telemetry

Span Hierarchy

invoke_agent claude-code
├── chat claude-sonnet-4-20250514
│   └── execute_tool Read
│   └── execute_tool Bash
├── chat claude-sonnet-4-20250514
│   └── execute_tool WebSearch
└── ...

Attributes (OpenTelemetry GenAI Semantic Conventions)

  • gen_ai.system: anthropic
  • gen_ai.operation.name: invoke_agent | chat | execute_tool
  • gen_ai.agent.name: Custom or claude-code
  • gen_ai.request.model: Model identifier
  • gen_ai.request.available_tools: Available tools from system init
  • gen_ai.response.id: Response/session ID
  • gen_ai.response.model: Actual model used
  • gen_ai.response.finish_reasons: Stop reason
  • gen_ai.response.text: Response text (when recordOutputs: true)
  • gen_ai.response.tool_calls: Tool calls made (when recordOutputs: true)
  • gen_ai.tool.name: Tool name (e.g., Read, Bash, WebSearch)
  • gen_ai.tool.type: function | extension | datastore
  • gen_ai.tool.input: Tool input (when recordInputs: true)
  • gen_ai.tool.output: Tool output (when recordOutputs: true)
  • gen_ai.usage.input_tokens: Input token count
  • gen_ai.usage.output_tokens: Output token count
  • gen_ai.usage.cache_creation_input_tokens: Cache creation tokens
  • gen_ai.usage.cache_read_input_tokens: Cache read tokens

Tool Type Classification

  • Function tools (client-side execution): Bash, Read, Write, Edit, Glob, Grep, Task, TodoWrite, NotebookEdit, SlashCommand, AskUserQuestion, Skill, etc.
  • Extension tools (external APIs): WebSearch, WebFetch, ListMcpResources, ReadMcpResource

Manual Instrumentation

For advanced use cases where auto-instrumentation is not suitable, patchClaudeCodeQuery is exported for manual patching:

import { patchClaudeCodeQuery } from '@sentry/node';
import { query } from '@anthropic-ai/claude-agent-sdk';

const instrumentedQuery = patchClaudeCodeQuery(query, {
  recordInputs: true,
  recordOutputs: true,
  agentName: 'my-agent',
});

Testing

Includes comprehensive integration tests covering:

  • Basic agent invocation with default PII settings
  • Input/output recording with sendDefaultPii: true
  • Custom recordInputs/recordOutputs options
  • Tool execution spans (function and extension types)
  • Error handling and span status

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@codyde codyde marked this pull request as draft October 1, 2025 23:17
@github-actions
Copy link
Contributor

github-actions bot commented Oct 1, 2025

size-limit report 📦

Path Size % Change Change
@sentry/browser 24.82 kB - -
@sentry/browser - with treeshaking flags 23.32 kB - -
@sentry/browser (incl. Tracing) 41.6 kB - -
@sentry/browser (incl. Tracing, Profiling) 46.19 kB - -
@sentry/browser (incl. Tracing, Replay) 80.17 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 69.91 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 84.85 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 97.1 kB - -
@sentry/browser (incl. Feedback) 41.54 kB - -
@sentry/browser (incl. sendFeedback) 29.51 kB - -
@sentry/browser (incl. FeedbackAsync) 34.5 kB - -
@sentry/browser (incl. Metrics) 25.85 kB - -
@sentry/browser (incl. Logs) 26.07 kB - -
@sentry/browser (incl. Metrics & Logs) 26.73 kB - -
@sentry/react 26.54 kB - -
@sentry/react (incl. Tracing) 43.79 kB - -
@sentry/vue 29.29 kB - -
@sentry/vue (incl. Tracing) 43.41 kB - -
@sentry/svelte 24.84 kB - -
CDN Bundle 27.25 kB - -
CDN Bundle (incl. Tracing) 42.24 kB - -
CDN Bundle (incl. Tracing, Replay) 78.96 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 84.41 kB - -
CDN Bundle - uncompressed 80.06 kB - -
CDN Bundle (incl. Tracing) - uncompressed 125.49 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 242.03 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 254.79 kB - -
@sentry/nextjs (client) 46.02 kB - -
@sentry/sveltekit (client) 41.97 kB - -
@sentry/node-core 51.62 kB - -
⛔️ @sentry/node (max: 162 kB) 162.87 kB +0.84% +1.34 kB 🔺
@sentry/node - without tracing 93.05 kB -0.01% -1 B 🔽
@sentry/aws-serverless 108.57 kB - -

View base workflow run

@github-actions
Copy link
Contributor

github-actions bot commented Oct 1, 2025

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,704 - 11,145 -22%
GET With Sentry 1,755 20% 1,889 -7%
GET With Sentry (error only) 6,067 70% 7,659 -21%
POST Baseline 1,199 - 1,171 +2%
POST With Sentry 596 50% 582 +2%
POST With Sentry (error only) 1,060 88% 1,039 +2%
MYSQL Baseline 3,297 - 4,024 -18%
MYSQL With Sentry 465 14% 600 -23%
MYSQL With Sentry (error only) 2,702 82% 3,153 -14%

View base workflow run

@codyde codyde changed the title feat(node): Add Claude Code Agent SDK instrumentation feat(agent-monitoring): Add Claude Code Agent SDK instrumentation Oct 2, 2025
@codyde codyde changed the title feat(agent-monitoring): Add Claude Code Agent SDK instrumentation feat(javascript): Add Claude Code Agent SDK instrumentation Oct 2, 2025
@RulaKhaled RulaKhaled assigned RulaKhaled and unassigned RulaKhaled Oct 6, 2025
@RulaKhaled RulaKhaled self-requested a review October 6, 2025 08:17

type ClaudeCodeInstrumentationOptions = ClaudeCodeOptions;

const GEN_AI_ATTRIBUTES = {
Copy link
Member

Choose a reason for hiding this comment

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

We already have these attributes in packages/core/src/utils/ai/gen-ai-attributes.ts


const SENTRY_ORIGIN = 'auto.ai.claude-code';

function setTokenUsageAttributes(
Copy link
Member

Choose a reason for hiding this comment

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

can you reuse function from packages/core/src/utils/ai/utils.ts?

// Parse query arguments
const [queryParams] = args as [Record<string, unknown>];
const { options: queryOptions, inputMessages } = queryParams || {};
const model = (queryOptions as Record<string, unknown>)?.model ?? 'sonnet';
Copy link
Member

Choose a reason for hiding this comment

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

can we revert to unknown if not model found here? i think this might be confusing if it's not accurate

name: CLAUDE_CODE_INTEGRATION_NAME,
options,
setupOnce() {
// Note: Automatic patching via require hooks doesn't work for ESM modules
Copy link
Member

Choose a reason for hiding this comment

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

actually i believe InstrumentationModuleDefinition will automatically patch Node.js modules when they're loaded via import, you can find some patterns in other AI integrations e.g anthropic AI

* Patches the Claude Code SDK query function with Sentry instrumentation.
* This function can be called directly to patch an imported query function.
*/
export function patchClaudeCodeQuery(
Copy link
Member

Choose a reason for hiding this comment

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

We should hook into the query and use proxy here

);

// Preserve Query interface methods
if (typeof (originalQueryInstance as Record<string, unknown>).interrupt === 'function') {
Copy link
Member

Choose a reason for hiding this comment

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

using proxy should clean this up a little

Copy link
Member

@RulaKhaled RulaKhaled left a comment

Choose a reason for hiding this comment

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

Thanks for working on this! For the first pass, the biggest lift here is to try to auto patch the functions we need automatically instead of asking user to import patched method, then we can move to tackling the other TODOs you have

@codyde
Copy link
Contributor Author

codyde commented Oct 6, 2025

Thanks for working on this! For the first pass, the biggest lift here is to try to auto patch the functions we need automatically instead of asking user to import patched method, then we can move to tackling the other TODOs you have

Thanks SO much for all these. I'll get started on them.

I tried REALLY hard to figure out how to hook into the existing query, and I couldn't get it to work no matter what I tried. I'll chat with you in slack on it, but I'd love some advice / guidance. I tried a bunch of different angles - but each time I ran into effectively timing issues where we couldn't hook fast enough. Felt like a limitation on how Claude Code's SDK works - but could be a total skill issue on my side.

@RulaKhaled
Copy link
Member

Thanks for working on this! For the first pass, the biggest lift here is to try to auto patch the functions we need automatically instead of asking user to import patched method, then we can move to tackling the other TODOs you have

Thanks SO much for all these. I'll get started on them.

I tried REALLY hard to figure out how to hook into the existing query, and I couldn't get it to work no matter what I tried. I'll chat with you in slack on it, but I'd love some advice / guidance. I tried a bunch of different angles - but each time I ran into effectively timing issues where we couldn't hook fast enough. Felt like a limitation on how Claude Code's SDK works - but could be a total skill issue on my side.

Hello @codyde, are you still working on this? if not, let's close this, we're trying to clean up the stale PRs

@codyde
Copy link
Contributor Author

codyde commented Oct 20, 2025

Thanks for working on this! For the first pass, the biggest lift here is to try to auto patch the functions we need automatically instead of asking user to import patched method, then we can move to tackling the other TODOs you have

Thanks SO much for all these. I'll get started on them.
I tried REALLY hard to figure out how to hook into the existing query, and I couldn't get it to work no matter what I tried. I'll chat with you in slack on it, but I'd love some advice / guidance. I tried a bunch of different angles - but each time I ran into effectively timing issues where we couldn't hook fast enough. Felt like a limitation on how Claude Code's SDK works - but could be a total skill issue on my side.

Hello @codyde, are you still working on this? if not, let's close this, we're trying to clean up the stale PRs

I definitely am! I pushed up a few more commits today that included fixes for some of the other items you mentioned - but im struggling to get through this proxy one. I might need some pairing time to take a look at it together since im less familiar with the functionality.

@codyde codyde force-pushed the claude-code-agent-instrumentation branch from 4df75cc to 6e267e0 Compare November 26, 2025 07:42
codyde and others added 11 commits December 27, 2025 22:14
Adds Sentry tracing instrumentation for the @anthropic-ai/claude-agent-sdk
following OpenTelemetry Semantic Conventions for Generative AI.

Key features:
- Captures agent invocation, LLM chat, and tool execution spans
- Records token usage, model info, and session tracking
- Supports input/output recording based on sendDefaultPii setting
- Provides createInstrumentedClaudeQuery() helper for clean DX

Due to ESM-only module constraints, this integration uses a helper function
pattern instead of automatic OpenTelemetry instrumentation hooks.

Usage:
```typescript
import { createInstrumentedClaudeQuery } from '@sentry/node';
const query = createInstrumentedClaudeQuery();
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…g in Claude Code integration

- Add SEMANTIC_ATTRIBUTE_SENTRY_OP to all span creation calls (invoke_agent, chat, execute_tool)
- Capture exceptions to Sentry in catch block with proper mechanism metadata
- Ensure child spans (currentLLMSpan, previousLLMSpan) are always closed in finally block
- Prevents incomplete traces if generator exits early
…umentation

- Add OpenTelemetry-based automatic instrumentation via SentryClaudeCodeAgentSdkInstrumentation
- Extract ClaudeCodeOptions to dedicated types.ts file
- Remove backwards compatibility exports (patchClaudeCodeQuery, createInstrumentedClaudeQuery)
- Rename integration to claudeCodeAgentSdkIntegration
- Register instrumentation in OTEL preload for automatic patching
- Update NextJS re-exports to match simplified API

Users now only need:
```typescript
Sentry.init({ integrations: [Sentry.claudeCodeAgentSdkIntegration()] });
import { query } from '@anthropic-ai/claude-agent-sdk'; // Auto-instrumented
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ec compliance

- Fix GEN_AI_SYSTEM_ATTRIBUTE to use 'anthropic' per OpenTelemetry semantic conventions
- Add GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE for capturing available tools from system init
- Add GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE for tracking stop_reason
- Use getTruncatedJsonString for proper payload truncation in span attributes
- Expand tool categorization with new tools (KillBash, EnterPlanMode, AskUserQuestion, Skill, MCP tools)
- Add better error metadata with function name in mechanism data
- Export patchClaudeCodeQuery for manual instrumentation use cases
- Add comprehensive integration tests for Claude Code Agent SDK instrumentation
@codyde codyde force-pushed the claude-code-agent-instrumentation branch from 6e267e0 to 3f69bfd Compare December 28, 2025 06:16
@codyde codyde marked this pull request as ready for review December 28, 2025 06:18

async *_createGenerator(params) {
const model = params.options?.model || 'claude-sonnet-4-20250514';
const sessionId = `sess_${Math.random().toString(36).substr(2, 9)}`;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 1 day ago

General fix: Replace use of Math.random() for generating identifiers with a cryptographically secure generator, such as crypto.randomBytes in Node.js. Convert securely generated bytes into a URL-safe or base36-like string for use in the sessionId, keeping the format and length similar to the original where practical.

Best concrete fix here: In _createGenerator, change the sessionId definition on line 43 to use crypto.randomBytes(6).toString('base64url') (or similar) instead of Math.random().... Add an import of Node’s built-in crypto module at the top of this file. This keeps the rest of the logic intact: sessionId is still a short string with an alphanumeric-ish prefix sess_, just the random part is generated securely.

Specific changes:

  • In dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs, add import crypto from 'crypto'; at the top.
  • Replace line 43’s sessionId assignment:
    • From: const sessionId = \sess_${Math.random().toString(36).substr(2, 9)}`;`
    • To: const sessionId = \sess_${crypto.randomBytes(6).toString('base64url')}`;`
  • No other code paths need adjustment because all uses of sessionId treat it as an opaque string.
Suggested changeset 1
dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs b/dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs
--- a/dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs
+++ b/dev-packages/node-integration-tests/suites/tracing/claude-code/mock-server.mjs
@@ -1,4 +1,5 @@
 /* eslint-disable no-console, max-lines */
+import crypto from 'crypto';
 /**
  * Mock implementation of @anthropic-ai/claude-agent-sdk
  * Simulates the query function behavior for testing
@@ -40,7 +41,7 @@
 
   async *_createGenerator(params) {
     const model = params.options?.model || 'claude-sonnet-4-20250514';
-    const sessionId = `sess_${Math.random().toString(36).substr(2, 9)}`;
+    const sessionId = `sess_${crypto.randomBytes(6).toString('base64url')}`;
     const scenarioName = params.options?.scenario || 'basic';
 
     // Get scenario or use default
EOF
@@ -1,4 +1,5 @@
/* eslint-disable no-console, max-lines */
import crypto from 'crypto';
/**
* Mock implementation of @anthropic-ai/claude-agent-sdk
* Simulates the query function behavior for testing
@@ -40,7 +41,7 @@

async *_createGenerator(params) {
const model = params.options?.model || 'claude-sonnet-4-20250514';
const sessionId = `sess_${Math.random().toString(36).substr(2, 9)}`;
const sessionId = `sess_${crypto.randomBytes(6).toString('base64url')}`;
const scenarioName = params.options?.scenario || 'basic';

// Get scenario or use default
Copilot is powered by AI and may make mistakes. Always verify output.
@codyde codyde changed the title feat(javascript): Add Claude Code Agent SDK instrumentation feat(node): Add Claude Code Agent SDK instrumentation Dec 28, 2025

export type ClaudeCodeInstrumentationOptions = ClaudeCodeOptions;

const SENTRY_ORIGIN = 'auto.ai.claude-code';
Copy link

Choose a reason for hiding this comment

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

Invalid hyphen in span origin violates trace specification (Bugbot Rules)

The SENTRY_ORIGIN constant is set to 'auto.ai.claude-code' which contains a hyphen character. According to the rules file, a proper origin must only contain [a-z], [A-Z], [0-9], _ and . characters. The hyphen violates this specification. This value is used for both the SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN span attribute and the mechanism.type in captureException. Looking at other AI integrations, the correct format would be 'auto.ai.claude_code' using an underscore instead. See the trace origin specification: https://develop.sentry.dev/sdk/telemetry/traces/trace-origin/

Additional Locations (1)

Fix in Cursor Fix in Web

const query1 = mockSdk.query({
prompt: 'Read the file',
options: { model: 'claude-sonnet-4-20250514', scenario: 'withTools' },
});
Copy link

Choose a reason for hiding this comment

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

Test scenarios missing instrumentation patching step (Bugbot Rules)

The test scenarios scenario-tools.mjs and scenario-errors.mjs call mockSdk.query() directly without patching it with patchClaudeCodeQuery. The working scenarios (scenario.mjs and scenario-simple.mjs) properly call patchClaudeCodeQuery(originalQuery, ...) before using the query function. Without patching, no instrumentation runs, so no spans or error events are created. The tests in test.ts that rely on these scenarios (tool execution tests and error handling tests) won't actually test the instrumentation's behavior. This violates the review rule: "Check that tests actually test the newly added behaviour."

Additional Locations (1)

Fix in Cursor Fix in Web

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.

3 participants