diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index fe5a75bd5c8b..85da1fdb9c57 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -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; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 5866f014ec69..83ffd0692cc6 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -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'; @@ -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 & { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4808f22b472b..4cf4765d44c9 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -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, diff --git a/packages/node/src/integrations/tracing/claude-code/helpers.ts b/packages/node/src/integrations/tracing/claude-code/helpers.ts new file mode 100644 index 000000000000..cddc2d5835dc --- /dev/null +++ b/packages/node/src/integrations/tracing/claude-code/helpers.ts @@ -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) | null = null; +let _initPromise: Promise | null = null; + +/** + * Lazily loads and patches the Claude Code SDK. + * Ensures only one patched instance exists globally. + */ +async function ensurePatchedQuery(): Promise { + 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 { + return async function* query(...args: unknown[]): AsyncGenerator { + await ensurePatchedQuery(); + + if (!_globalPatchedQuery) { + throw new Error('[Sentry] Failed to initialize instrumented Claude Code query function'); + } + + yield* _globalPatchedQuery(...args); + }; +} diff --git a/packages/node/src/integrations/tracing/claude-code/index.ts b/packages/node/src/integrations/tracing/claude-code/index.ts new file mode 100644 index 000000000000..ca4009ab0826 --- /dev/null +++ b/packages/node/src/integrations/tracing/claude-code/index.ts @@ -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 + // 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 }; diff --git a/packages/node/src/integrations/tracing/claude-code/instrumentation.ts b/packages/node/src/integrations/tracing/claude-code/instrumentation.ts new file mode 100644 index 000000000000..822fecb2c76d --- /dev/null +++ b/packages/node/src/integrations/tracing/claude-code/instrumentation.ts @@ -0,0 +1,398 @@ +import type { Span } from '@opentelemetry/api'; +import { + getClient, + startSpanManual, + withActiveSpan, + startSpan, + captureException, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_OP, +} from '@sentry/core'; +import type { ClaudeCodeOptions } from './index'; + +type ClaudeCodeInstrumentationOptions = ClaudeCodeOptions; + +const GEN_AI_ATTRIBUTES = { + SYSTEM: 'gen_ai.system', + OPERATION_NAME: 'gen_ai.operation.name', + REQUEST_MODEL: 'gen_ai.request.model', + REQUEST_MESSAGES: 'gen_ai.request.messages', + RESPONSE_TEXT: 'gen_ai.response.text', + RESPONSE_TOOL_CALLS: 'gen_ai.response.tool_calls', + RESPONSE_ID: 'gen_ai.response.id', + RESPONSE_MODEL: 'gen_ai.response.model', + USAGE_INPUT_TOKENS: 'gen_ai.usage.input_tokens', + USAGE_OUTPUT_TOKENS: 'gen_ai.usage.output_tokens', + USAGE_TOTAL_TOKENS: 'gen_ai.usage.total_tokens', + TOOL_NAME: 'gen_ai.tool.name', + TOOL_INPUT: 'gen_ai.tool.input', + TOOL_OUTPUT: 'gen_ai.tool.output', + AGENT_NAME: 'gen_ai.agent.name', +} as const; + +const SENTRY_ORIGIN = 'auto.ai.claude-code'; + +function setTokenUsageAttributes( + span: Span, + inputTokens?: number, + outputTokens?: number, + cacheCreationTokens?: number, + cacheReadTokens?: number, +): void { + const attrs: Record = {}; + + if (typeof inputTokens === 'number') { + attrs[GEN_AI_ATTRIBUTES.USAGE_INPUT_TOKENS] = inputTokens; + } + if (typeof outputTokens === 'number') { + attrs[GEN_AI_ATTRIBUTES.USAGE_OUTPUT_TOKENS] = outputTokens; + } + + const total = (inputTokens ?? 0) + (outputTokens ?? 0) + (cacheCreationTokens ?? 0) + (cacheReadTokens ?? 0); + if (total > 0) { + attrs[GEN_AI_ATTRIBUTES.USAGE_TOTAL_TOKENS] = total; + } + + if (Object.keys(attrs).length > 0) { + span.setAttributes(attrs); + } +} + +/** + * 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( + queryFunction: (...args: unknown[]) => AsyncGenerator, + options: ClaudeCodeInstrumentationOptions = {}, +): (...args: unknown[]) => AsyncGenerator { + const patchedQuery = function (this: unknown, ...args: unknown[]): AsyncGenerator { + const client = getClient(); + const defaultPii = Boolean(client?.getOptions().sendDefaultPii); + + const recordInputs = options.recordInputs ?? defaultPii; + const recordOutputs = options.recordOutputs ?? defaultPii; + + // Parse query arguments + const [queryParams] = args as [Record]; + const { options: queryOptions, inputMessages } = queryParams || {}; + const model = (queryOptions as Record)?.model ?? 'sonnet'; + + // Create original query instance + const originalQueryInstance = queryFunction.apply(this, args); + + // Create instrumented generator + const instrumentedGenerator = _createInstrumentedGenerator( + originalQueryInstance, + model as string, + { recordInputs, recordOutputs, inputMessages }, + ); + + // Preserve Query interface methods + if (typeof (originalQueryInstance as Record).interrupt === 'function') { + (instrumentedGenerator as unknown as Record).interrupt = ( + (originalQueryInstance as Record).interrupt as Function + ).bind(originalQueryInstance); + } + if (typeof (originalQueryInstance as Record).setPermissionMode === 'function') { + (instrumentedGenerator as unknown as Record).setPermissionMode = ( + (originalQueryInstance as Record).setPermissionMode as Function + ).bind(originalQueryInstance); + } + + return instrumentedGenerator; + }; + + return patchedQuery as typeof queryFunction; +} + +/** + * Creates an instrumented async generator that wraps the original query. + */ +function _createInstrumentedGenerator( + originalQuery: AsyncGenerator, + model: string, + instrumentationOptions: { recordInputs?: boolean; recordOutputs?: boolean; inputMessages?: unknown }, +): AsyncGenerator { + return startSpanManual( + { + name: `invoke_agent claude-code`, + op: 'gen_ai.invoke_agent', + attributes: { + [GEN_AI_ATTRIBUTES.SYSTEM]: 'claude-code', + [GEN_AI_ATTRIBUTES.REQUEST_MODEL]: model, + [GEN_AI_ATTRIBUTES.OPERATION_NAME]: 'invoke_agent', + [GEN_AI_ATTRIBUTES.AGENT_NAME]: 'claude-code', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.invoke_agent', + }, + }, + async function* (span: Span) { + // State accumulation + let sessionId: string | null = null; + let currentLLMSpan: Span | null = null; + let currentTurnContent = ''; + let currentTurnTools: unknown[] = []; + let currentTurnId: string | null = null; + let currentTurnModel: string | null = null; + let inputMessagesCaptured = false; + let finalResult: string | null = null; + let previousLLMSpan: Span | null = null; + let previousTurnTools: unknown[] = []; + + try { + for await (const message of originalQuery) { + const msg = message as Record; + + // Extract session ID from system message + if (msg.type === 'system' && msg.session_id) { + sessionId = msg.session_id as string; + + if ( + !inputMessagesCaptured && + instrumentationOptions.recordInputs && + msg.conversation_history + ) { + span.setAttributes({ + [GEN_AI_ATTRIBUTES.REQUEST_MESSAGES]: JSON.stringify(msg.conversation_history), + }); + inputMessagesCaptured = true; + } + } + + // Handle assistant messages + if (msg.type === 'assistant') { + // Close previous LLM span if still open + if (previousLLMSpan) { + previousLLMSpan.setStatus({ code: 1 }); + previousLLMSpan.end(); + previousLLMSpan = null; + previousTurnTools = []; + } + + // Create new LLM span + if (!currentLLMSpan) { + currentLLMSpan = withActiveSpan(span, () => { + return startSpanManual( + { + name: `chat ${model}`, + op: 'gen_ai.chat', + attributes: { + [GEN_AI_ATTRIBUTES.SYSTEM]: 'claude-code', + [GEN_AI_ATTRIBUTES.REQUEST_MODEL]: model, + [GEN_AI_ATTRIBUTES.OPERATION_NAME]: 'chat', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat', + }, + }, + (childSpan: Span) => { + if (instrumentationOptions.recordInputs && instrumentationOptions.inputMessages) { + childSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.REQUEST_MESSAGES]: JSON.stringify( + instrumentationOptions.inputMessages, + ), + }); + } + return childSpan; + }, + ); + }); + + currentTurnContent = ''; + currentTurnTools = []; + } + + // Accumulate content + const content = (msg.message as Record)?.content as unknown[]; + if (Array.isArray(content)) { + const textContent = content + .filter((c) => (c as Record).type === 'text') + .map((c) => (c as Record).text as string) + .join(''); + if (textContent) { + currentTurnContent += textContent; + } + + const tools = content.filter((c) => (c as Record).type === 'tool_use'); + if (tools.length > 0) { + currentTurnTools.push(...tools); + } + } + + if ((msg.message as Record)?.id) { + currentTurnId = (msg.message as Record).id as string; + } + if ((msg.message as Record)?.model) { + currentTurnModel = (msg.message as Record).model as string; + } + } + + // Handle result messages + if (msg.type === 'result') { + if (msg.result) { + finalResult = msg.result as string; + } + + // Close previous LLM span + if (previousLLMSpan) { + previousLLMSpan.setStatus({ code: 1 }); + previousLLMSpan.end(); + previousLLMSpan = null; + previousTurnTools = []; + } + + // Finalize current LLM span + if (currentLLMSpan) { + if (instrumentationOptions.recordOutputs && currentTurnContent) { + currentLLMSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_TEXT]: currentTurnContent, + }); + } + + if (instrumentationOptions.recordOutputs && currentTurnTools.length > 0) { + currentLLMSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_TOOL_CALLS]: JSON.stringify(currentTurnTools), + }); + } + + if (currentTurnId) { + currentLLMSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_ID]: currentTurnId, + }); + } + if (currentTurnModel) { + currentLLMSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_MODEL]: currentTurnModel, + }); + } + + if (msg.usage) { + const usage = msg.usage as Record; + setTokenUsageAttributes( + currentLLMSpan, + usage.input_tokens, + usage.output_tokens, + usage.cache_creation_input_tokens, + usage.cache_read_input_tokens, + ); + } + + currentLLMSpan.setStatus({ code: 1 }); + currentLLMSpan.end(); + + previousLLMSpan = currentLLMSpan; + previousTurnTools = currentTurnTools; + + currentLLMSpan = null; + currentTurnContent = ''; + currentTurnTools = []; + currentTurnId = null; + currentTurnModel = null; + } + } + + // Handle tool results + if (msg.type === 'user' && (msg.message as Record)?.content) { + const content = (msg.message as Record).content as unknown[]; + const toolResults = Array.isArray(content) + ? content.filter((c) => (c as Record).type === 'tool_result') + : []; + + for (const toolResult of toolResults) { + const tr = toolResult as Record; + let matchingTool = currentTurnTools.find( + (t) => (t as Record).id === tr.tool_use_id, + ) as Record | undefined; + let parentLLMSpan = currentLLMSpan; + + if (!matchingTool && previousTurnTools.length > 0) { + matchingTool = previousTurnTools.find( + (t) => (t as Record).id === tr.tool_use_id, + ) as Record | undefined; + parentLLMSpan = previousLLMSpan; + } + + if (matchingTool && parentLLMSpan) { + withActiveSpan(parentLLMSpan, () => { + startSpan( + { + name: `execute_tool ${matchingTool!.name as string}`, + op: 'gen_ai.execute_tool', + attributes: { + [GEN_AI_ATTRIBUTES.SYSTEM]: 'claude-code', + [GEN_AI_ATTRIBUTES.REQUEST_MODEL]: model, + [GEN_AI_ATTRIBUTES.OPERATION_NAME]: 'execute_tool', + [GEN_AI_ATTRIBUTES.AGENT_NAME]: 'claude-code', + [GEN_AI_ATTRIBUTES.TOOL_NAME]: matchingTool!.name as string, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SENTRY_ORIGIN, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.execute_tool', + }, + }, + (toolSpan: Span) => { + if (instrumentationOptions.recordInputs && matchingTool!.input) { + toolSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.TOOL_INPUT]: JSON.stringify(matchingTool!.input), + }); + } + + if (instrumentationOptions.recordOutputs && tr.content) { + toolSpan.setAttributes({ + [GEN_AI_ATTRIBUTES.TOOL_OUTPUT]: + typeof tr.content === 'string' ? tr.content : JSON.stringify(tr.content), + }); + } + + if (tr.is_error) { + toolSpan.setStatus({ code: 2, message: 'Tool execution error' }); + } + }, + ); + }); + } + } + } + + yield message; + } + + if (instrumentationOptions.recordOutputs && finalResult) { + span.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_TEXT]: finalResult, + }); + } + + if (sessionId) { + span.setAttributes({ + [GEN_AI_ATTRIBUTES.RESPONSE_ID]: sessionId, + }); + } + + span.setStatus({ code: 1 }); + } catch (error) { + // Capture exception to Sentry with proper metadata + captureException(error, { + mechanism: { + type: SENTRY_ORIGIN, + handled: false, + }, + }); + + span.setStatus({ code: 2, message: (error as Error).message }); + throw error; + } finally { + // Ensure all child spans are closed even if generator exits early + if (currentLLMSpan && currentLLMSpan.isRecording()) { + currentLLMSpan.setStatus({ code: 1 }); + currentLLMSpan.end(); + } + + if (previousLLMSpan && previousLLMSpan.isRecording()) { + previousLLMSpan.setStatus({ code: 1 }); + previousLLMSpan.end(); + } + + span.end(); + } + }, + ); +} diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index dd9d9ac8df2b..48b5f75970f6 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -2,6 +2,7 @@ import type { Integration } from '@sentry/core'; import { instrumentOtelHttp, instrumentSentryHttp } from '../http'; import { amqplibIntegration, instrumentAmqplib } from './amqplib'; import { anthropicAIIntegration, instrumentAnthropicAi } from './anthropic-ai'; +import { claudeCodeIntegration } from './claude-code'; import { connectIntegration, instrumentConnect } from './connect'; import { expressIntegration, instrumentExpress } from './express'; import { fastifyIntegration, instrumentFastify, instrumentFastifyV3 } from './fastify'; @@ -56,6 +57,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { firebaseIntegration(), anthropicAIIntegration(), googleGenAIIntegration(), + claudeCodeIntegration(), ]; }