/** * Analytics sink implementation * * This module contains the actual analytics routing logic and should be * initialized during app startup. It routes events to Datadog and 1P event * logging. * * Usage: Call initializeAnalyticsSink() during app startup to attach the sink. */ import { trackDatadogEvent } from './datadog.js' import { logEventTo1P, shouldSampleEvent } from './firstPartyEventLogger.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE } from './growthbook.js' import { attachAnalyticsSink, stripProtoFields } from './index.js' import { isSinkKilled } from './sinkKillswitch.js' // Local type matching the logEvent metadata signature type LogEventMetadata = { [key: string]: boolean | number | undefined } const DATADOG_GATE_NAME = 'tengu_log_datadog_events' // Module-level gate state - starts undefined, initialized during startup let isDatadogGateEnabled: boolean | undefined = undefined /** * Check if Datadog tracking is enabled. * Falls back to cached value from previous session if not yet initialized. */ function shouldTrackDatadog(): boolean { if (isSinkKilled('datadog')) { return false } if (isDatadogGateEnabled !== undefined) { return isDatadogGateEnabled } // Fallback to cached value from previous session try { return checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME) } catch { return false } } /** * Log an event (synchronous implementation) */ function logEventImpl(eventName: string, metadata: LogEventMetadata): void { // Check if this event should be sampled const sampleResult = shouldSampleEvent(eventName) // If sample result is 0, the event was not selected for logging if (sampleResult === 0) { return } // If sample result is a positive number, add it to metadata const metadataWithSampleRate = sampleResult !== null ? { ...metadata, sample_rate: sampleResult } : metadata if (shouldTrackDatadog()) { // Datadog is a general-access backend — strip _PROTO_* keys // (unredacted PII-tagged values meant only for the 1P privileged column). void trackDatadogEvent(eventName, stripProtoFields(metadataWithSampleRate)) } // 1P receives the full payload including _PROTO_* — the exporter // destructures and routes those keys to proto fields itself. logEventTo1P(eventName, metadataWithSampleRate) } /** * Log an event (asynchronous implementation) * * With Segment removed the two remaining sinks are fire-and-forget, so this * just wraps the sync impl — kept to preserve the sink interface contract. */ function logEventAsyncImpl( eventName: string, metadata: LogEventMetadata, ): Promise { logEventImpl(eventName, metadata) return Promise.resolve() } /** * Initialize analytics gates during startup. * * Updates gate values from server. Early events use cached values from previous * session to avoid data loss during initialization. * * Called from main.tsx during setupBackend(). */ export function initializeAnalyticsGates(): void { isDatadogGateEnabled = checkStatsigFeatureGate_CACHED_MAY_BE_STALE(DATADOG_GATE_NAME) } /** * Initialize the analytics sink. * * Call this during app startup to attach the analytics backend. * Any events logged before this is called will be queued and drained. * * Idempotent: safe to call multiple times (subsequent calls are no-ops). */ export function initializeAnalyticsSink(): void { attachAnalyticsSink({ logEvent: logEventImpl, logEventAsync: logEventAsyncImpl, }) }