events.ts
1 import type { Attributes } from '@opentelemetry/api' 2 import { getEventLogger, getPromptId } from 'src/bootstrap/state.js' 3 import { logForDebugging } from '../debug.js' 4 import { isEnvTruthy } from '../envUtils.js' 5 import { getTelemetryAttributes } from '../telemetryAttributes.js' 6 7 // Monotonically increasing counter for ordering events within a session 8 let eventSequence = 0 9 10 // Track whether we've already warned about a null event logger to avoid spamming 11 let hasWarnedNoEventLogger = false 12 13 function isUserPromptLoggingEnabled() { 14 return isEnvTruthy(process.env.OTEL_LOG_USER_PROMPTS) 15 } 16 17 export function redactIfDisabled(content: string): string { 18 return isUserPromptLoggingEnabled() ? content : '<REDACTED>' 19 } 20 21 export async function logOTelEvent( 22 eventName: string, 23 metadata: { [key: string]: string | undefined } = {}, 24 ): Promise<void> { 25 const eventLogger = getEventLogger() 26 if (!eventLogger) { 27 if (!hasWarnedNoEventLogger) { 28 hasWarnedNoEventLogger = true 29 logForDebugging( 30 `[3P telemetry] Event dropped (no event logger initialized): ${eventName}`, 31 { level: 'warn' }, 32 ) 33 } 34 return 35 } 36 37 // Skip logging in test environment 38 if (process.env.NODE_ENV === 'test') { 39 return 40 } 41 42 const attributes: Attributes = { 43 ...getTelemetryAttributes(), 44 'event.name': eventName, 45 'event.timestamp': new Date().toISOString(), 46 'event.sequence': eventSequence++, 47 } 48 49 // Add prompt ID to events (but not metrics, where it would cause unbounded cardinality) 50 const promptId = getPromptId() 51 if (promptId) { 52 attributes['prompt.id'] = promptId 53 } 54 55 // Workspace directory from the desktop app (host path). Events only — 56 // filesystem paths are too high-cardinality for metric dimensions, and 57 // the BQ metrics pipeline must never see them. 58 const workspaceDir = process.env.CLAUDE_CODE_WORKSPACE_HOST_PATHS 59 if (workspaceDir) { 60 attributes['workspace.host_paths'] = workspaceDir.split('|') 61 } 62 63 // Add metadata as attributes - all values are already strings 64 for (const [key, value] of Object.entries(metadata)) { 65 if (value !== undefined) { 66 attributes[key] = value 67 } 68 } 69 70 // Emit log record as an event 71 eventLogger.emit({ 72 body: `claude_code.${eventName}`, 73 attributes, 74 }) 75 }