115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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,
|
|
})
|
|
}
|