Spaces:
Sleeping
Sleeping
| 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}); | |
| }); | |
| } | |
| } | |