From da165a589ada84fe4d07238c1a3c78d26bdc231b Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 28 Feb 2026 11:27:57 -0800 Subject: [PATCH] refactor(dashboards): wire @bytelyst/telemetry-client into admin-web + tracker-web, add onInit + baseUrl to react-auth --- dashboards/admin-web/package.json | 1 + dashboards/admin-web/src/lib/telemetry.ts | 144 ++++---------------- dashboards/tracker-web/package.json | 1 + dashboards/tracker-web/src/lib/telemetry.ts | 142 ++++--------------- packages/react-auth/src/auth-context.tsx | 16 ++- packages/react-auth/src/types.ts | 4 + pnpm-lock.yaml | 6 + 7 files changed, 72 insertions(+), 242 deletions(-) diff --git a/dashboards/admin-web/package.json b/dashboards/admin-web/package.json index 49326608..639dbb78 100644 --- a/dashboards/admin-web/package.json +++ b/dashboards/admin-web/package.json @@ -34,6 +34,7 @@ "@bytelyst/extraction": "workspace:*", "@bytelyst/logger": "workspace:*", "@bytelyst/react-auth": "workspace:*", + "@bytelyst/telemetry-client": "workspace:*", "@radix-ui/react-slider": "^1.3.6", "@tailwindcss/typography": "^0.5.19", "bcryptjs": "^3.0.3", diff --git a/dashboards/admin-web/src/lib/telemetry.ts b/dashboards/admin-web/src/lib/telemetry.ts index e81e69a3..36c19bb9 100644 --- a/dashboards/admin-web/src/lib/telemetry.ts +++ b/dashboards/admin-web/src/lib/telemetry.ts @@ -1,72 +1,34 @@ /** * Client-side self-telemetry for the admin dashboard. * - * Tracks admin page views, filter usage, and interaction events. - * Sends to platform-service via the admin dashboard's /api/telemetry/admin-ingest - * proxy (separate from the admin telemetry query route at /api/telemetry). + * Delegates to @bytelyst/telemetry-client shared package. + * Sends to platform-service via /api/telemetry/admin-ingest proxy. * * Privacy: No PII. Only page paths, action names, and timing metrics. * See docs/WINDSURF/CLIENT_TELEMETRY_DESIGN.md */ -// Product ID resolved from env var set by the deploying product. +import { createTelemetryClient, type TelemetryClient } from '@bytelyst/telemetry-client'; + const PRODUCT_ID = process.env.NEXT_PUBLIC_PRODUCT_ID || 'unknown'; -const PLATFORM = 'web'; -const OS_FAMILY = 'other'; // Zod OsFamilyEnum doesn't include 'web'; use 'other' -const CHANNEL = 'web_app'; -const MAX_QUEUE = 50; -const FLUSH_INTERVAL_MS = 30_000; -interface TelemetryEvent { - id: string; - productId: string; - userId?: 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; - metrics?: Record; - occurredAt: string; -} +let _client: TelemetryClient | null = null; -let queue: TelemetryEvent[] = []; -let sessionId = crypto.randomUUID(); -let installId = ''; -let flushTimer: ReturnType | null = null; - -function getInstallId(): string { - if (installId) return installId; - try { - const stored = localStorage.getItem(`${PRODUCT_ID}_admin_telemetry_install_id`); - if (stored) { - installId = stored; - return installId; - } - installId = crypto.randomUUID(); - localStorage.setItem(`${PRODUCT_ID}_admin_telemetry_install_id`, installId); - } catch { - installId = crypto.randomUUID(); - } - return installId; -} - -function getUserAgent(): string { - try { - return navigator.userAgent; - } catch { - return 'unknown'; +function getClient(): TelemetryClient { + if (!_client) { + _client = createTelemetryClient({ + productId: PRODUCT_ID, + baseUrl: '', + endpoint: '/api/telemetry/admin-ingest', + platform: 'web', + channel: 'web_app', + transport: 'beacon', + appVersion: '0.0.0', + buildNumber: '0', + releaseChannel: 'beta', + }); } + return _client; } export function trackEvent( @@ -81,34 +43,7 @@ export function trackEvent( } ): void { if (typeof window === 'undefined') return; - - const event: TelemetryEvent = { - id: crypto.randomUUID(), - productId: PRODUCT_ID, - anonymousInstallId: getInstallId(), - sessionId, - platform: PLATFORM, - channel: CHANNEL, - osFamily: OS_FAMILY, - osVersion: getUserAgent().substring(0, 128), - appVersion: '0.0.0', - buildNumber: '0', - 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(); - } + getClient().trackEvent(eventType, module, eventName, options); } export function trackPageView(path: string): void { @@ -118,44 +53,11 @@ export function trackPageView(path: string): void { } export function flush(): void { - if (typeof window === 'undefined' || queue.length === 0) return; - - const events = [...queue]; - queue = []; - - // Use sendBeacon for reliability (works during page unload) - const body = JSON.stringify({ productId: PRODUCT_ID, events }); - try { - const sent = navigator.sendBeacon('/api/telemetry/admin-ingest', body); - if (!sent) { - // Fallback to fetch - fetch('/api/telemetry/admin-ingest', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body, - keepalive: true, - }).catch(() => {}); - } - } catch { - // Best effort - } + if (typeof window === 'undefined') return; + getClient().flush(); } export function initTelemetry(): void { if (typeof window === 'undefined') return; - - sessionId = crypto.randomUUID(); - - // Flush on page unload - window.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - flush(); - } - }); - - // Periodic flush - if (flushTimer) clearInterval(flushTimer); - flushTimer = setInterval(flush, FLUSH_INTERVAL_MS); - - trackEvent('info', 'app_lifecycle', 'session_started'); + getClient().init(); } diff --git a/dashboards/tracker-web/package.json b/dashboards/tracker-web/package.json index 9f8b43cb..4bab08c4 100644 --- a/dashboards/tracker-web/package.json +++ b/dashboards/tracker-web/package.json @@ -28,6 +28,7 @@ "@bytelyst/api-client": "workspace:*", "@bytelyst/config": "workspace:*", "@bytelyst/errors": "workspace:*", + "@bytelyst/telemetry-client": "workspace:*", "@bytelyst/logger": "workspace:*", "clsx": "^2.1.1", "next": "16.1.6", diff --git a/dashboards/tracker-web/src/lib/telemetry.ts b/dashboards/tracker-web/src/lib/telemetry.ts index 69b14bfd..299465e1 100644 --- a/dashboards/tracker-web/src/lib/telemetry.ts +++ b/dashboards/tracker-web/src/lib/telemetry.ts @@ -1,72 +1,36 @@ /** * Client-side telemetry for the tracker dashboard. * - * Sends lightweight page-view and interaction events to platform-service - * via the tracker dashboard's /api/telemetry/ingest proxy. Runs in the browser only. + * Delegates to @bytelyst/telemetry-client shared package. + * Sends events via /api/telemetry/ingest proxy. * * Privacy: No PII. Only page paths, action names, and timing metrics. * See docs/WINDSURF/CLIENT_TELEMETRY_DESIGN.md */ +import { createTelemetryClient, type TelemetryClient } from '@bytelyst/telemetry-client'; + // Product ID resolved from env var set by the deploying product. // Each product sets NEXT_PUBLIC_PRODUCT_ID in its .env (e.g. 'lysnrai', 'chronomind', 'nomgap'). const PRODUCT_ID = process.env.NEXT_PUBLIC_PRODUCT_ID || 'unknown'; -const PLATFORM = 'web'; -const OS_FAMILY = 'other'; // Zod OsFamilyEnum doesn't include 'web'; use 'other' -const CHANNEL = 'web_app'; -const MAX_QUEUE = 50; -const FLUSH_INTERVAL_MS = 30_000; -interface TelemetryEvent { - id: string; - productId: string; - userId?: 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; - metrics?: Record; - occurredAt: string; -} +let _client: TelemetryClient | null = null; -let queue: TelemetryEvent[] = []; -let sessionId = crypto.randomUUID(); -let installId = ''; -let flushTimer: ReturnType | null = null; - -function getInstallId(): string { - if (installId) return installId; - try { - const stored = localStorage.getItem(`${PRODUCT_ID}_telemetry_install_id`); - if (stored) { - installId = stored; - return installId; - } - installId = crypto.randomUUID(); - localStorage.setItem(`${PRODUCT_ID}_telemetry_install_id`, installId); - } catch { - installId = crypto.randomUUID(); - } - return installId; -} - -function getUserAgent(): string { - try { - return navigator.userAgent; - } catch { - return 'unknown'; +function getClient(): TelemetryClient { + if (!_client) { + _client = createTelemetryClient({ + productId: PRODUCT_ID, + baseUrl: '', + endpoint: '/api/telemetry/ingest', + platform: 'web', + channel: 'web_app', + transport: 'beacon', + appVersion: '0.0.0', + buildNumber: '0', + releaseChannel: 'beta', + }); } + return _client; } export function trackEvent( @@ -81,34 +45,7 @@ export function trackEvent( } ): void { if (typeof window === 'undefined') return; - - const event: TelemetryEvent = { - id: crypto.randomUUID(), - productId: PRODUCT_ID, - anonymousInstallId: getInstallId(), - sessionId, - platform: PLATFORM, - channel: CHANNEL, - osFamily: OS_FAMILY, - osVersion: getUserAgent().substring(0, 128), - appVersion: '0.0.0', - buildNumber: '0', - 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(); - } + getClient().trackEvent(eventType, module, eventName, options); } export function trackPageView(path: string): void { @@ -118,44 +55,11 @@ export function trackPageView(path: string): void { } export function flush(): void { - if (typeof window === 'undefined' || queue.length === 0) return; - - const events = [...queue]; - queue = []; - - // Use sendBeacon for reliability (works during page unload) - const body = JSON.stringify({ productId: PRODUCT_ID, events }); - try { - const sent = navigator.sendBeacon('/api/telemetry/ingest', body); - if (!sent) { - // Fallback to fetch - fetch('/api/telemetry/ingest', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body, - keepalive: true, - }).catch(() => {}); - } - } catch { - // Best effort - } + if (typeof window === 'undefined') return; + getClient().flush(); } export function initTelemetry(): void { if (typeof window === 'undefined') return; - - sessionId = crypto.randomUUID(); - - // Flush on page unload - window.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - flush(); - } - }); - - // Periodic flush - if (flushTimer) clearInterval(flushTimer); - flushTimer = setInterval(flush, FLUSH_INTERVAL_MS); - - trackEvent('info', 'app_lifecycle', 'session_started'); + getClient().init(); } diff --git a/packages/react-auth/src/auth-context.tsx b/packages/react-auth/src/auth-context.tsx index 5ccf4f66..51ab5ac9 100644 --- a/packages/react-auth/src/auth-context.tsx +++ b/packages/react-auth/src/auth-context.tsx @@ -38,6 +38,7 @@ import type { AuthConfig, AuthContextValue, BaseUser } from './types.js'; */ export function createAuthProvider(config: AuthConfig) { const { + baseUrl: configBaseUrl = '/api', storagePrefix, loginEndpoint, registerEndpoint, @@ -48,6 +49,7 @@ export function createAuthProvider(config: Au refreshIntervalMs = 45 * 60 * 1000, mapLoginResponse, onLoginFallback, + onInit, onLogout, } = config; @@ -80,14 +82,24 @@ export function createAuthProvider(config: Au } function AuthProvider({ children }: { children: ReactNode }) { - const [user, setUser] = useState(getStoredUser); + const [user, setUser] = useState(() => { + // Allow onInit to provide an initial session (e.g. from SSO cookies) + if (onInit) { + const initResult = onInit(); + if (initResult) { + saveSession(initResult.user, initResult.accessToken, initResult.refreshToken); + return initResult.user; + } + } + return getStoredUser(); + }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const refreshTimerRef = useRef | null>(null); const api = createApiClient({ - baseUrl: '/api', + baseUrl: configBaseUrl, getToken: () => (typeof window !== 'undefined' ? localStorage.getItem(TOKEN_KEY) : null), }); diff --git a/packages/react-auth/src/types.ts b/packages/react-auth/src/types.ts index 04bbac40..f1e26744 100644 --- a/packages/react-auth/src/types.ts +++ b/packages/react-auth/src/types.ts @@ -27,6 +27,8 @@ export interface LoginResult { } export interface AuthConfig { + /** Base URL for auth API calls. Default: '/api'. */ + baseUrl?: string; storagePrefix: string; loginEndpoint: string; registerEndpoint?: string; @@ -42,5 +44,7 @@ export interface AuthConfig { password: string, error: string ) => Promise | null>; + /** Called once on mount to provide an initial session (e.g. from SSO cookies). Return null to fall through to localStorage. */ + onInit?: () => LoginResult | null; onLogout?: () => void; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9aded40..9ea7d109 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: '@bytelyst/react-auth': specifier: workspace:* version: link:../../packages/react-auth + '@bytelyst/telemetry-client': + specifier: workspace:* + version: link:../../packages/telemetry-client '@radix-ui/react-slider': specifier: ^1.3.6 version: 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -225,6 +228,9 @@ importers: '@bytelyst/logger': specifier: workspace:* version: link:../../packages/logger + '@bytelyst/telemetry-client': + specifier: workspace:* + version: link:../../packages/telemetry-client clsx: specifier: ^2.1.1 version: 2.1.1