(
eventName: string,
metadata: { [key: string]: string | undefined } = {},
)
| 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 | } |
| 76 |
no test coverage detected