refactor(dashboards): wire @bytelyst/telemetry-client into admin-web + tracker-web, add onInit + baseUrl to react-auth

This commit is contained in:
saravanakumardb1 2026-02-28 11:27:57 -08:00
parent b400c76c0a
commit da165a589a
7 changed files with 72 additions and 242 deletions

View File

@ -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",

View File

@ -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<string, string>;
metrics?: Record<string, number>;
occurredAt: string;
}
let _client: TelemetryClient | null = null;
let queue: TelemetryEvent[] = [];
let sessionId = crypto.randomUUID();
let installId = '';
let flushTimer: ReturnType<typeof setInterval> | 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();
}

View File

@ -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",

View File

@ -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<string, string>;
metrics?: Record<string, number>;
occurredAt: string;
}
let _client: TelemetryClient | null = null;
let queue: TelemetryEvent[] = [];
let sessionId = crypto.randomUUID();
let installId = '';
let flushTimer: ReturnType<typeof setInterval> | 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();
}

View File

@ -38,6 +38,7 @@ import type { AuthConfig, AuthContextValue, BaseUser } from './types.js';
*/
export function createAuthProvider<TUser extends BaseUser = BaseUser>(config: AuthConfig<TUser>) {
const {
baseUrl: configBaseUrl = '/api',
storagePrefix,
loginEndpoint,
registerEndpoint,
@ -48,6 +49,7 @@ export function createAuthProvider<TUser extends BaseUser = BaseUser>(config: Au
refreshIntervalMs = 45 * 60 * 1000,
mapLoginResponse,
onLoginFallback,
onInit,
onLogout,
} = config;
@ -80,14 +82,24 @@ export function createAuthProvider<TUser extends BaseUser = BaseUser>(config: Au
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<TUser | null>(getStoredUser);
const [user, setUser] = useState<TUser | null>(() => {
// 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<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const refreshTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const api = createApiClient({
baseUrl: '/api',
baseUrl: configBaseUrl,
getToken: () => (typeof window !== 'undefined' ? localStorage.getItem(TOKEN_KEY) : null),
});

View File

@ -27,6 +27,8 @@ export interface LoginResult<TUser extends BaseUser = BaseUser> {
}
export interface AuthConfig<TUser extends BaseUser = BaseUser> {
/** Base URL for auth API calls. Default: '/api'. */
baseUrl?: string;
storagePrefix: string;
loginEndpoint: string;
registerEndpoint?: string;
@ -42,5 +44,7 @@ export interface AuthConfig<TUser extends BaseUser = BaseUser> {
password: string,
error: string
) => Promise<LoginResult<TUser> | null>;
/** Called once on mount to provide an initial session (e.g. from SSO cookies). Return null to fall through to localStorage. */
onInit?: () => LoginResult<TUser> | null;
onLogout?: () => void;
}

6
pnpm-lock.yaml generated
View File

@ -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