import { ImpressionDataType, EventDataType, LastSync, HttpErrors, HttpLatencies, StreamingEvent, Method, OperationType, MethodExceptions, MethodLatencies, TelemetryUsageStatsPayload, UpdatesFromSSEEnum, UpdatesFromSSE } from '../../sync/submitters/types'; import { DEDUPED, DROPPED, LOCALHOST_MODE, QUEUED } from '../../utils/constants'; import { checkIfServerSide } from '../../utils/key'; import { findLatencyIndex } from '../findLatencyIndex'; import { ISegmentsCacheSync, ISplitsCacheSync, IStorageFactoryParams, ITelemetryCacheSync } from '../types'; const MAX_STREAMING_EVENTS = 20; const MAX_TAGS = 10; export const MAX_LATENCY_BUCKET_COUNT = 23; export function newBuckets() { // MAX_LATENCY_BUCKET_COUNT (length) is 23 // Not using Array.fill for old browsers compatibility return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } const ACCEPTANCE_RANGE = 0.001; /** * Record telemetry if mode is not localhost. * All factory instances track telemetry on server-side, and 0.1% on client-side. */ export function shouldRecordTelemetry({ settings }: IStorageFactoryParams) { return settings.mode !== LOCALHOST_MODE && (checkIfServerSide(settings) || Math.random() <= ACCEPTANCE_RANGE); } export class TelemetryCacheInMemory implements ITelemetryCacheSync { public name = 'telemetry stats'; constructor(private splits?: ISplitsCacheSync, private segments?: ISegmentsCacheSync, private largeSegments?: ISegmentsCacheSync) { } // isEmpty flag private e = true; isEmpty() { return this.e; } clear() { /* no-op */ } pop(): TelemetryUsageStatsPayload { this.e = true; return { lS: this.getLastSynchronization(), mL: this.popLatencies(), mE: this.popExceptions(), hE: this.popHttpErrors(), hL: this.popHttpLatencies(), tR: this.popTokenRefreshes(), aR: this.popAuthRejections(), iQ: this.getImpressionStats(QUEUED), iDe: this.getImpressionStats(DEDUPED), iDr: this.getImpressionStats(DROPPED), spC: this.splits && this.splits.getSplitNames().length, seC: this.segments && this.segments.getRegisteredSegments().length, skC: this.segments && this.segments.getKeysCount(), lsC: this.largeSegments && this.largeSegments.getRegisteredSegments().length, lskC: this.largeSegments && this.largeSegments.getKeysCount(), sL: this.getSessionLength(), eQ: this.getEventStats(QUEUED), eD: this.getEventStats(DROPPED), sE: this.popStreamingEvents(), t: this.popTags(), ufs: this.popUpdatesFromSSE(), }; } /** Config stats */ private timeUntilReady?: number; getTimeUntilReady() { return this.timeUntilReady; } recordTimeUntilReady(ms: number) { this.timeUntilReady = ms; } private timeUntilReadyFromCache?: number; getTimeUntilReadyFromCache() { return this.timeUntilReadyFromCache; } recordTimeUntilReadyFromCache(ms: number) { this.timeUntilReadyFromCache = ms; } private notReadyUsage = 0; getNonReadyUsage() { return this.notReadyUsage; } recordNonReadyUsage() { this.notReadyUsage++; } /** Usage stats */ private impressionStats = [0, 0, 0]; getImpressionStats(type: ImpressionDataType) { return this.impressionStats[type]; } recordImpressionStats(type: ImpressionDataType, count: number) { this.impressionStats[type] += count; this.e = false; } private eventStats = [0, 0]; getEventStats(type: EventDataType) { return this.eventStats[type]; } recordEventStats(type: EventDataType, count: number) { this.eventStats[type] += count; this.e = false; } private lastSync: LastSync = {}; getLastSynchronization() { return this.lastSync; } recordSuccessfulSync(resource: OperationType, timeMs: number) { this.lastSync[resource] = timeMs; this.e = false; } private httpErrors: HttpErrors = {}; popHttpErrors() { const result = this.httpErrors; this.httpErrors = {}; return result; } recordHttpError(resource: OperationType, status: number) { const statusErrors = (this.httpErrors[resource] = this.httpErrors[resource] || {}); statusErrors[status] = (statusErrors[status] || 0) + 1; this.e = false; } private httpLatencies: HttpLatencies = {}; popHttpLatencies() { const result = this.httpLatencies; this.httpLatencies = {}; return result; } recordHttpLatency(resource: OperationType, latencyMs: number) { const latencyBuckets = (this.httpLatencies[resource] = this.httpLatencies[resource] || newBuckets()); latencyBuckets[findLatencyIndex(latencyMs)]++; this.e = false; } private authRejections = 0; popAuthRejections() { const result = this.authRejections; this.authRejections = 0; return result; } recordAuthRejections() { this.authRejections++; this.e = false; } private tokenRefreshes = 0; popTokenRefreshes() { const result = this.tokenRefreshes; this.tokenRefreshes = 0; return result; } recordTokenRefreshes() { this.tokenRefreshes++; this.e = false; } private streamingEvents: StreamingEvent[] = []; popStreamingEvents() { return this.streamingEvents.splice(0); } recordStreamingEvents(streamingEvent: StreamingEvent) { if (this.streamingEvents.length < MAX_STREAMING_EVENTS) { this.streamingEvents.push(streamingEvent); } this.e = false; } private tags: string[] = []; popTags() { return this.tags.splice(0); } addTag(tag: string) { if (this.tags.length < MAX_TAGS) { this.tags.push(tag); } this.e = false; } private sessionLength?: number; getSessionLength() { return this.sessionLength; } recordSessionLength(ms: number) { this.sessionLength = ms; this.e = false; } private exceptions: MethodExceptions = {}; popExceptions() { const result = this.exceptions; this.exceptions = {}; return result; } recordException(method: Method) { this.exceptions[method] = (this.exceptions[method] || 0) + 1; this.e = false; } private latencies: MethodLatencies = {}; popLatencies() { const result = this.latencies; this.latencies = {}; return result; } recordLatency(method: Method, latencyMs: number) { const latencyBuckets = (this.latencies[method] = this.latencies[method] || newBuckets()); latencyBuckets[findLatencyIndex(latencyMs)]++; this.e = false; } private updatesFromSSE: UpdatesFromSSE = {}; popUpdatesFromSSE() { const result = this.updatesFromSSE; this.updatesFromSSE = {}; return result; } recordUpdatesFromSSE(type: UpdatesFromSSEEnum) { this.updatesFromSSE[type] = (this.updatesFromSSE[type] || 0) + 1; this.e = false; } }