3e4cfcf683
- Create detailed event tracking system (views, watch time, retention, traffic) - Build channel analytics with daily aggregation - Implement real-time stats caching - Add traffic source detection and tracking - Create audience demographics and retention tracking - Build creator dashboard with Chart.js visualizations - Implement automatic video analytics tracker JavaScript - Add cron script for daily stats aggregation - Document complete analytics system Features: - Channel overview (views, watch time, subscribers, engagement) - Time series charts with multiple metrics - Traffic sources breakdown (search, social, direct, etc.) - Audience demographics (countries, devices, age ranges) - Top videos by performance metric - Audience retention graphs (minute-by-minute) - Real-time viewer statistics - Automatic tracking (no manual instrumentation) - Privacy-compliant (hashed IPs, GDPR-ready) - Production-ready with proper indexing
233 lines
7.8 KiB
JavaScript
233 lines
7.8 KiB
JavaScript
/**
|
|
* Video Analytics Tracker
|
|
* Tracks views, watch time, and retention automatically
|
|
*/
|
|
|
|
class VideoAnalyticsTracker {
|
|
constructor(videoId, options = {}) {
|
|
this.videoId = videoId;
|
|
this.playerElement = options.playerElement || document.querySelector('video');
|
|
this.trackingInterval = options.trackingInterval || 10000; // 10 seconds
|
|
|
|
this.startTime = Date.now();
|
|
this.lastTrackedSecond = 0;
|
|
this.watchedMinutes = new Set();
|
|
this.hasTrackedView = false;
|
|
this.isPlaying = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
if (!this.playerElement) {
|
|
console.warn('Video player element not found');
|
|
return;
|
|
}
|
|
|
|
this.attachEventListeners();
|
|
this.startPeriodicTracking();
|
|
this.detectTrafficSource();
|
|
}
|
|
|
|
attachEventListeners() {
|
|
// Track when video starts playing
|
|
this.playerElement.addEventListener('play', () => {
|
|
this.isPlaying = true;
|
|
|
|
if (!this.hasTrackedView) {
|
|
this.trackView();
|
|
this.hasTrackedView = true;
|
|
}
|
|
});
|
|
|
|
// Track when video pauses
|
|
this.playerElement.addEventListener('pause', () => {
|
|
this.isPlaying = false;
|
|
this.trackProgress();
|
|
});
|
|
|
|
// Track when video ends
|
|
this.playerElement.addEventListener('ended', () => {
|
|
this.isPlaying = false;
|
|
this.trackCompletion();
|
|
});
|
|
|
|
// Track time updates (for retention)
|
|
this.playerElement.addEventListener('timeupdate', () => {
|
|
this.trackRetention();
|
|
});
|
|
|
|
// Track when user leaves page
|
|
window.addEventListener('beforeunload', () => {
|
|
this.trackProgress();
|
|
});
|
|
}
|
|
|
|
async trackView() {
|
|
try {
|
|
await fetch('/api/analytics.php?action=track', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
video_id: this.videoId,
|
|
event_type: 'view',
|
|
source_type: this.trafficSource?.type,
|
|
source_detail: this.trafficSource?.detail
|
|
})
|
|
});
|
|
|
|
console.log('View tracked');
|
|
} catch (error) {
|
|
console.error('Failed to track view:', error);
|
|
}
|
|
}
|
|
|
|
async trackProgress() {
|
|
const currentTime = this.playerElement.currentTime;
|
|
const duration = this.playerElement.duration;
|
|
|
|
if (!duration || duration === 0) return;
|
|
|
|
const watchDuration = Math.floor(currentTime);
|
|
const completionPercentage = Math.min((currentTime / duration) * 100, 100);
|
|
|
|
try {
|
|
await fetch('/api/analytics.php?action=track', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
video_id: this.videoId,
|
|
event_type: 'watch_time',
|
|
watch_duration: watchDuration,
|
|
completion_percentage: completionPercentage.toFixed(2)
|
|
})
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to track progress:', error);
|
|
}
|
|
}
|
|
|
|
async trackCompletion() {
|
|
try {
|
|
await fetch('/api/analytics.php?action=track', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
video_id: this.videoId,
|
|
event_type: 'completion',
|
|
completion_percentage: 100
|
|
})
|
|
});
|
|
|
|
console.log('Completion tracked');
|
|
} catch (error) {
|
|
console.error('Failed to track completion:', error);
|
|
}
|
|
}
|
|
|
|
trackRetention() {
|
|
const currentTime = Math.floor(this.playerElement.currentTime);
|
|
const currentMinute = Math.floor(currentTime / 60);
|
|
|
|
// Track each minute once
|
|
if (!this.watchedMinutes.has(currentMinute)) {
|
|
this.watchedMinutes.add(currentMinute);
|
|
|
|
fetch('/api/analytics.php?action=track', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
video_id: this.videoId,
|
|
event_type: 'view',
|
|
minute: currentMinute
|
|
})
|
|
}).catch(err => console.error('Failed to track retention:', err));
|
|
}
|
|
}
|
|
|
|
detectTrafficSource() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const referrer = document.referrer;
|
|
|
|
// Check URL parameters first
|
|
if (urlParams.get('ref')) {
|
|
this.trafficSource = {
|
|
type: 'external',
|
|
detail: urlParams.get('ref')
|
|
};
|
|
} else if (urlParams.get('list')) {
|
|
this.trafficSource = {
|
|
type: 'playlist',
|
|
detail: urlParams.get('list')
|
|
};
|
|
} else if (urlParams.get('notification')) {
|
|
this.trafficSource = {
|
|
type: 'notification',
|
|
detail: urlParams.get('notification')
|
|
};
|
|
} else if (referrer) {
|
|
// Parse referrer
|
|
try {
|
|
const refUrl = new URL(referrer);
|
|
const hostname = refUrl.hostname;
|
|
|
|
if (hostname === window.location.hostname) {
|
|
// Internal referrer
|
|
if (refUrl.pathname.includes('/search')) {
|
|
this.trafficSource = { type: 'search', detail: refUrl.search };
|
|
} else if (refUrl.pathname.includes('/browse')) {
|
|
this.trafficSource = { type: 'suggested', detail: 'browse' };
|
|
} else {
|
|
this.trafficSource = { type: 'internal', detail: refUrl.pathname };
|
|
}
|
|
} else {
|
|
// External referrer
|
|
if (hostname.includes('google')) {
|
|
this.trafficSource = { type: 'search', detail: 'google' };
|
|
} else if (hostname.includes('facebook') || hostname.includes('twitter') || hostname.includes('reddit')) {
|
|
this.trafficSource = { type: 'social', detail: hostname };
|
|
} else {
|
|
this.trafficSource = { type: 'external', detail: hostname };
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.trafficSource = { type: 'direct', detail: '' };
|
|
}
|
|
} else {
|
|
this.trafficSource = { type: 'direct', detail: '' };
|
|
}
|
|
|
|
console.log('Traffic source:', this.trafficSource);
|
|
}
|
|
|
|
startPeriodicTracking() {
|
|
// Periodically save progress while playing
|
|
this.trackingTimer = setInterval(() => {
|
|
if (this.isPlaying) {
|
|
this.trackProgress();
|
|
}
|
|
}, this.trackingInterval);
|
|
}
|
|
|
|
destroy() {
|
|
if (this.trackingTimer) {
|
|
clearInterval(this.trackingTimer);
|
|
}
|
|
|
|
// Final progress update
|
|
this.trackProgress();
|
|
}
|
|
}
|
|
|
|
// Auto-initialize if video element and video ID exist
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const videoElement = document.querySelector('video');
|
|
const videoId = new URLSearchParams(window.location.search).get('v');
|
|
|
|
if (videoElement && videoId) {
|
|
window.videoAnalyticsTracker = new VideoAnalyticsTracker(videoId, {
|
|
playerElement: videoElement
|
|
});
|
|
}
|
|
});
|