import { BatchQueue } from './batch-queue'; import type { AnalyticsConfig, BatchedEvent, ViewEventData, EngagementEventData, } from './types'; // Type declarations for browser APIs (allows compilation in Node.js environments) declare const window: { addEventListener: (event: string, handler: () => void) => void } | undefined; declare const localStorage: { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void } | undefined; export class AnalyticsClient { private config: Required; private queue: BatchQueue; private sessionId: string; constructor(config: AnalyticsConfig) { this.config = { batchSize: 10, batchInterval: 5000, enableDebugLogging: false, sessionIdKey: 'analytics_session_id', enabled: true, ...config, }; // Skip initialization if analytics is disabled if (!this.config.enabled) { this.sessionId = ''; this.queue = null as unknown as BatchQueue; if (this.config.enableDebugLogging) { console.log('[Analytics] Disabled via config'); } return; } this.sessionId = this.getOrCreateSessionId(); this.queue = new BatchQueue( this.config.batchSize, this.config.batchInterval, this.flushBatch.bind(this), this.config.enableDebugLogging, ); // Flush on page unload if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { this.flush(); }); } } trackView(data: Omit): void { if (!this.config.enabled) {return;} const event: BatchedEvent = { type: 'view', data: { ...data, app: this.config.appName, sessionId: this.sessionId, }, timestamp: Date.now(), }; this.queue.add(event); } trackEngagement(data: EngagementEventData): void { if (!this.config.enabled) {return;} const event: BatchedEvent = { type: 'engagement', data, timestamp: Date.now(), }; this.queue.add(event); } async flush(): Promise { if (!this.config.enabled) {return;} await this.queue.flush(); } destroy(): void { if (!this.config.enabled) {return;} this.queue.destroy(); } private async flushBatch(events: BatchedEvent[]): Promise { const viewEvents = events .filter((e) => e.type === 'view') .map((e) => e.data as ViewEventData); const engagementEvents = events .filter((e) => e.type === 'engagement') .map((e) => e.data as EngagementEventData); const promises: Promise[] = []; if (viewEvents.length > 0) { promises.push(this.sendViewEvents(viewEvents)); } if (engagementEvents.length > 0) { promises.push(this.sendEngagementEvents(engagementEvents)); } await Promise.all(promises); } private async sendViewEvents(events: ViewEventData[]): Promise { for (const event of events) { await fetch(`${this.config.apiBaseUrl}/analytics/track/view`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(event), }); } } private async sendEngagementEvents( events: EngagementEventData[], ): Promise { for (const event of events) { await fetch(`${this.config.apiBaseUrl}/analytics/track/engagement`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(event), credentials: 'include', }); } } private getOrCreateSessionId(): string { if (typeof window === 'undefined' || typeof localStorage === 'undefined') { return this.generateSessionId(); } const stored = localStorage.getItem(this.config.sessionIdKey); if (stored) { return stored; } const newSessionId = this.generateSessionId(); localStorage.setItem(this.config.sessionIdKey, newSessionId); return newSessionId; } private generateSessionId(): string { return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; } }