feat(web): add platform-service telemetry client
This commit is contained in:
parent
1fc1d6478a
commit
1713ce058b
@ -1,8 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { AuthProvider } from '@/lib/auth-context';
|
import { AuthProvider } from '@/lib/auth-context';
|
||||||
|
import { initTelemetry } from '@/lib/telemetry';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export function Providers({ children }: { children: ReactNode }) {
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
|
useEffect(() => {
|
||||||
|
initTelemetry();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <AuthProvider>{children}</AuthProvider>;
|
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