Video Engagement Tracker
YouTube Analytics Integration
Advanced YouTube video engagement tracking with milestone detection (25%, 50%, 75%, 100%), play/pause events, and detailed analytics integration for comprehensive video performance measurement.
Key Features
Script Overview
What It Does
This script integrates with YouTube's Player API to track detailed video engagement metrics including play/pause events, seek behaviors, and milestone completions. Perfect for analyzing how users interact with your product demos and educational content.
Use Case
Perfect for SaaS companies with product demo videos or educational content. Essential for understanding which parts of your videos drive engagement and conversions.
Key Benefits
- •Detailed video engagement analytics
- •Milestone-based conversion tracking
- •User interaction behavior insights
- •Demo effectiveness measurement
- •Content optimization data
- •Enhanced user journey tracking
JavaScript Code
<script>
// Video Engagement Tracker Script
// Version: 1.0 - YouTube Integration
// Last Updated: 2025-09-09
(function() {
'use strict';
var config = {
// Milestone percentages to track
milestones: [25, 50, 75, 100],
// Events to track
events: {
videoStart: 'video_start',
videoPause: 'video_pause',
videoResume: 'video_resume',
videoComplete: 'video_complete',
videoMilestone: 'video_milestone',
videoSeek: 'video_seek'
},
// Minimum watch time before counting as "started" (seconds)
minWatchTime: 3,
debug: false
};
var trackedVideos = {};
var ytReady = false;
function debugLog(message, data) {
if (config.debug) {
console.log('[Video Tracker] ' + message, data || '');
}
}
function initYouTubeAPI() {
if (window.YT && window.YT.Player) {
ytReady = true;
initVideoTracking();
return;
}
// Load YouTube API if not already loaded
if (!window.onYouTubeIframeAPIReady) {
var tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = function() {
ytReady = true;
initVideoTracking();
};
}
}
function getVideoData(player, iframe) {
try {
var videoUrl = player.getVideoUrl && player.getVideoUrl();
var videoId = player.getVideoData && player.getVideoData().video_id;
var title = player.getVideoData && player.getVideoData().title;
return {
videoId: videoId || 'unknown',
title: title || 'Unknown Video',
url: videoUrl || iframe.src,
duration: player.getDuration && player.getDuration() || 0,
iframe: iframe
};
} catch (e) {
debugLog('Error getting video data:', e);
return {
videoId: 'unknown',
title: 'Unknown Video',
url: iframe.src || 'unknown',
duration: 0,
iframe: iframe
};
}
}
function trackVideoEvent(eventName, videoData, additionalData) {
window.dataLayer = window.dataLayer || [];
var eventData = {
event: eventName,
video_id: videoData.videoId,
video_title: videoData.title,
video_url: videoData.url,
video_duration: videoData.duration,
video_current_time: additionalData && additionalData.currentTime || 0,
video_percent_watched: additionalData && additionalData.percentWatched || 0
};
if (additionalData) {
Object.keys(additionalData).forEach(function(key) {
if (key !== 'currentTime' && key !== 'percentWatched') {
eventData['video_' + key] = additionalData[key];
}
});
}
window.dataLayer.push(eventData);
debugLog('Video event tracked:', eventData);
}
function setupVideoPlayer(iframe) {
try {
var player = new YT.Player(iframe, {
events: {
onStateChange: function(event) {
handleStateChange(event, iframe);
},
onReady: function(event) {
var videoData = getVideoData(event.target, iframe);
trackedVideos[iframe.id] = {
player: event.target,
data: videoData,
startTime: null,
milestones: {},
totalWatchTime: 0,
interactions: 0
};
debugLog('Video player ready:', videoData);
}
}
});
} catch (e) {
debugLog('Error setting up video player:', e);
}
}
function handleStateChange(event, iframe) {
var videoInfo = trackedVideos[iframe.id];
if (!videoInfo) return;
var player = event.target;
var currentTime = player.getCurrentTime && player.getCurrentTime() || 0;
var duration = player.getDuration && player.getDuration() || 0;
var percentWatched = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;
switch (event.data) {
case YT.PlayerState.PLAYING:
if (!videoInfo.startTime && currentTime > config.minWatchTime) {
videoInfo.startTime = Date.now();
trackVideoEvent(config.events.videoStart, videoInfo.data, {
currentTime: currentTime,
percentWatched: percentWatched
});
} else if (videoInfo.startTime) {
trackVideoEvent(config.events.videoResume, videoInfo.data, {
currentTime: currentTime,
percentWatched: percentWatched
});
}
// Start milestone tracking
startMilestoneTracking(iframe.id);
break;
case YT.PlayerState.PAUSED:
if (videoInfo.startTime) {
trackVideoEvent(config.events.videoPause, videoInfo.data, {
currentTime: currentTime,
percentWatched: percentWatched
});
}
stopMilestoneTracking(iframe.id);
break;
case YT.PlayerState.ENDED:
trackVideoEvent(config.events.videoComplete, videoInfo.data, {
currentTime: currentTime,
percentWatched: 100,
totalWatchTime: videoInfo.totalWatchTime
});
stopMilestoneTracking(iframe.id);
break;
}
}
function startMilestoneTracking(videoId) {
var videoInfo = trackedVideos[videoId];
if (!videoInfo || videoInfo.milestoneInterval) return;
videoInfo.milestoneInterval = setInterval(function() {
checkMilestones(videoId);
}, 1000);
}
function stopMilestoneTracking(videoId) {
var videoInfo = trackedVideos[videoId];
if (videoInfo && videoInfo.milestoneInterval) {
clearInterval(videoInfo.milestoneInterval);
videoInfo.milestoneInterval = null;
}
}
function checkMilestones(videoId) {
var videoInfo = trackedVideos[videoId];
if (!videoInfo) return;
try {
var currentTime = videoInfo.player.getCurrentTime();
var duration = videoInfo.player.getDuration();
var percentWatched = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;
config.milestones.forEach(function(milestone) {
if (percentWatched >= milestone && !videoInfo.milestones[milestone]) {
videoInfo.milestones[milestone] = true;
trackVideoEvent(config.events.videoMilestone, videoInfo.data, {
currentTime: currentTime,
percentWatched: percentWatched,
milestone: milestone
});
}
});
} catch (e) {
debugLog('Error checking milestones:', e);
}
}
function initVideoTracking() {
if (!ytReady) return;
var iframes = document.querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtu.be"]');
Array.prototype.forEach.call(iframes, function(iframe, index) {
if (!iframe.id) {
iframe.id = 'yt-player-' + index;
}
// Ensure iframe has required parameters for API
var src = iframe.src;
if (src.indexOf('enablejsapi=1') === -1) {
src += (src.indexOf('?') > -1 ? '&' : '?') + 'enablejsapi=1';
iframe.src = src;
}
setupVideoPlayer(iframe);
});
debugLog('Initialized tracking for ' + iframes.length + ' YouTube videos');
}
// Initialize
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initYouTubeAPI);
} else {
initYouTubeAPI();
}
// Expose utility functions
window.videoEngagementTracker = {
getTrackedVideos: function() {
return trackedVideos;
},
reinitialize: function() {
initVideoTracking();
}
};
})();
</script>
💡 Pro Tip: Copy the entire code block above and paste it directly into a GTM Custom HTML tag. The script is self-contained and ready to use immediately.