Skip to content

Track Video Engagement

Video engagement tracking tells you whether users actually watch your content, not just whether they land on the page. This recipe covers three scenarios: YouTube via GTM’s built-in trigger, Vimeo via the Player API, and HTML5 <video> elements via custom events.

GTM has a YouTube Video trigger that handles progress milestones, play, pause, seek, and completion automatically. It uses the YouTube iFrame Player API under the hood.

  1. Enable YouTube Video Trigger

    In GTM → Variables → Configure, enable these built-in video variables:

    • Video Provider
    • Video Status
    • Video URL
    • Video Title
    • Video Duration
    • Video Current Time
    • Video Percent
  2. Create a YouTube Video Trigger

    • Trigger type: YouTube Video
    • Capture: check Start, Complete, Pause, and Progress
    • Add percentages: 10, 25, 50, 75, 90
    • Fire on: All Videos (or filter by URL)
  3. Create a GA4 Event Tag

    • Tag type: Google Analytics: GA4 Event
    • Event name: video_{{Video Status}} — this creates dynamic event names like video_start, video_pause, video_complete

    Or use a fixed event name video_engagement with a video_action parameter:

    • video_action{{Video Status}}
    • video_title{{Video Title}}
    • video_url{{Video URL}}
    • video_percent{{Video Percent}}
    • video_current_time{{Video Current Time}}
    • video_duration{{Video Duration}}
    • Trigger: the YouTube trigger above
  4. Test in Preview Mode

    Visit a page with an embedded YouTube video. Press Play. The Summary pane should show a gtm.video event within 1-2 seconds.

Tag Configuration

GA4 - video_engagement

Type
Google Analytics: GA4 Event
Trigger
YouTube Video - start, pause, progress, complete
Variables
Video StatusVideo TitleVideo URLVideo PercentVideo Duration

GTM has no built-in Vimeo trigger. You need to load the Vimeo Player SDK and push to the dataLayer from JavaScript.

dataLayer.push() video_engagement

Push this for each Vimeo player event. Load the Vimeo Player SDK first.

// Load Vimeo Player SDK (or add via a GTM Custom HTML tag)
// <script src="https://player.vimeo.com/api/player.js"></script>
document.querySelectorAll('iframe[src*="vimeo.com"]').forEach(function(iframe) {
var player = new Vimeo.Player(iframe);
var videoData = {};
// Fetch metadata once
Promise.all([
player.getVideoTitle(),
player.getDuration()
]).then(function(values) {
videoData.title = values[0];
videoData.duration = values[1];
videoData.url = iframe.src;
});
function pushVideoEvent(action, percent) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'video_engagement',
video_provider: 'vimeo',
video_action: action,
video_title: videoData.title,
video_url: videoData.url,
video_duration: videoData.duration,
video_percent: percent || 0
});
}
player.on('play', function() { pushVideoEvent('start', 0); });
player.on('pause', function(data) {
pushVideoEvent('pause', Math.round(data.percent * 100));
});
player.on('ended', function() { pushVideoEvent('complete', 100); });
// Progress milestones
var milestones = [25, 50, 75, 90];
var fired = {};
player.on('timeupdate', function(data) {
var percent = Math.round(data.percent * 100);
milestones.forEach(function(m) {
if (percent >= m && !fired[m]) {
fired[m] = true;
pushVideoEvent('progress_' + m, m);
}
});
});
});

Add this as a Custom HTML tag in GTM with a Window Loaded trigger so the SDK and iframes are ready.

dataLayer.push() video_engagement

Attach to native HTML5 video elements on your page.

document.querySelectorAll('video').forEach(function(video) {
var milestones = [25, 50, 75, 90];
var fired = {};
var videoTitle = video.dataset.title || video.title || video.src.split('/').pop();
function pushVideoEvent(action, percent) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'video_engagement',
video_provider: 'html5',
video_action: action,
video_title: videoTitle,
video_url: video.currentSrc || video.src,
video_duration: Math.round(video.duration),
video_percent: percent
});
}
video.addEventListener('play', function() {
if (!fired.start) {
fired.start = true;
pushVideoEvent('start', 0);
}
});
video.addEventListener('pause', function() {
if (!video.ended) {
pushVideoEvent('pause', Math.round((video.currentTime / video.duration) * 100));
}
});
video.addEventListener('ended', function() {
pushVideoEvent('complete', 100);
});
video.addEventListener('timeupdate', function() {
var pct = Math.round((video.currentTime / video.duration) * 100);
milestones.forEach(function(m) {
if (pct >= m && !fired[m]) {
fired[m] = true;
pushVideoEvent('progress_' + m, m);
}
});
});
});
  1. Add the dataLayer push code as a Custom HTML tag in GTM with a Window Loaded trigger. This ensures the video elements exist before you query them.

  2. Create a Custom Event Trigger

    • Trigger type: Custom Event
    • Event name: video_engagement
  3. Create Data Layer Variables

    • DLV - video_actionvideo_action
    • DLV - video_titlevideo_title
    • DLV - video_percentvideo_percent
    • DLV - video_providervideo_provider
  4. Create a GA4 Event Tag

    • Event name: video_engagement
    • Parameters: map all DLV variables above
    • Trigger: the Custom Event trigger
  5. Test in Preview Mode

    Play the video, pause it, and let it reach 25% completion. Each action should appear as a separate video_engagement event in the Summary pane.

  1. Open GTM Preview and navigate to a page with a video
  2. Press Play — verify a video_start or video_engagement event fires
  3. Let the video reach 25% — verify a progress milestone event fires
  4. Check the Variables tab for correct video_title, video_percent, and video_duration
  5. In GA4 DebugView, verify events appear with correct parameters

YouTube trigger requires enablejsapi=1 in the embed URL. GTM’s built-in trigger adds this automatically if it is missing, but consent-blocked iframes may not reload with the parameter. Check Preview mode if no events appear.

Vimeo SDK must load before the iframe. If the SDK script loads after the Vimeo iframes, new Vimeo.Player(iframe) will fail silently. Use a Window Loaded trigger for the Custom HTML tag or add SDK loading to the same tag.

Video loaded dynamically after page load. If your video player is injected after page load (common in SPAs), the document.querySelectorAll call at Window Loaded will find nothing. Use a MutationObserver or push to the dataLayer from your video component’s mount lifecycle instead.