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:
Krystie
2026-03-30 17:34:18 -07:00
parent 9928a23c25
commit 3e4cfcf683
7 changed files with 1746 additions and 0 deletions
+232
View File
@@ -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
});
}
});