feat(web): add platform-service telemetry client
This commit is contained in:
parent
1fc1d6478a
commit
1713ce058b
@ -1,8 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { AuthProvider } from '@/lib/auth-context';
|
||||
import { initTelemetry } from '@/lib/telemetry';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
useEffect(() => {
|
||||
initTelemetry();
|
||||
}, []);
|
||||
|
||||
return <AuthProvider>{children}</AuthProvider>;
|
||||
}
|
||||
|
||||
155
web/src/lib/telemetry.ts
Normal file
155
web/src/lib/telemetry.ts
Normal file
@ -0,0 +1,155 @@
|
||||
// ── Platform Telemetry Client ─────────────────────────────────
|
||||
// Sends lightweight events to platform-service telemetry endpoint.
|
||||
// Runs in the browser only. Privacy: no PII, only action names + timing.
|
||||
|
||||
import { PRODUCT_ID } from './platform-sync';
|
||||
|
||||
const PLATFORM = 'web';
|
||||
const OS_FAMILY = 'other';
|
||||
const CHANNEL = 'pwa';
|
||||
const MAX_QUEUE = 50;
|
||||
const FLUSH_INTERVAL_MS = 30_000;
|
||||
|
||||
interface TelemetryEvent {
|
||||
id: string;
|
||||
productId: string;
|
||||
anonymousInstallId: string;
|
||||
sessionId: string;
|
||||
platform: string;
|
||||
channel: string;
|
||||
osFamily: string;
|
||||
osVersion: string;
|
||||
appVersion: string;
|
||||
buildNumber: string;
|
||||
releaseChannel: string;
|
||||
eventType: string;
|
||||
module: string;
|
||||
eventName: string;
|
||||
feature?: string;
|
||||
message?: string;
|
||||
tags?: Record<string, string>;
|
||||
metrics?: Record<string, number>;
|
||||
occurredAt: string;
|
||||
}
|
||||
|
||||
let queue: TelemetryEvent[] = [];
|
||||
let sessionId = '';
|
||||
let installId = '';
|
||||
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let baseUrl = '';
|
||||
|
||||
function uuid(): string {
|
||||
if (typeof crypto?.randomUUID === 'function') return 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);
|
||||
});
|
||||
}
|
||||
|
||||
function getInstallId(): string {
|
||||
if (installId) return installId;
|
||||
try {
|
||||
const stored = localStorage.getItem('chronomind_telemetry_install_id');
|
||||
if (stored) {
|
||||
installId = stored;
|
||||
return installId;
|
||||
}
|
||||
installId = uuid();
|
||||
localStorage.setItem('chronomind_telemetry_install_id', installId);
|
||||
} catch {
|
||||
installId = uuid();
|
||||
}
|
||||
return installId;
|
||||
}
|
||||
|
||||
export function trackEvent(
|
||||
eventType: 'debug' | 'info' | 'warn' | 'error',
|
||||
module: string,
|
||||
eventName: string,
|
||||
options?: {
|
||||
feature?: string;
|
||||
message?: string;
|
||||
tags?: Record<string, string>;
|
||||
metrics?: Record<string, number>;
|
||||
}
|
||||
): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const event: TelemetryEvent = {
|
||||
id: uuid(),
|
||||
productId: PRODUCT_ID,
|
||||
anonymousInstallId: getInstallId(),
|
||||
sessionId,
|
||||
platform: PLATFORM,
|
||||
channel: CHANNEL,
|
||||
osFamily: OS_FAMILY,
|
||||
osVersion: navigator.userAgent.substring(0, 128),
|
||||
appVersion: '0.1.0',
|
||||
buildNumber: '1',
|
||||
releaseChannel: 'beta',
|
||||
eventType,
|
||||
module,
|
||||
eventName,
|
||||
occurredAt: new Date().toISOString(),
|
||||
...options,
|
||||
};
|
||||
|
||||
queue.push(event);
|
||||
if (queue.length > MAX_QUEUE) {
|
||||
queue = queue.slice(-MAX_QUEUE);
|
||||
}
|
||||
|
||||
if (queue.length >= 10) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function trackTimerEvent(eventName: string, tags?: Record<string, string>, metrics?: Record<string, number>): void {
|
||||
trackEvent('info', 'timers', eventName, { tags, metrics });
|
||||
}
|
||||
|
||||
export function trackPageView(path: string): void {
|
||||
trackEvent('info', 'navigation', 'page_view', { tags: { path } });
|
||||
}
|
||||
|
||||
export function flush(): void {
|
||||
if (typeof window === 'undefined' || queue.length === 0 || !baseUrl) return;
|
||||
|
||||
const events = [...queue];
|
||||
queue = [];
|
||||
|
||||
const body = JSON.stringify({ productId: PRODUCT_ID, events });
|
||||
const url = `${baseUrl}/telemetry/events`;
|
||||
|
||||
try {
|
||||
const sent = navigator.sendBeacon(url, body);
|
||||
if (!sent) {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// Best effort — telemetry is non-critical
|
||||
}
|
||||
}
|
||||
|
||||
export function initTelemetry(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
baseUrl = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
|
||||
sessionId = uuid();
|
||||
|
||||
window.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
flush();
|
||||
}
|
||||
});
|
||||
|
||||
if (flushTimer) clearInterval(flushTimer);
|
||||
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
|
||||
|
||||
trackEvent('info', 'app_lifecycle', 'session_started');
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user