Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
28 kB · 927 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928/** * Session Tracing for Claude Code using OpenTelemetry (BETA) * * This module provides a high-level API for creating and managing spans * to trace Claude Code workflows. Each user interaction creates a root * interaction span, which contains operation spans (LLM requests, tool calls, etc.). * * Requirements: * - Enhanced telemetry is enabled via feature('ENHANCED_TELEMETRY_BETA') * - Configure OTEL_TRACES_EXPORTER (console, otlp, etc.) */
import { feature } from 'bun:bundle'import { context as otelContext, type Span, trace } from '@opentelemetry/api'import { AsyncLocalStorage } from 'async_hooks'import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'import type { AssistantMessage, UserMessage } from '../../types/message.js'import { isEnvDefinedFalsy, isEnvTruthy } from '../envUtils.js'import { getTelemetryAttributes } from '../telemetryAttributes.js'import { addBetaInteractionAttributes, addBetaLLMRequestAttributes, addBetaLLMResponseAttributes, addBetaToolInputAttributes, addBetaToolResultAttributes, isBetaTracingEnabled, type LLMRequestNewContext, truncateContent,} from './betaSessionTracing.js'import { endInteractionPerfettoSpan, endLLMRequestPerfettoSpan, endToolPerfettoSpan, endUserInputPerfettoSpan, isPerfettoTracingEnabled, startInteractionPerfettoSpan, startLLMRequestPerfettoSpan, startToolPerfettoSpan, startUserInputPerfettoSpan,} from './perfettoTracing.js'
// Re-export for callersexport type { Span }export { isBetaTracingEnabled, type LLMRequestNewContext }
// Message type for API calls (UserMessage or AssistantMessage)type APIMessage = UserMessage | AssistantMessage
type SpanType = | 'interaction' | 'llm_request' | 'tool' | 'tool.blocked_on_user' | 'tool.execution' | 'hook'
interface SpanContext { span: Span startTime: number attributes: Record<string, string | number | boolean> ended?: boolean perfettoSpanId?: string}
// ALS stores SpanContext directly so it holds a strong reference while a span// is active. With that, activeSpans can use WeakRef — when ALS is cleared// (enterWith(undefined)) and no other code holds the SpanContext, GC can collect// it and the WeakRef goes stale.const interactionContext = new AsyncLocalStorage<SpanContext | undefined>()const toolContext = new AsyncLocalStorage<SpanContext | undefined>()const activeSpans = new Map<string, WeakRef<SpanContext>>()// Spans not stored in ALS (LLM request, blocked-on-user, tool execution, hook)// need a strong reference to prevent GC from collecting the SpanContext before// the corresponding end* function retrieves it.const strongSpans = new Map<string, SpanContext>()let interactionSequence = 0let _cleanupIntervalStarted = false
const SPAN_TTL_MS = 30 * 60 * 1000 // 30 minutes
function getSpanId(span: Span): string { return span.spanContext().spanId || ''}
/** * Lazily start a background interval that evicts orphaned spans from activeSpans. * * Normal teardown calls endInteractionSpan / endToolSpan, which delete spans * immediately. This interval is a safety net for spans that were never ended * (e.g. aborted streams, uncaught exceptions mid-query) — without it they * accumulate in activeSpans indefinitely, holding references to Span objects * and the OpenTelemetry context chain. * * Initialized on the first startInteractionSpan call (not at module load) to * avoid triggering the no-top-level-side-effects lint rule and to keep the * interval from running in processes that never start a span. * unref() prevents the timer from keeping the process alive after all other * work is done. */function ensureCleanupInterval(): void { if (_cleanupIntervalStarted) return _cleanupIntervalStarted = true const interval = setInterval(() => { const cutoff = Date.now() - SPAN_TTL_MS for (const [spanId, weakRef] of activeSpans) { const ctx = weakRef.deref() if (ctx === undefined) { activeSpans.delete(spanId) strongSpans.delete(spanId) } else if (ctx.startTime < cutoff) { if (!ctx.ended) ctx.span.end() // flush any recorded attributes to the exporter activeSpans.delete(spanId) strongSpans.delete(spanId) } } }, 60_000) if (typeof interval.unref === 'function') { interval.unref() // Node.js / Bun: don't block process exit }}
/** * Check if enhanced telemetry is enabled. * Priority: env var override > ant build > GrowthBook gate */export function isEnhancedTelemetryEnabled(): boolean { if (feature('ENHANCED_TELEMETRY_BETA')) { const env = process.env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA ?? process.env.ENABLE_ENHANCED_TELEMETRY_BETA if (isEnvTruthy(env)) { return true } if (isEnvDefinedFalsy(env)) { return false } return ( process.env.USER_TYPE === 'ant' || getFeatureValue_CACHED_MAY_BE_STALE('enhanced_telemetry_beta', false) ) } return false}
/** * Check if any tracing is enabled (either standard enhanced telemetry OR beta tracing) */function isAnyTracingEnabled(): boolean { return isEnhancedTelemetryEnabled() || isBetaTracingEnabled()}
function getTracer() { return trace.getTracer('com.anthropic.claude_code.tracing', '1.0.0')}
function createSpanAttributes( spanType: SpanType, customAttributes: Record<string, string | number | boolean> = {},): Record<string, string | number | boolean> { const baseAttributes = getTelemetryAttributes()
const attributes: Record<string, string | number | boolean> = { ...baseAttributes, 'span.type': spanType, ...customAttributes, }
return attributes}
/** * Start an interaction span. This wraps a user request -> Claude response cycle. * This is now a root span that includes all session-level attributes. * Sets the interaction context for all subsequent operations. */export function startInteractionSpan(userPrompt: string): Span { ensureCleanupInterval()
// Start Perfetto span regardless of OTel tracing state const perfettoSpanId = isPerfettoTracingEnabled() ? startInteractionPerfettoSpan(userPrompt) : undefined
if (!isAnyTracingEnabled()) { // Still track Perfetto span even if OTel is disabled if (perfettoSpanId) { const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy') const spanId = getSpanId(dummySpan) const spanContextObj: SpanContext = { span: dummySpan, startTime: Date.now(), attributes: {}, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) interactionContext.enterWith(spanContextObj) return dummySpan } return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const isUserPromptLoggingEnabled = isEnvTruthy( process.env.OTEL_LOG_USER_PROMPTS, ) const promptToLog = isUserPromptLoggingEnabled ? userPrompt : '<REDACTED>'
interactionSequence++
const attributes = createSpanAttributes('interaction', { user_prompt: promptToLog, user_prompt_length: userPrompt.length, 'interaction.sequence': interactionSequence, })
const span = tracer.startSpan('claude_code.interaction', { attributes, })
// Add experimental attributes (new_context) addBetaInteractionAttributes(span, userPrompt)
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj))
interactionContext.enterWith(spanContextObj)
return span}
export function endInteractionSpan(): void { const spanContext = interactionContext.getStore() if (!spanContext) { return }
if (spanContext.ended) { return }
// End Perfetto span if (spanContext.perfettoSpanId) { endInteractionPerfettoSpan(spanContext.perfettoSpanId) }
if (!isAnyTracingEnabled()) { spanContext.ended = true activeSpans.delete(getSpanId(spanContext.span)) // Clear the store so async continuations created after this point (timers, // promise callbacks, I/O) do not inherit a reference to the ended span. // enterWith(undefined) is intentional: exit(() => {}) is a no-op because it // only suppresses the store inside the callback and returns immediately. interactionContext.enterWith(undefined) return }
const duration = Date.now() - spanContext.startTime spanContext.span.setAttributes({ 'interaction.duration_ms': duration, })
spanContext.span.end() spanContext.ended = true activeSpans.delete(getSpanId(spanContext.span)) interactionContext.enterWith(undefined)}
export function startLLMRequestSpan( model: string, newContext?: LLMRequestNewContext, messagesForAPI?: APIMessage[], fastMode?: boolean,): Span { // Start Perfetto span regardless of OTel tracing state const perfettoSpanId = isPerfettoTracingEnabled() ? startLLMRequestPerfettoSpan({ model, querySource: newContext?.querySource, messageId: undefined, // Will be set in endLLMRequestSpan }) : undefined
if (!isAnyTracingEnabled()) { // Still track Perfetto span even if OTel is disabled if (perfettoSpanId) { const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy') const spanId = getSpanId(dummySpan) const spanContextObj: SpanContext = { span: dummySpan, startTime: Date.now(), attributes: { model }, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj) return dummySpan } return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const parentSpanCtx = interactionContext.getStore()
const attributes = createSpanAttributes('llm_request', { model: model, 'llm_request.context': parentSpanCtx ? 'interaction' : 'standalone', speed: fastMode ? 'fast' : 'normal', })
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan('claude_code.llm_request', { attributes }, ctx)
// Add query_source (agent name) if provided if (newContext?.querySource) { span.setAttribute('query_source', newContext.querySource) }
// Add experimental attributes (system prompt, new_context) addBetaLLMRequestAttributes(span, newContext, messagesForAPI)
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj)
return span}
/** * End an LLM request span and attach response metadata. * * @param span - Optional. The exact span returned by startLLMRequestSpan(). * IMPORTANT: When multiple LLM requests run in parallel (e.g., warmup requests, * topic classifier, file path extractor, main thread), you MUST pass the specific span * to ensure responses are attached to the correct request. Without it, responses may be * incorrectly attached to whichever span happens to be "last" in the activeSpans map. * * If not provided, falls back to finding the most recent llm_request span (legacy behavior). */export function endLLMRequestSpan( span?: Span, metadata?: { inputTokens?: number outputTokens?: number cacheReadTokens?: number cacheCreationTokens?: number success?: boolean statusCode?: number error?: string attempt?: number modelResponse?: string /** Text output from the model (non-thinking content) */ modelOutput?: string /** Thinking/reasoning output from the model */ thinkingOutput?: string /** Whether the output included tool calls (look at tool spans for details) */ hasToolCall?: boolean /** Time to first token in milliseconds */ ttftMs?: number /** Time spent in pre-request setup before the successful attempt */ requestSetupMs?: number /** Timestamps (Date.now()) of each attempt start — used to emit retry sub-spans */ attemptStartTimes?: number[] },): void { let llmSpanContext: SpanContext | undefined
if (span) { // Use the provided span directly - this is the correct approach for parallel requests const spanId = getSpanId(span) llmSpanContext = activeSpans.get(spanId)?.deref() } else { // Legacy fallback: find the most recent llm_request span // WARNING: This can cause mismatched responses when multiple requests are in flight llmSpanContext = Array.from(activeSpans.values()) .findLast(r => { const ctx = r.deref() return ( ctx?.attributes['span.type'] === 'llm_request' || ctx?.attributes['model'] ) }) ?.deref() }
if (!llmSpanContext) { // Span was already ended or never tracked return }
const duration = Date.now() - llmSpanContext.startTime
// End Perfetto span with full metadata if (llmSpanContext.perfettoSpanId) { endLLMRequestPerfettoSpan(llmSpanContext.perfettoSpanId, { ttftMs: metadata?.ttftMs, ttltMs: duration, // Time to last token is the total duration promptTokens: metadata?.inputTokens, outputTokens: metadata?.outputTokens, cacheReadTokens: metadata?.cacheReadTokens, cacheCreationTokens: metadata?.cacheCreationTokens, success: metadata?.success, error: metadata?.error, requestSetupMs: metadata?.requestSetupMs, attemptStartTimes: metadata?.attemptStartTimes, }) }
if (!isAnyTracingEnabled()) { const spanId = getSpanId(llmSpanContext.span) activeSpans.delete(spanId) strongSpans.delete(spanId) return }
const endAttributes: Record<string, string | number | boolean> = { duration_ms: duration, }
if (metadata) { if (metadata.inputTokens !== undefined) endAttributes['input_tokens'] = metadata.inputTokens if (metadata.outputTokens !== undefined) endAttributes['output_tokens'] = metadata.outputTokens if (metadata.cacheReadTokens !== undefined) endAttributes['cache_read_tokens'] = metadata.cacheReadTokens if (metadata.cacheCreationTokens !== undefined) endAttributes['cache_creation_tokens'] = metadata.cacheCreationTokens if (metadata.success !== undefined) endAttributes['success'] = metadata.success if (metadata.statusCode !== undefined) endAttributes['status_code'] = metadata.statusCode if (metadata.error !== undefined) endAttributes['error'] = metadata.error if (metadata.attempt !== undefined) endAttributes['attempt'] = metadata.attempt if (metadata.hasToolCall !== undefined) endAttributes['response.has_tool_call'] = metadata.hasToolCall if (metadata.ttftMs !== undefined) endAttributes['ttft_ms'] = metadata.ttftMs
// Add experimental response attributes (model_output, thinking_output) addBetaLLMResponseAttributes(endAttributes, metadata) }
llmSpanContext.span.setAttributes(endAttributes) llmSpanContext.span.end()
const spanId = getSpanId(llmSpanContext.span) activeSpans.delete(spanId) strongSpans.delete(spanId)}
export function startToolSpan( toolName: string, toolAttributes?: Record<string, string | number | boolean>, toolInput?: string,): Span { // Start Perfetto span regardless of OTel tracing state const perfettoSpanId = isPerfettoTracingEnabled() ? startToolPerfettoSpan(toolName, toolAttributes) : undefined
if (!isAnyTracingEnabled()) { // Still track Perfetto span even if OTel is disabled if (perfettoSpanId) { const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy') const spanId = getSpanId(dummySpan) const spanContextObj: SpanContext = { span: dummySpan, startTime: Date.now(), attributes: { 'span.type': 'tool', tool_name: toolName }, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) toolContext.enterWith(spanContextObj) return dummySpan } return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const parentSpanCtx = interactionContext.getStore()
const attributes = createSpanAttributes('tool', { tool_name: toolName, ...toolAttributes, })
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan('claude_code.tool', { attributes }, ctx)
// Add experimental tool input attributes if (toolInput) { addBetaToolInputAttributes(span, toolName, toolInput) }
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj))
toolContext.enterWith(spanContextObj)
return span}
export function startToolBlockedOnUserSpan(): Span { // Start Perfetto span regardless of OTel tracing state const perfettoSpanId = isPerfettoTracingEnabled() ? startUserInputPerfettoSpan('tool_permission') : undefined
if (!isAnyTracingEnabled()) { // Still track Perfetto span even if OTel is disabled if (perfettoSpanId) { const dummySpan = trace.getActiveSpan() || getTracer().startSpan('dummy') const spanId = getSpanId(dummySpan) const spanContextObj: SpanContext = { span: dummySpan, startTime: Date.now(), attributes: { 'span.type': 'tool.blocked_on_user' }, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj) return dummySpan } return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const parentSpanCtx = toolContext.getStore()
const attributes = createSpanAttributes('tool.blocked_on_user')
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan( 'claude_code.tool.blocked_on_user', { attributes }, ctx, )
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, perfettoSpanId, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj)
return span}
export function endToolBlockedOnUserSpan( decision?: string, source?: string,): void { const blockedSpanContext = Array.from(activeSpans.values()) .findLast( r => r.deref()?.attributes['span.type'] === 'tool.blocked_on_user', ) ?.deref()
if (!blockedSpanContext) { return }
// End Perfetto span if (blockedSpanContext.perfettoSpanId) { endUserInputPerfettoSpan(blockedSpanContext.perfettoSpanId, { decision, source, }) }
if (!isAnyTracingEnabled()) { const spanId = getSpanId(blockedSpanContext.span) activeSpans.delete(spanId) strongSpans.delete(spanId) return }
const duration = Date.now() - blockedSpanContext.startTime const attributes: Record<string, string | number | boolean> = { duration_ms: duration, }
if (decision) { attributes['decision'] = decision } if (source) { attributes['source'] = source }
blockedSpanContext.span.setAttributes(attributes) blockedSpanContext.span.end()
const spanId = getSpanId(blockedSpanContext.span) activeSpans.delete(spanId) strongSpans.delete(spanId)}
export function startToolExecutionSpan(): Span { if (!isAnyTracingEnabled()) { return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const parentSpanCtx = toolContext.getStore()
const attributes = createSpanAttributes('tool.execution')
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan( 'claude_code.tool.execution', { attributes }, ctx, )
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj)
return span}
export function endToolExecutionSpan(metadata?: { success?: boolean error?: string}): void { if (!isAnyTracingEnabled()) { return }
const executionSpanContext = Array.from(activeSpans.values()) .findLast(r => r.deref()?.attributes['span.type'] === 'tool.execution') ?.deref()
if (!executionSpanContext) { return }
const duration = Date.now() - executionSpanContext.startTime const attributes: Record<string, string | number | boolean> = { duration_ms: duration, }
if (metadata) { if (metadata.success !== undefined) attributes['success'] = metadata.success if (metadata.error !== undefined) attributes['error'] = metadata.error }
executionSpanContext.span.setAttributes(attributes) executionSpanContext.span.end()
const spanId = getSpanId(executionSpanContext.span) activeSpans.delete(spanId) strongSpans.delete(spanId)}
export function endToolSpan(toolResult?: string, resultTokens?: number): void { const toolSpanContext = toolContext.getStore()
if (!toolSpanContext) { return }
// End Perfetto span if (toolSpanContext.perfettoSpanId) { endToolPerfettoSpan(toolSpanContext.perfettoSpanId, { success: true, resultTokens, }) }
if (!isAnyTracingEnabled()) { const spanId = getSpanId(toolSpanContext.span) activeSpans.delete(spanId) // Same reasoning as interactionContext above: clear so subsequent async // work doesn't hold a stale reference to the ended tool span. toolContext.enterWith(undefined) return }
const duration = Date.now() - toolSpanContext.startTime const endAttributes: Record<string, string | number | boolean> = { duration_ms: duration, }
// Add experimental tool result attributes (new_context) if (toolResult) { const toolName = toolSpanContext.attributes['tool_name'] || 'unknown' addBetaToolResultAttributes(endAttributes, toolName, toolResult) }
if (resultTokens !== undefined) { endAttributes['result_tokens'] = resultTokens }
toolSpanContext.span.setAttributes(endAttributes) toolSpanContext.span.end()
const spanId = getSpanId(toolSpanContext.span) activeSpans.delete(spanId) toolContext.enterWith(undefined)}
function isToolContentLoggingEnabled(): boolean { return isEnvTruthy(process.env.OTEL_LOG_TOOL_CONTENT)}
/** * Add a span event with tool content/output data. * Only logs if OTEL_LOG_TOOL_CONTENT=1 is set. * Truncates content if it exceeds MAX_CONTENT_SIZE. */export function addToolContentEvent( eventName: string, attributes: Record<string, string | number | boolean>,): void { if (!isAnyTracingEnabled() || !isToolContentLoggingEnabled()) { return }
const currentSpanCtx = toolContext.getStore() if (!currentSpanCtx) { return }
// Truncate string attributes that might be large const processedAttributes: Record<string, string | number | boolean> = {} for (const [key, value] of Object.entries(attributes)) { if (typeof value === 'string') { const { content, truncated } = truncateContent(value) processedAttributes[key] = content if (truncated) { processedAttributes[`${key}_truncated`] = true processedAttributes[`${key}_original_length`] = value.length } } else { processedAttributes[key] = value } }
currentSpanCtx.span.addEvent(eventName, processedAttributes)}
export function getCurrentSpan(): Span | null { if (!isAnyTracingEnabled()) { return null }
return ( toolContext.getStore()?.span ?? interactionContext.getStore()?.span ?? null )}
export async function executeInSpan<T>( spanName: string, fn: (span: Span) => Promise<T>, attributes?: Record<string, string | number | boolean>,): Promise<T> { if (!isAnyTracingEnabled()) { return fn(trace.getActiveSpan() || getTracer().startSpan('dummy')) }
const tracer = getTracer() const parentSpanCtx = toolContext.getStore() ?? interactionContext.getStore()
const finalAttributes = createSpanAttributes('tool', { ...attributes, })
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan(spanName, { attributes: finalAttributes }, ctx)
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes: finalAttributes, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj)
try { const result = await fn(span) span.end() activeSpans.delete(spanId) strongSpans.delete(spanId) return result } catch (error) { if (error instanceof Error) { span.recordException(error) } span.end() activeSpans.delete(spanId) strongSpans.delete(spanId) throw error }}
/** * Start a hook execution span. * Only creates a span when beta tracing is enabled. * @param hookEvent The hook event type (e.g., 'PreToolUse', 'PostToolUse') * @param hookName The full hook name (e.g., 'PreToolUse:Write') * @param numHooks The number of hooks being executed * @param hookDefinitions JSON string of hook definitions for tracing * @returns The span (or a dummy span if tracing is disabled) */export function startHookSpan( hookEvent: string, hookName: string, numHooks: number, hookDefinitions: string,): Span { if (!isBetaTracingEnabled()) { return trace.getActiveSpan() || getTracer().startSpan('dummy') }
const tracer = getTracer() const parentSpanCtx = toolContext.getStore() ?? interactionContext.getStore()
const attributes = createSpanAttributes('hook', { hook_event: hookEvent, hook_name: hookName, num_hooks: numHooks, hook_definitions: hookDefinitions, })
const ctx = parentSpanCtx ? trace.setSpan(otelContext.active(), parentSpanCtx.span) : otelContext.active() const span = tracer.startSpan('claude_code.hook', { attributes }, ctx)
const spanId = getSpanId(span) const spanContextObj: SpanContext = { span, startTime: Date.now(), attributes, } activeSpans.set(spanId, new WeakRef(spanContextObj)) strongSpans.set(spanId, spanContextObj)
return span}
/** * End a hook execution span with outcome metadata. * Only does work when beta tracing is enabled. * @param span The span to end (returned from startHookSpan) * @param metadata The outcome metadata for the hook execution */export function endHookSpan( span: Span, metadata?: { numSuccess?: number numBlocking?: number numNonBlockingError?: number numCancelled?: number },): void { if (!isBetaTracingEnabled()) { return }
const spanId = getSpanId(span) const spanContext = activeSpans.get(spanId)?.deref()
if (!spanContext) { return }
const duration = Date.now() - spanContext.startTime const endAttributes: Record<string, string | number | boolean> = { duration_ms: duration, }
if (metadata) { if (metadata.numSuccess !== undefined) endAttributes['num_success'] = metadata.numSuccess if (metadata.numBlocking !== undefined) endAttributes['num_blocking'] = metadata.numBlocking if (metadata.numNonBlockingError !== undefined) endAttributes['num_non_blocking_error'] = metadata.numNonBlockingError if (metadata.numCancelled !== undefined) endAttributes['num_cancelled'] = metadata.numCancelled }
spanContext.span.setAttributes(endAttributes) spanContext.span.end() activeSpans.delete(spanId) strongSpans.delete(spanId)}