class AnalyticsTracker { constructor(options = {}) { this.endpoint = options.endpoint || '/report'; this.bufferSize = options.bufferSize || 5; this.flushInterval = options.flushInterval || 3000; this.maxRetries = options.maxRetries || 3; this.baseBackoff = options.baseBackoff || 1000; this.onSuccess = options.onSuccess || (() => {}); this.buffer = []; this.timer = null; this.isSending = false; // Auto-start timer this.startTimer(); // Initialize auto-tracking if (options.trackErrors !== false) this.initErrorTracking(); if (options.trackPerformance !== false) this.initPerformanceTracking(); } log(type, data = {}) { const event = { type, data, timestamp: Date.now(), uuid: crypto.randomUUID() }; console.log(`[Tracker] Queued event: ${type}`, event); this.buffer.push(event); if (this.buffer.length >= this.bufferSize) { this.flush(); } } startTimer() { if (this.timer) clearInterval(this.timer); this.timer = setInterval(() => { if (this.buffer.length > 0) { this.flush(); } }, this.flushInterval); } async flush() { if (this.buffer.length === 0 || this.isSending) return; const eventsToSend = [...this.buffer]; this.buffer = []; // Clear buffer immediately to allow new events this.isSending = true; this._send(eventsToSend, 0); } async _send(events, retryCount) { try { console.log(`[Tracker] Sending batch of ${events.length} events... (Attempt ${retryCount + 1})`); const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ events }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('[Tracker] Upload success:', result); this.isSending = false; // Trigger UI callback this.onSuccess(events, result); } catch (error) { console.error('[Tracker] Upload failed:', error); if (retryCount < this.maxRetries) { const backoff = this.baseBackoff * Math.pow(2, retryCount); console.log(`[Tracker] Retrying in ${backoff}ms...`); setTimeout(() => { this._send(events, retryCount + 1); }, backoff); } else { console.error('[Tracker] Max retries reached. Dropping events.'); this.isSending = false; // Optionally put them back in buffer or separate failed queue } } } // --- Specific Trackers --- trackExposure(element, data = {}) { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { this.log('exposure', { ...data, target: element.id || element.tagName }); observer.unobserve(element); // Only track once per session usually } }); }); observer.observe(element); } trackInteraction(element, eventType, data = {}) { element.addEventListener(eventType, (e) => { this.log('interaction', { eventType, target: element.id || element.tagName, ...data }); }); } initErrorTracking() { // JS Errors window.addEventListener('error', (event) => { this.log('error', { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno }); }); // Promise Rejections window.addEventListener('unhandledrejection', (event) => { this.log('error', { type: 'unhandledrejection', reason: event.reason ? event.reason.toString() : 'Unknown' }); }); } initPerformanceTracking() { window.addEventListener('load', () => { // Basic Navigation Timing const navEntry = performance.getEntriesByType('navigation')[0]; if (navEntry) { this.log('performance', { loadTime: navEntry.loadEventEnd - navEntry.startTime, domReady: navEntry.domContentLoadedEventEnd - navEntry.startTime, ttfb: navEntry.responseStart - navEntry.startTime }); } // Web Vitals (Simplified) // LCP new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { this.log('performance', { metric: 'LCP', value: entry.startTime }); } }).observe({type: 'largest-contentful-paint', buffered: true}); // CLS let clsValue = 0; new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (!entry.hadRecentInput) { clsValue += entry.value; } } // Report CLS periodically or on visibility change, here we just log updates this.log('performance', { metric: 'CLS', value: clsValue }); }).observe({type: 'layout-shift', buffered: true}); }); } }