refactor(dashboards): wire @bytelyst/telemetry-client into admin-web + tracker-web, add onInit + baseUrl to react-auth
This commit is contained in:
parent
b400c76c0a
commit
da165a589a
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
|
||||
@ -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
6
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user