tracking-sdk / static /tracker.js
duqing2026's picture
init
cdf3420
Raw
History Blame Contribute Delete
5.92 kB
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});
});
}
}