learning_ai_common_plat/packages/react-native-platform-sdk/src/telemetry/index.ts
Saravana Achu Mac e174335a9e fix(rn-platform-sdk): align providers with platform-service APIs
- Feature flags: GET /flags/poll legacy { flags } + optional userId
- Kill switch: GET /settings/kill-switch, map message to reason
- Broadcasts: GET /broadcasts, POST dismiss; map server message shape
- Surveys: GET /surveys/active; submit via start/response/complete
- Auth: register(); login/register bodies include productId
- Telemetry: map queued events to TelemetryEventSchema; RN Platform import
- prepare script runs tsc on install

Made-with: Cursor
2026-03-30 01:12:18 -07:00

135 lines
4.2 KiB
TypeScript

/**
* 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<string, unknown>;
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<string, unknown>) => void;
flush: () => Promise<void>;
}
const TelemetryContext = createContext<TelemetryContextType | null>(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<TelemetryEvent[]>([]);
const sessionIdRef = useRef(randomUuid());
const installIdRef = useRef<string | null>(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<string, unknown>) => {
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);
}