/** * Telemetry module — React context + hook for event tracking in React Native apps. * Maps queued events to platform-service TelemetryEventSchema for POST /telemetry/events. */ import React, { createContext, useContext, useCallback, useRef, useEffect } from 'react'; import { Platform } from 'react-native'; import type { PlatformSDK } from '../core.js'; export interface TelemetryEvent { name: string; properties?: Record; timestamp?: string; } export interface TelemetryConfig { /** Flush interval in ms (default: 30000) */ flushInterval?: number; /** Max batch size before auto-flush (default: 20) */ maxBatchSize?: number; appVersion?: string; buildNumber?: string; /** Default: beta */ releaseChannel?: 'dev' | 'beta' | 'prod'; /** Stable anonymous id (e.g. from MMKV). If omitted, generated per app session. */ getInstallId?: () => string; } function randomUuid(): string { if (typeof globalThis.crypto?.randomUUID === 'function') { return globalThis.crypto.randomUUID(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); } interface TelemetryContextType { track: (name: string, properties?: Record) => void; flush: () => Promise; } const TelemetryContext = createContext(null); export function useTelemetry(): TelemetryContextType { const ctx = useContext(TelemetryContext); if (!ctx) throw new Error('useTelemetry must be used within a TelemetryProvider'); return ctx; } interface TelemetryProviderProps { sdk: PlatformSDK; config?: TelemetryConfig; children: React.ReactNode; } export function TelemetryProvider({ sdk, config, children, }: TelemetryProviderProps): React.JSX.Element { const queue = useRef([]); const sessionIdRef = useRef(randomUuid()); const installIdRef = useRef(null); const flushInterval = config?.flushInterval ?? 30_000; const maxBatchSize = config?.maxBatchSize ?? 20; const resolveInstallId = useCallback((): string => { if (!installIdRef.current) { installIdRef.current = config?.getInstallId?.() ?? randomUuid(); } return installIdRef.current; }, [config?.getInstallId]); const flush = useCallback(async () => { if (queue.current.length === 0) return; const batch = queue.current.splice(0); const os = Platform.OS; const platform = os === 'ios' ? 'ios' : os === 'android' ? 'android' : 'web'; const osFamily = platform === 'ios' ? 'ios' : platform === 'android' ? 'android' : 'other'; const events = batch.map(e => ({ id: randomUuid(), productId: sdk.config.productId, anonymousInstallId: resolveInstallId(), sessionId: sessionIdRef.current, platform, channel: 'mobile_app' as const, osFamily, osVersion: String(Platform.Version ?? ''), appVersion: config?.appVersion ?? '0.0.0', buildNumber: config?.buildNumber ?? '0', releaseChannel: config?.releaseChannel ?? ('beta' as const), eventType: 'info' as const, module: 'app', eventName: e.name, occurredAt: e.timestamp ?? new Date().toISOString(), context: e.properties, })); try { await sdk.fetch('/telemetry/events', { method: 'POST', body: JSON.stringify({ productId: sdk.config.productId, events }), }); } catch { queue.current.unshift(...batch); } }, [sdk, config?.appVersion, config?.buildNumber, config?.releaseChannel, resolveInstallId]); const track = useCallback( (name: string, properties?: Record) => { queue.current.push({ name, properties, timestamp: new Date().toISOString(), }); if (queue.current.length >= maxBatchSize) { flush(); } }, [maxBatchSize, flush] ); useEffect(() => { const id = setInterval(flush, flushInterval); return () => { clearInterval(id); flush(); }; }, [flush, flushInterval]); const value: TelemetryContextType = { track, flush }; return React.createElement(TelemetryContext.Provider, { value }, children); }