import { BatchQueue } from './batch-queue'; import type { AnalyticsConfig, BatchedEvent, ViewEventData, EngagementEventData, InteractionEvent, InteractionEventPayload, } 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; private interactionQueue: InteractionEventPayload[] = []; private interactionFlushTimer: ReturnType | null = null; 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(); }); } // Set up periodic interaction flush this.interactionFlushTimer = setInterval(() => { this.flushInteractions(); }, this.config.batchInterval); } 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); } trackInteraction(event: InteractionEvent): void { if (!this.config.enabled) {return;} const payload: InteractionEventPayload = { type: event.type, data: event.data, sessionId: this.sessionId, timestamp: Date.now(), }; this.interactionQueue.push(payload); // Flush if batch size reached if (this.interactionQueue.length >= this.config.batchSize) { this.flushInteractions(); } } async flush(): Promise { if (!this.config.enabled) {return;} await Promise.all([ this.queue.flush(), this.flushInteractions(), ]); } destroy(): void { if (!this.config.enabled) {return;} this.queue.destroy(); if (this.interactionFlushTimer) { clearInterval(this.interactionFlushTimer); this.interactionFlushTimer = null; } } private async flushInteractions(): Promise { if (this.interactionQueue.length === 0) {return;} const events = [...this.interactionQueue]; this.interactionQueue = []; try { await fetch(`${this.config.apiBaseUrl}/analytics/track/interaction`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ events }), credentials: 'include', }); if (this.config.enableDebugLogging) { console.log(`[Analytics] Flushed ${events.length} interaction events`); } } catch (error) { // Re-queue on failure this.interactionQueue = [...events, ...this.interactionQueue]; if (this.config.enableDebugLogging) { console.error('[Analytics] Failed to flush interactions:', error); } } } 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)}`; } }