398 lines
14 KiB
JavaScript
398 lines
14 KiB
JavaScript
// Advanced analytics and heatmap tracking
|
|
class AdvancedAnalytics {
|
|
constructor() {
|
|
this.sessionId = this.generateSessionId();
|
|
this.startTime = Date.now();
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.trackSession();
|
|
this.setupHeatmapTracking();
|
|
this.setupScrollTracking();
|
|
this.setupClickTracking();
|
|
this.setupTimeOnPage();
|
|
this.setupVisibilityTracking();
|
|
}
|
|
|
|
generateSessionId() {
|
|
return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
}
|
|
|
|
// Track user session
|
|
trackSession() {
|
|
const sessionData = {
|
|
sessionId: this.sessionId,
|
|
userAgent: navigator.userAgent,
|
|
screenResolution: `${screen.width}x${screen.height}`,
|
|
windowSize: `${window.innerWidth}x${window.innerHeight}`,
|
|
language: navigator.language,
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
referrer: document.referrer,
|
|
landingPage: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this.sendAnalytics('session_start', sessionData);
|
|
}
|
|
|
|
// Setup heatmap tracking
|
|
setupHeatmapTracking() {
|
|
let mousePositions = [];
|
|
let lastSampleTime = 0;
|
|
const sampleRate = 100; // Sample every 100ms
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
const now = Date.now();
|
|
if (now - lastSampleTime > sampleRate) {
|
|
mousePositions.push({
|
|
x: e.pageX,
|
|
y: e.pageY,
|
|
timestamp: now
|
|
});
|
|
lastSampleTime = now;
|
|
|
|
// Send data in batches
|
|
if (mousePositions.length >= 50) {
|
|
this.sendAnalytics('heatmap_data', {
|
|
sessionId: this.sessionId,
|
|
positions: mousePositions,
|
|
page: window.location.pathname
|
|
});
|
|
mousePositions = [];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Send remaining data on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (mousePositions.length > 0) {
|
|
navigator.sendBeacon('/api/analytics/heatmap', JSON.stringify({
|
|
sessionId: this.sessionId,
|
|
positions: mousePositions,
|
|
page: window.location.pathname
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Setup scroll depth tracking
|
|
setupScrollTracking() {
|
|
let maxScroll = 0;
|
|
const scrollMilestones = [25, 50, 75, 90, 100];
|
|
const triggered = new Set();
|
|
|
|
window.addEventListener('scroll', () => {
|
|
const scrollPercent = Math.round(
|
|
(window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight)) * 100
|
|
);
|
|
|
|
if (scrollPercent > maxScroll) {
|
|
maxScroll = scrollPercent;
|
|
}
|
|
|
|
// Track scroll milestones
|
|
scrollMilestones.forEach(milestone => {
|
|
if (scrollPercent >= milestone && !triggered.has(milestone)) {
|
|
triggered.add(milestone);
|
|
this.sendAnalytics('scroll_depth', {
|
|
sessionId: this.sessionId,
|
|
depth: milestone,
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Setup click tracking
|
|
setupClickTracking() {
|
|
document.addEventListener('click', (e) => {
|
|
const element = e.target;
|
|
const tagName = element.tagName.toLowerCase();
|
|
|
|
// Track important elements
|
|
if (['a', 'button', 'input'].includes(tagName) || element.classList.contains('trackable')) {
|
|
const clickData = {
|
|
sessionId: this.sessionId,
|
|
element: {
|
|
tagName,
|
|
id: element.id,
|
|
className: element.className,
|
|
text: element.textContent?.substring(0, 100),
|
|
href: element.href,
|
|
type: element.type
|
|
},
|
|
position: {
|
|
x: e.pageX,
|
|
y: e.pageY
|
|
},
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
this.sendAnalytics('click_tracking', clickData);
|
|
}
|
|
|
|
// Track form field clicks
|
|
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
|
this.sendAnalytics('form_interaction', {
|
|
sessionId: this.sessionId,
|
|
fieldName: element.name,
|
|
fieldType: element.type,
|
|
formId: element.closest('form')?.id,
|
|
action: 'focus',
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Setup time on page tracking
|
|
setupTimeOnPage() {
|
|
let pageStartTime = Date.now();
|
|
let isActive = true;
|
|
let totalActiveTime = 0;
|
|
let lastActiveTime = pageStartTime;
|
|
|
|
// Track when page becomes active/inactive
|
|
document.addEventListener('visibilitychange', () => {
|
|
const now = Date.now();
|
|
|
|
if (document.hidden) {
|
|
if (isActive) {
|
|
totalActiveTime += now - lastActiveTime;
|
|
isActive = false;
|
|
}
|
|
} else {
|
|
isActive = true;
|
|
lastActiveTime = now;
|
|
}
|
|
});
|
|
|
|
// Send time data periodically
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const currentActiveTime = isActive ? totalActiveTime + (now - lastActiveTime) : totalActiveTime;
|
|
|
|
this.sendAnalytics('time_on_page', {
|
|
sessionId: this.sessionId,
|
|
activeTime: Math.round(currentActiveTime / 1000), // in seconds
|
|
totalTime: Math.round((now - pageStartTime) / 1000),
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}, 30000); // Every 30 seconds
|
|
|
|
// Send final time on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
const now = Date.now();
|
|
const finalActiveTime = isActive ? totalActiveTime + (now - lastActiveTime) : totalActiveTime;
|
|
|
|
navigator.sendBeacon('/api/analytics/time', JSON.stringify({
|
|
sessionId: this.sessionId,
|
|
activeTime: Math.round(finalActiveTime / 1000),
|
|
totalTime: Math.round((now - pageStartTime) / 1000),
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
});
|
|
}
|
|
|
|
// Setup visibility tracking for elements
|
|
setupVisibilityTracking() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const element = entry.target;
|
|
this.sendAnalytics('element_view', {
|
|
sessionId: this.sessionId,
|
|
element: {
|
|
id: element.id,
|
|
className: element.className,
|
|
tagName: element.tagName.toLowerCase()
|
|
},
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
}, { threshold: 0.5 });
|
|
|
|
// Track important sections
|
|
document.querySelectorAll('section, .hero-section, .features-section, .conversion-section').forEach(el => {
|
|
observer.observe(el);
|
|
});
|
|
}
|
|
|
|
// A/B Test tracking
|
|
trackABTest(testName, variant) {
|
|
this.sendAnalytics('ab_test', {
|
|
sessionId: this.sessionId,
|
|
testName,
|
|
variant,
|
|
page: window.location.pathname,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Error tracking
|
|
trackError(error, context = {}) {
|
|
this.sendAnalytics('javascript_error', {
|
|
sessionId: this.sessionId,
|
|
error: {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
filename: error.filename,
|
|
lineno: error.lineno,
|
|
colno: error.colno
|
|
},
|
|
context,
|
|
page: window.location.pathname,
|
|
userAgent: navigator.userAgent,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Send analytics data
|
|
async sendAnalytics(event, data) {
|
|
try {
|
|
const payload = {
|
|
event,
|
|
data,
|
|
url: window.location.href,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
// Use sendBeacon for important events if available
|
|
if (navigator.sendBeacon && ['session_start', 'conversion', 'form_submit'].includes(event)) {
|
|
navigator.sendBeacon('/api/analytics/track', JSON.stringify(payload));
|
|
} else {
|
|
// Fallback to fetch
|
|
fetch('/api/analytics/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
}).catch(err => console.error('Analytics error:', err));
|
|
}
|
|
} catch (error) {
|
|
console.error('Analytics sending error:', error);
|
|
}
|
|
}
|
|
|
|
// Get session statistics
|
|
getSessionStats() {
|
|
return {
|
|
sessionId: this.sessionId,
|
|
duration: Math.round((Date.now() - this.startTime) / 1000),
|
|
page: window.location.pathname
|
|
};
|
|
}
|
|
}
|
|
|
|
// Performance monitoring
|
|
class PerformanceMonitor {
|
|
constructor() {
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Monitor page load performance
|
|
window.addEventListener('load', () => {
|
|
setTimeout(() => {
|
|
this.trackPagePerformance();
|
|
}, 0);
|
|
});
|
|
|
|
// Monitor Core Web Vitals
|
|
this.trackWebVitals();
|
|
}
|
|
|
|
trackPagePerformance() {
|
|
if ('performance' in window) {
|
|
const navigation = performance.getEntriesByType('navigation')[0];
|
|
const paint = performance.getEntriesByType('paint');
|
|
|
|
const performanceData = {
|
|
loadTime: Math.round(navigation.loadEventEnd - navigation.loadEventStart),
|
|
domContentLoaded: Math.round(navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart),
|
|
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
|
|
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
|
|
pageSize: navigation.transferSize,
|
|
resources: performance.getEntriesByType('resource').length
|
|
};
|
|
|
|
window.analytics?.sendAnalytics('page_performance', performanceData);
|
|
}
|
|
}
|
|
|
|
trackWebVitals() {
|
|
// Track Largest Contentful Paint (LCP)
|
|
if ('PerformanceObserver' in window) {
|
|
new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
const lastEntry = entries[entries.length - 1];
|
|
|
|
window.analytics?.sendAnalytics('web_vital_lcp', {
|
|
value: lastEntry.startTime,
|
|
rating: lastEntry.startTime > 2500 ? 'poor' : lastEntry.startTime > 1200 ? 'needs-improvement' : 'good'
|
|
});
|
|
}).observe({ entryTypes: ['largest-contentful-paint'] });
|
|
|
|
// Track First Input Delay (FID)
|
|
new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
entries.forEach(entry => {
|
|
window.analytics?.sendAnalytics('web_vital_fid', {
|
|
value: entry.processingStart - entry.startTime,
|
|
rating: entry.processingStart - entry.startTime > 100 ? 'poor' :
|
|
entry.processingStart - entry.startTime > 25 ? 'needs-improvement' : 'good'
|
|
});
|
|
});
|
|
}).observe({ entryTypes: ['first-input'] });
|
|
|
|
// Track Cumulative Layout Shift (CLS)
|
|
let clsScore = 0;
|
|
new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
entries.forEach(entry => {
|
|
if (!entry.hadRecentInput) {
|
|
clsScore += entry.value;
|
|
}
|
|
});
|
|
|
|
window.analytics?.sendAnalytics('web_vital_cls', {
|
|
value: clsScore,
|
|
rating: clsScore > 0.25 ? 'poor' : clsScore > 0.1 ? 'needs-improvement' : 'good'
|
|
});
|
|
}).observe({ entryTypes: ['layout-shift'] });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize analytics
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
window.analytics = new AdvancedAnalytics();
|
|
window.performanceMonitor = new PerformanceMonitor();
|
|
|
|
// Global error handling
|
|
window.addEventListener('error', (event) => {
|
|
window.analytics?.trackError(event.error, {
|
|
type: 'javascript_error',
|
|
source: event.filename,
|
|
line: event.lineno,
|
|
column: event.colno
|
|
});
|
|
});
|
|
|
|
// Promise rejection handling
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
window.analytics?.trackError(new Error(event.reason), {
|
|
type: 'unhandled_promise_rejection'
|
|
});
|
|
});
|
|
}); |