learning_ai_common_plat/packages/telemetry-client/src/client.ts
saravanakumardb1 b400c76c0a feat(packages): add @bytelyst/auth-client + telemetry-client, extend react-auth lifecycle
- @bytelyst/auth-client: browser/RN-safe auth API wrapper (17 tests)
- @bytelyst/telemetry-client: shared telemetry with configurable transport (11 tests)
- @bytelyst/react-auth: add register, forgotPw, changePw, deleteAccount, token refresh (10 tests)
- eslint.config: add missing browser globals
2026-02-28 04:49:46 -08:00

237 lines
5.6 KiB
TypeScript

/**
* Browser/React Native-safe telemetry client for platform-service.
*
* Replaces hand-rolled telemetry clients in ChronoMind web, NomGap, and LysnrAI user-dashboard.
* No Node.js dependencies — uses globalThis.fetch and configurable storage.
*
* @example
* ```ts
* import { createTelemetryClient } from '@bytelyst/telemetry-client';
*
* const telemetry = createTelemetryClient({
* productId: 'chronomind',
* baseUrl: 'http://localhost:4003/api',
* platform: 'web',
* channel: 'pwa',
* transport: 'beacon',
* });
*
* telemetry.init();
* telemetry.trackEvent('info', 'timer', 'timer_created');
* ```
*/
import type {
TelemetryClient,
TelemetryClientConfig,
TelemetryEvent,
TelemetryStorage,
} from './types.js';
// ── UUID helper (browser + RN safe) ──────────────────────────────
function uuid(): 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);
});
}
// ── Noop storage ─────────────────────────────────────────────────
const noopStorage: TelemetryStorage = {
getItem: () => null,
setItem: () => {},
};
function getDefaultStorage(): TelemetryStorage {
if (
typeof globalThis.localStorage !== 'undefined' &&
typeof globalThis.localStorage?.getItem === 'function'
) {
return globalThis.localStorage;
}
return noopStorage;
}
// ── Factory ──────────────────────────────────────────────────────
export function createTelemetryClient(config: TelemetryClientConfig): TelemetryClient {
const {
productId,
baseUrl,
endpoint = '/telemetry/events',
platform,
channel,
transport = 'fetch',
maxQueue = 50,
flushIntervalMs = 30_000,
appVersion = '0.0.0',
buildNumber = '0',
releaseChannel = 'dev',
osFamily = 'other',
osVersion = '',
} = config;
const storage = config.storage ?? getDefaultStorage();
const INSTALL_KEY = `${productId}_telemetry_install_id`;
let queue: TelemetryEvent[] = [];
let sessionId = '';
let installId = '';
let flushTimer: ReturnType<typeof setInterval> | null = null;
function getInstallId(): string {
if (installId) return installId;
const stored = storage.getItem(INSTALL_KEY);
if (stored) {
installId = stored;
return installId;
}
installId = uuid();
storage.setItem(INSTALL_KEY, installId);
return installId;
}
function getSessionId(): string {
return sessionId;
}
function flushViaBeacon(): void {
if (queue.length === 0) return;
const events = [...queue];
queue = [];
const body = JSON.stringify({ productId, events });
const url = `${baseUrl}${endpoint}`;
try {
const sent = typeof navigator?.sendBeacon === 'function' && navigator.sendBeacon(url, body);
if (!sent) {
// Fallback to fetch
globalThis
.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-product-id': productId,
'x-request-id': uuid(),
},
body,
keepalive: true,
})
.catch(() => {});
}
} catch {
// Silently ignore telemetry failures
}
}
function flushViaFetch(): void {
if (queue.length === 0) return;
const events = [...queue];
queue = [];
const body = JSON.stringify({ productId, events });
const url = `${baseUrl}${endpoint}`;
globalThis
.fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-product-id': productId,
'x-request-id': uuid(),
},
body,
})
.catch(() => {});
}
function flush(): void {
if (transport === 'beacon') {
flushViaBeacon();
} else {
flushViaFetch();
}
}
function trackEvent(
eventType: string,
module: string,
eventName: string,
extra?: {
feature?: string;
message?: string;
tags?: Record<string, string>;
metrics?: Record<string, number>;
userId?: string;
}
): void {
const event: TelemetryEvent = {
id: uuid(),
productId,
anonymousInstallId: getInstallId(),
sessionId,
platform,
channel,
osFamily,
osVersion,
appVersion,
buildNumber,
releaseChannel,
eventType,
module,
eventName,
...extra,
occurredAt: new Date().toISOString(),
};
queue.push(event);
if (queue.length >= maxQueue) {
flush();
}
}
function init(): void {
sessionId = uuid();
getInstallId();
// Auto-flush on visibility change (web only)
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flush();
}
});
}
// Periodic flush
if (flushTimer) clearInterval(flushTimer);
flushTimer = setInterval(flush, flushIntervalMs);
trackEvent('info', 'app_lifecycle', 'session_started');
}
function shutdown(): void {
flush();
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
}
return {
init,
trackEvent,
flush,
shutdown,
getInstallId,
getSessionId,
};
}