/ src / utils / telemetry / events.ts
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  }