- 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
135 lines
4.2 KiB
TypeScript
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);
|
|
}
|