Skip to content
5 changes: 5 additions & 0 deletions packages/nextjs/src/index.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export declare const contextLinesIntegration: typeof clientSdk.contextLinesInteg
// Different implementation in server and worker
export declare const vercelAIIntegration: typeof serverSdk.vercelAIIntegration;

// Claude Code integration (server-only)
export declare const claudeCodeIntegration: typeof serverSdk.claudeCodeIntegration;
export declare const createInstrumentedClaudeQuery: typeof serverSdk.createInstrumentedClaudeQuery;
export declare const patchClaudeCodeQuery: typeof serverSdk.patchClaudeCodeQuery;

export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;

Expand Down
17 changes: 16 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ import {
stripUrlQueryAndFragment,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
claudeCodeIntegration,
createInstrumentedClaudeQuery,
patchClaudeCodeQuery,
} from '@sentry/node';
import { getScopesFromContext } from '@sentry/opentelemetry';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
Expand All @@ -41,6 +48,14 @@ import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegrati

export * from '@sentry/node';

// Explicit re-exports for Claude Code integration
// We re-export these explicitly to ensure rollup doesn't tree-shake them
export { claudeCodeIntegration, createInstrumentedClaudeQuery, patchClaudeCodeQuery };

// Force rollup to keep the imports by "using" them
const _forceInclude = { claudeCodeIntegration, createInstrumentedClaudeQuery, patchClaudeCodeQuery };
if (false as boolean) { console.log(_forceInclude); }

export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
Expand Down
2 changes: 2 additions & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export { amqplibIntegration } from './integrations/tracing/amqplib';
export { vercelAIIntegration } from './integrations/tracing/vercelai';
export { openAIIntegration } from './integrations/tracing/openai';
export { anthropicAIIntegration } from './integrations/tracing/anthropic-ai';
export { claudeCodeIntegration, patchClaudeCodeQuery } from './integrations/tracing/claude-code';
export { createInstrumentedClaudeQuery } from './integrations/tracing/claude-code/helpers';
export { googleGenAIIntegration } from './integrations/tracing/google-genai';
export {
launchDarklyIntegration,
Expand Down
117 changes: 117 additions & 0 deletions packages/node/src/integrations/tracing/claude-code/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { getClient } from '@sentry/core';
import { patchClaudeCodeQuery } from './instrumentation';
import type { ClaudeCodeOptions } from './index';

const CLAUDE_CODE_INTEGRATION_NAME = 'ClaudeCode';

// Global singleton - only patch once per application instance
let _globalPatchedQuery: ((...args: unknown[]) => AsyncGenerator<unknown, void, unknown>) | null = null;
let _initPromise: Promise<void> | null = null;

/**
* Lazily loads and patches the Claude Code SDK.
* Ensures only one patched instance exists globally.
*/
async function ensurePatchedQuery(): Promise<void> {
if (_globalPatchedQuery) {
return;
}

if (_initPromise) {
return _initPromise;
}

_initPromise = (async () => {
try {
// Use webpackIgnore to prevent webpack from trying to resolve this at build time
// The import resolves at runtime from the user's node_modules
const sdkPath = '@anthropic-ai/claude-agent-sdk';
const claudeSDK = await import(/* webpackIgnore: true */ sdkPath);

if (!claudeSDK || typeof claudeSDK.query !== 'function') {
throw new Error(
`Failed to find 'query' function in @anthropic-ai/claude-agent-sdk.\n` +
`Make sure you have version >=0.1.0 installed.`,
);
}

const client = getClient();
const integration = client?.getIntegrationByName(CLAUDE_CODE_INTEGRATION_NAME);
const options = (integration as any)?.options as ClaudeCodeOptions | undefined || {};

_globalPatchedQuery = patchClaudeCodeQuery(claudeSDK.query, options);
} catch (error) {
// Reset state on failure to allow retry on next call
_initPromise = null;

const errorMessage =
error instanceof Error
? error.message
: 'Unknown error occurred while loading @anthropic-ai/claude-agent-sdk';

throw new Error(
`Failed to instrument Claude Code SDK:\n${errorMessage}\n\n` +
`Make sure @anthropic-ai/claude-agent-sdk is installed:\n` +
` npm install @anthropic-ai/claude-agent-sdk\n` +
` # or\n` +
` yarn add @anthropic-ai/claude-agent-sdk`,
);
}
})();

return _initPromise;
}

/**
* Creates a Sentry-instrumented query function for the Claude Code SDK.
*
* This is a convenience helper that reduces boilerplate to a single line.
* The SDK is lazily loaded on first query call, and the patched version is cached globally.
*
* **Important**: This helper is NOT automatic. You must call it in your code.
* The Claude Code SDK cannot be automatically instrumented due to ESM module
* and webpack bundling limitations.
*
* @returns An instrumented query function ready to use
*
* @example
* ```typescript
* import { createInstrumentedClaudeQuery } from '@sentry/node';
* import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
*
* const query = createInstrumentedClaudeQuery();
*
* // Use as normal - automatically instrumented!
* for await (const message of query({
* prompt: 'Hello',
* options: { model: 'claude-sonnet-4-5' }
* })) {
* console.log(message);
* }
* ```
*
* Configuration is automatically pulled from your `claudeCodeIntegration()` setup:
*
* @example
* ```typescript
* Sentry.init({
* integrations: [
* Sentry.claudeCodeIntegration({
* recordInputs: true, // These options are used
* recordOutputs: true, // by createInstrumentedClaudeQuery()
* })
* ]
* });
* ```
*/
export function createInstrumentedClaudeQuery(): (...args: unknown[]) => AsyncGenerator<unknown, void, unknown> {
return async function* query(...args: unknown[]): AsyncGenerator<unknown, void, unknown> {
await ensurePatchedQuery();

if (!_globalPatchedQuery) {
throw new Error('[Sentry] Failed to initialize instrumented Claude Code query function');
}

yield* _globalPatchedQuery(...args);
};
}
130 changes: 130 additions & 0 deletions packages/node/src/integrations/tracing/claude-code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { patchClaudeCodeQuery } from './instrumentation';

export interface ClaudeCodeOptions {
/**
* Whether to record prompt messages.
* Defaults to Sentry client's `sendDefaultPii` setting.
*/
recordInputs?: boolean;

/**
* Whether to record response text, tool calls, and tool outputs.
* Defaults to Sentry client's `sendDefaultPii` setting.
*/
recordOutputs?: boolean;
}

const CLAUDE_CODE_INTEGRATION_NAME = 'ClaudeCode';

const _claudeCodeIntegration = ((options: ClaudeCodeOptions = {}) => {
return {
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

// or webpack-bundled dependencies. Users must manually patch using patchClaudeCodeQuery()
// in their route files.
},
};
}) satisfies IntegrationFn;

/**
* Adds Sentry tracing instrumentation for the Claude Code SDK.
*
* **Important**: Due to ESM module and bundler limitations, this integration requires
* using the `createInstrumentedClaudeQuery()` helper function in your code.
* See the example below for proper usage.
*
* This integration captures telemetry data following OpenTelemetry Semantic Conventions
* for Generative AI, including:
* - Agent invocation spans (`invoke_agent`)
* - LLM chat spans (`chat`)
* - Tool execution spans (`execute_tool`)
* - Token usage, model info, and session tracking
*
* @example
* ```typescript
* // Step 1: Configure the integration
* import * as Sentry from '@sentry/node';
*
* Sentry.init({
* dsn: 'your-dsn',
* integrations: [
* Sentry.claudeCodeIntegration({
* recordInputs: true,
* recordOutputs: true
* })
* ],
* });
*
* // Step 2: Use the helper in your routes
* import { createInstrumentedClaudeQuery } from '@sentry/node';
*
* const query = createInstrumentedClaudeQuery();
*
* // Use query as normal - automatically instrumented!
* for await (const message of query({
* prompt: 'Hello',
* options: { model: 'claude-sonnet-4-5' }
* })) {
* console.log(message);
* }
* ```
*
* ## Options
*
* - `recordInputs`: Whether to record prompt messages (default: respects `sendDefaultPii` client option)
* - `recordOutputs`: Whether to record response text, tool calls, and outputs (default: respects `sendDefaultPii` client option)
*
* ### Default Behavior
*
* By default, the integration will:
* - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options
* - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled
*
* @example
* ```typescript
* // Record inputs and outputs when sendDefaultPii is false
* Sentry.init({
* integrations: [
* Sentry.claudeCodeIntegration({
* recordInputs: true,
* recordOutputs: true
* })
* ],
* });
*
* // Never record inputs/outputs regardless of sendDefaultPii
* Sentry.init({
* sendDefaultPii: true,
* integrations: [
* Sentry.claudeCodeIntegration({
* recordInputs: false,
* recordOutputs: false
* })
* ],
* });
* ```
*
* @see https://docs.sentry.io/platforms/javascript/guides/node/ai-monitoring/
*/
export const claudeCodeIntegration = defineIntegration(_claudeCodeIntegration);

/**
* Manually patch the Claude Code SDK query function with Sentry instrumentation.
*
* **Note**: Most users should use `createInstrumentedClaudeQuery()` instead,
* which is simpler and handles option retrieval automatically.
*
* This low-level function is exported for advanced use cases where you need
* explicit control over the patching process.
*
* @param queryFunction - The original query function from @anthropic-ai/claude-agent-sdk
* @param options - Instrumentation options (recordInputs, recordOutputs)
* @returns Instrumented query function
*
* @see createInstrumentedClaudeQuery for the recommended high-level helper
*/
export { patchClaudeCodeQuery };
Loading
Loading