feat: Add comprehensive analytics dashboard for creators
- 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
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user