Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
5.5 kB · 173 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174/** * Analytics service - public API for event logging * * This module serves as the main entry point for analytics events in Claude CLI. * * DESIGN: This module has NO dependencies to avoid import cycles. * Events are queued until attachAnalyticsSink() is called during app initialization. * The sink handles routing to Datadog and 1P event logging. */
/** * Marker type for verifying analytics metadata doesn't contain sensitive data * * This type forces explicit verification that string values being logged * don't contain code snippets, file paths, or other sensitive information. * * Usage: `myString as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` */export type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS = never
/** * Marker type for values routed to PII-tagged proto columns via `_PROTO_*` * payload keys. The destination BQ column has privileged access controls, * so unredacted values are acceptable — unlike general-access backends. * * sink.ts strips `_PROTO_*` keys before Datadog fanout; only the 1P * exporter (firstPartyEventLoggingExporter) sees them and hoists them to the * top-level proto field. A single stripProtoFields call guards all non-1P * sinks — no per-sink filtering to forget. * * Usage: `rawName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED` */export type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED = never
/** * Strip `_PROTO_*` keys from a payload destined for general-access storage. * Used by: * - sink.ts: before Datadog fanout (never sees PII-tagged values) * - firstPartyEventLoggingExporter: defensive strip of additional_metadata * after hoisting known _PROTO_* keys to proto fields — prevents a future * unrecognized _PROTO_foo from silently landing in the BQ JSON blob. * * Returns the input unchanged (same reference) when no _PROTO_ keys present. */export function stripProtoFields<V>( metadata: Record<string, V>,): Record<string, V> { let result: Record<string, V> | undefined for (const key in metadata) { if (key.startsWith('_PROTO_')) { if (result === undefined) { result = { ...metadata } } delete result[key] } } return result ?? metadata}
// Internal type for logEvent metadata - different from the enriched EventMetadata in metadata.tstype LogEventMetadata = { [key: string]: boolean | number | undefined }
type QueuedEvent = { eventName: string metadata: LogEventMetadata async: boolean}
/** * Sink interface for the analytics backend */export type AnalyticsSink = { logEvent: (eventName: string, metadata: LogEventMetadata) => void logEventAsync: ( eventName: string, metadata: LogEventMetadata, ) => Promise<void>}
// Event queue for events logged before sink is attachedconst eventQueue: QueuedEvent[] = []
// Sink - initialized during app startuplet sink: AnalyticsSink | null = null
/** * Attach the analytics sink that will receive all events. * Queued events are drained asynchronously via queueMicrotask to avoid * adding latency to the startup path. * * Idempotent: if a sink is already attached, this is a no-op. This allows * calling from both the preAction hook (for subcommands) and setup() (for * the default command) without coordination. */export function attachAnalyticsSink(newSink: AnalyticsSink): void { if (sink !== null) { return } sink = newSink
// Drain the queue asynchronously to avoid blocking startup if (eventQueue.length > 0) { const queuedEvents = [...eventQueue] eventQueue.length = 0
// Log queue size for ants to help debug analytics initialization timing if (process.env.USER_TYPE === 'ant') { sink.logEvent('analytics_sink_attached', { queued_event_count: queuedEvents.length, }) }
queueMicrotask(() => { for (const event of queuedEvents) { if (event.async) { void sink!.logEventAsync(event.eventName, event.metadata) } else { sink!.logEvent(event.eventName, event.metadata) } } }) }}
/** * Log an event to analytics backends (synchronous) * * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config. * When sampled, the sample_rate is added to the event metadata. * * If no sink is attached, events are queued and drained when the sink attaches. */export function logEvent( eventName: string, // intentionally no strings unless AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, // to avoid accidentally logging code/filepaths metadata: LogEventMetadata,): void { if (sink === null) { eventQueue.push({ eventName, metadata, async: false }) return } sink.logEvent(eventName, metadata)}
/** * Log an event to analytics backends (asynchronous) * * Events may be sampled based on the 'tengu_event_sampling_config' dynamic config. * When sampled, the sample_rate is added to the event metadata. * * If no sink is attached, events are queued and drained when the sink attaches. */export async function logEventAsync( eventName: string, // intentionally no strings, to avoid accidentally logging code/filepaths metadata: LogEventMetadata,): Promise<void> { if (sink === null) { eventQueue.push({ eventName, metadata, async: true }) return } await sink.logEventAsync(eventName, metadata)}
/** * Reset analytics state for testing purposes only. * @internal */export function _resetForTesting(): void { sink = null eventQueue.length = 0}