Tracking embedded and dynamically loaded YouTube videos via Google Tag Manager

Tracking YouTube videos in Google Analytics via Google Tag Manager is not a trivial thing. Previously, I have been using the method proposed by CardinalPath and it works pretty well for most people. However, I needed a bit more flexibility; e.g. I need to track not only embedded videos but also videos loaded dynamically. This could be videos loaded in a lightbox popup. In my case, videos loaded via the Magnific Popup script.

When looking for a solution I stumbled upon Simo Ahava’s blog. In his post on the tracking YouTube videos, he shows how to extend a solution proposed by LunaMetrics to be able to track pop up and dynamically loaded videos in general.

You can either follow Simo’s post or take a look at the description here:

Step 1: Create a custom HTML Tag with tracking script in Google Tag Manager

In Google Tag Manger create a Custom HTML Tag with the name ‘Utility – Video Tracking’ and past the following code:

<script>
// Respectfully copied and modified from 
// https://github.com/lunametrics/youtube-google-analytics
// 
// Original implementation by LunaMetrics
var ytTracker = (function( document, window, config ) {

  'use strict';

  window.onYouTubeIframeAPIReady = (function() {
    
    var cached = window.onYouTubeIframeAPIReady;

    return function() {
        
      if( cached ) {

        cached.apply(this, arguments);

      }

      // This script won't work on IE 6 or 7, so we bail at this point if we detect that UA
      if( !navigator.userAgent.match( /MSIE [67]./gi ) ) {

        init(); 
    
      }

    };

  })();
  
  var _config = config || {};
  var forceSyntax = _config.forceSyntax || 0;
  var dataLayerName = _config.dataLayerName || 'dataLayer';
  // Default configuration for events
  var eventsFired = {
    'Play'        : true,
    'Pause'       : true,
    'Watch to End': true
  };
  
  // Overwrites defaults with customizations, if any
  var key;
  for( key in _config.events ) {

    if( _config.events.hasOwnProperty( key ) ) {

      eventsFired[ key ] = _config.events[ key ];

    }

  }
  
  //*****//
  // DO NOT EDIT ANYTHING BELOW THIS LINE EXCEPT CONFIG AT THE BOTTOM
  //*****//

  // Invoked by the YouTube API when it's ready
  function init() {

    var iframes = document.getElementsByTagName( 'iframe' );
    var embeds  = document.getElementsByTagName( 'embed' );

    digestPotentialVideos( iframes );
    digestPotentialVideos( embeds );

  }

  var tag            = document.createElement( 'script' );
  tag.src            = '//www.youtube.com/iframe_api';
  var firstScriptTag = document.getElementsByTagName( 'script' )[0];
  firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );

  // Take our videos and turn them into trackable videos with events
  function digestPotentialVideos( potentialVideos ) {

    var i;

    for( i = 0; i < potentialVideos.length; i++ ) {       
      var isYouTubeVideo = checkIfYouTubeVideo( potentialVideos[ i ] );       
      if( isYouTubeVideo ) {
        var normalizedYouTubeIframe = normalizeYouTubeIframe( potentialVideos[ i ] );
        addYouTubeEvents( normalizedYouTubeIframe );
      }
    }

  }   

  // Determine if the element is a YouTube video or not   
  function checkIfYouTubeVideo( potentialYouTubeVideo ) {
    // Exclude already decorated videos     
    if (potentialYouTubeVideo.getAttribute('data-gtm-yt')) {       
      return false;
    }

    var potentialYouTubeVideoSrc = potentialYouTubeVideo.src || '';     
    if( potentialYouTubeVideoSrc.indexOf( 'youtube.com/embed/' ) > -1 || potentialYouTubeVideoSrc.indexOf( 'youtube.com/v/' ) > -1 ) {

      return true;

    }

    return false;

  }

  // Turn embed objects into iframe objects and ensure they have the right parameters
  function normalizeYouTubeIframe( youTubeVideo ) {
    
    var a           = document.createElement( 'a' );
        a.href      = youTubeVideo.src;
        a.hostname  = 'www.youtube.com';
        a.protocol  = document.location.protocol;
    var tmpPathname = a.pathname.charAt( 0 ) === '/' ? a.pathname : '/' + a.pathname;  // IE10 shim
    
    // For security reasons, YouTube wants an origin parameter set that matches our hostname
    var origin = window.location.protocol + '%2F%2F' + window.location.hostname + ( window.location.port ? ':' + window.location.port : '' );

    if( a.search.indexOf( 'enablejsapi' ) === -1 ) {

      a.search = ( a.search.length > 0 ? a.search + '&' : '' ) + 'enablejsapi=1';

    }

    // Don't set if testing locally
    if( a.search.indexOf( 'origin' ) === -1  && window.location.hostname.indexOf( 'localhost' ) === -1 ) {

      a.search = a.search + '&origin=' + origin;

    }

    if( youTubeVideo.type === 'application/x-shockwave-flash' ) {

      var newIframe     = document.createElement( 'iframe' );
      newIframe.height  = youTubeVideo.height;
      newIframe.width   = youTubeVideo.width;
      tmpPathname = tmpPathname.replace('/v/', '/embed/');

      youTubeVideo.parentNode.parentNode.replaceChild( newIframe, youTubeVideo.parentNode );

      youTubeVideo = newIframe;

    }

    a.pathname       = tmpPathname;
    if(youTubeVideo.src !== a.href + a.hash) {
    
      youTubeVideo.src = a.href + a.hash;

    }

    youTubeVideo.setAttribute('data-gtm-yt', 'true');

    return youTubeVideo;

  }

  // Add event handlers for events emitted by the YouTube API
  function addYouTubeEvents( youTubeIframe ) {

    youTubeIframe.pauseFlag  = false;

    new YT.Player( youTubeIframe, {

      events: {

        onStateChange: function( evt ) {

          onStateChangeHandler( evt, youTubeIframe );

        }

      }

    } );

  }

  // Returns key/value pairs of percentages: number of seconds to achieve
  function getMarks(duration) {

    var marks = {}; 

    // For full support, we're handling Watch to End with percentage viewed
    if (_config.events[ 'Watch to End' ] ) {

      marks[ 'Watch to End' ] = duration * 99 / 100;

    }

    if( _config.percentageTracking ) {

      var points = [];
      var i;

      if( _config.percentageTracking.each ) {

        points = points.concat( _config.percentageTracking.each );

      }

      if( _config.percentageTracking.every ) {

        var every = parseInt( _config.percentageTracking.every, 10 );
        var num = 100 / every;
        
        for( i = 1; i < num; i++ ) {
      
          points.push(i * every);

        }

      }

      for(i = 0; i < points.length; i++) {

        var _point = points[i];
        var _mark = _point + '%';
        var _time = duration * _point / 100;
        
        marks[_mark] = Math.floor( _time );

      }

    }

    return marks;

  }

  function checkCompletion(player, marks, videoId) {

    var duration     = player.getDuration();
    var currentTime  = player.getCurrentTime();
    var playbackRate = player.getPlaybackRate();
    player[videoId] = player[videoId] || {};
    var key;

    for( key in marks ) {

      if( marks[key] <= currentTime && !player[videoId][key] ) {

        player[videoId][key] = true;
        fireAnalyticsEvent( videoId, key );

      }

    }

  }

  // Event handler for events emitted from the YouTube API
  function onStateChangeHandler( evt, youTubeIframe ) {
 
    var stateIndex     = evt.data;
    var player         = evt.target;
    var targetVideoUrl = player.getVideoUrl();
    var targetVideoId  = targetVideoUrl.match( /[?&]v=([^&#]*)/ )[ 1 ];  // Extract the ID    
    var playerState    = player.getPlayerState();
    var duration       = player.getDuration();
    var marks          = getMarks(duration);
    var playerStatesIndex = {
      '1' : 'Play',
      '2' : 'Pause'
    };
    var state = playerStatesIndex[ stateIndex ]; 

    youTubeIframe.playTracker = youTubeIframe.playTracker || {};

    if( playerState === 1 && !youTubeIframe.timer ) {

      clearInterval(youTubeIframe.timer);

      youTubeIframe.timer = setInterval(function() {

        // Check every second to see if we've hit any of our percentage viewed marks
        checkCompletion(player, marks, youTubeIframe.videoId);

      }, 1000);

    } else {

      clearInterval(youTubeIframe.timer);
      youTubeIframe.timer = false;

    }

    // Playlist edge-case handler
    if( stateIndex === 1 ) {

      youTubeIframe.playTracker[ targetVideoId ] = true;
      youTubeIframe.videoId = targetVideoId;
      youTubeIframe.pauseFlag = false;

    }

    if( !youTubeIframe.playTracker[ youTubeIframe.videoId ] ) {

      // This video hasn't started yet, so this is spam
      return false;

    }

    if( stateIndex === 2 ) {

      if( !youTubeIframe.pauseFlag ) { 
      
        youTubeIframe.pauseFlag = true;

      } else {

        // We don't want to fire consecutive pause events
        return false;

      }

    }

    // If we're meant to track this event, fire it
    if( eventsFired[ state ] ) {
    
      fireAnalyticsEvent( youTubeIframe.videoId, state );

    }

  }

  // Fire an event to Google Analytics or Google Tag Manager
  function fireAnalyticsEvent( videoId, state ) {

    var videoUrl = 'https://www.youtube.com/watch?v=' + videoId;
    var _ga = window.GoogleAnalyticsObject;

    if( typeof window[ dataLayerName ] !== 'undefined' && !_config.forceSyntax ) { 
      
      window[ dataLayerName ].push( {

        'event'     : 'youTubeTrack',
        'attributes': {

          'videoUrl': videoUrl,
          'videoAction': state

        }

      } );

    } else if( typeof window[ _ga ] === 'function' && 
               typeof window[ _ga ].getAll === 'function' && 
               _config.forceSyntax !== 2 ) 
    {

      window[ _ga ]( 'send', 'event', 'Videos', state, videoUrl );

    } else if( typeof window._gaq !== 'undefined' && forceSyntax !== 1 ) {

      window._gaq.push( [ '_trackEvent', 'Videos', state, videoUrl ] );

    }

  }
  
  return {
    init : init,
    digestPotentialVideos : digestPotentialVideos
  }
    
})(document, window, {
  'events': {
    'Play': true,
    'Pause': true,
    'Watch to End': true
  },
  'percentageTracking': {
    'every': 25,
    'each': [ 10, 90 ]
  }
});
/*
 * Configuration Details
 *
 * @property events object
 * Defines which events emitted by YouTube API
 * will be turned into Google Analytics or GTM events
 *
 * @property percentageTracking object
 * Object with configurations for percentage viewed events
 *
 *   @property each array
 *   Fires an event once each percentage ahs been reached
 *
 *   @property every number
 *   Fires an event for every n% viewed
 *
 * @property forceSyntax int 0, 1, or 2
 * Forces script to use Classic (2) or Universal(1)
 *
 * @property dataLayerName string
 * Tells script to use custom dataLayer name instead of default
 */
</script>

Set the script to fire on All Pages. Alternatively, you can make a trigger that only fires when there is a video on the page. The tag should look like this:

youtube tracking in google tag manager-script

To get the details on what this script does, pay a visit to Simo’s post. The most important thing it does is that it exposes a variable named ytTracker to the global namespace. From this variable, we will be able to call the functions ‘init’ and ‘digestPotentialVideos’ in the script at will. In short, this means that if we call ytTracker.init() at any point in time it will run the script again. This is, of course, exactly what we want for dynamically loaded videos.

Step 2: Create Data Layer variables to catch URL of video and user action on video

Create the following two Data Layer variables:

1. Name: DataLayer – attributes.videoAction

  • Type: Data Layer
  • Data Layer Variable Name: attributes.videoAction

This will catch the action the user has taken, e.g, Play or Pause. Furthermore, it will catch non-user interactions like Watch To End, 10 % watched, 90 % watched, etc. The variable should look like this:

youtube-tracking-in-google-tag-manager-video-action

2. Name: DataLayer – attributes.videoUrl

  • Type: Data Layer
  • Data Layer Variable Name: attributes.videoUrl

This will catch the URL of the video on YouTube. The variable should look like this:

youtube-tracking-in-google-tag-manager-video-url

Step 3: Create a Trigger that responds to video actions

Create a Custom Event trigger and set it to fire on Event Name youTubeTrack. This is the event dispatched by the script in step 1 above. Click the ‘Add filters’ button to reveal ‘Fire this trigger when an Event occurs and all of these conditions are true.’. in the first field select the Data Layer variable named ‘DataLayer – attributes.videoAction’ that we created in step 2. Select ‘matches RegEx (ignore case) in field two and enter ‘Play|Pause|Watch to EndPlay|10%|25%|50%|75%|90%’ in field three. This should make the trigger look like this:

youtube-tracking-in-google-tag-manager-trigger

Step 4: Create a Tag to send YouTube events to Google Analytics

As we now have the all the data and can catch it as events happen it is time to actually use it. So go ahead and create a tag with the following details:

  • Name: GA – Event – YouTube Video Interaction
  • Choose Google Analytics as product and Universal Analytics as Tag Type
  • Under Configure Tag insert your Google Analytics Tracking ID, Chose Event as Track Type and set Category to Video. Under Action and Label choose the variables DataLayer – attributes.videoAction and DataLayer – attributes.videoUrl we created in step 2. Finally, you might want to give the action a value.
  • In the Fire On step, choose More and select the trigger named Event – YouTube Video Interaction that we created in step 3.

The final tag should look somewhat like this.

google-tag-manager-youtube-tag

Step 5: Initiate videos

In this final step we need to initiate the videos. This will either be on page load for on page videos but could also be on specific events like e.g. a popup. In the case of Magnific Popup this can be done using one of the following

ytTracker.init(); // Check all videos on the page
ytTracker.digestPotentialVideos(iframe); // Only check videos that has been passed as variable

So on the case of popups with Magnific Popup the script could look like this (also using jQuery).

//magnific popup
$(document).ready(function () {
  $('.lightbox-video').magnificPopup({
    disableOn: 0,
    type: 'iframe',
    mainClass: 'mfp-fade',
    removalDelay: 160,
    preloader: false,
    fixedContentPos: true,
    callbacks: {
      open: function() {
          //send track to YouTube script if present
        if(ytTracker != null) {
          ytTracker.init();
        }

      }
    }
  });
});

This will target all of the following tags and make sure interactions with the respective videos will be tracked in Google Analytics.

<a class="lightbox-video" href="http://www.youtube.com/watch?v=-m5cJE8bwhM">Watch video</a>

So to wrap up; this might seem like a lot of steps to track YouTube videos in Google Analytics with Google Tag Manager. However, it is pretty efficient and not that big a task.

15 responses to “Tracking embedded and dynamically loaded YouTube videos via Google Tag Manager

    1. Hi Nathan, You should be able to see it under Behaviour > Events > Overview if you have set it up correctly. If you have no data you might want to debug with Google Tag Assistant. You can find it in the Crome web store.

  1. Is there a way to gather the video title as well? The Simo Ahava fix only works with the LunaMetrics method, but the LunaMetrics method doesn’t append the video title into the event label.

    The Cardinal Path method, however, collects the video title for the event label, but doesn’t work with the Sim Ahava dynamic loading fix. 🙁

  2. I was able to track youtube videos that are only embedded and had difficulties applying to videos that are loaded dynamically so thanks for this code Søren, much appreciated.

  3. Hi,

    Thank you for this post. Once I’ve done all the steps, i don’t understand how to do with the 5th step? Can i do with in Google Tag Manager or do I have to do anything else on the website?

    Then what do I have to do in order to see the events appears in analytics? I’m a begginer, I hope you can help me.
    Thanks a lot.
    Mathilde

      1. I am having issues with Step 5 as well. I’m using YooTheme’s Widgetkit Lightbox. I tried putting the code on the page and in the script file. I keep getting an error that says, “ytTracker is not defined.” Any ideas?

      2. Hard to say. I would try to debug and see if this was available in the console by writing ytTracker when the page has loaded. It should return the ytTracker object. If it is not there you might want to look at your Tag Manager code and see if that is working. If it does return an object make sure that you don’t just call it on document ready. It probably has to be in a callback (as in step 5 in the example) as Tag Manager lazy loads it’s tags. I hope this guides you in the right direction.

  4. Hi there –

    This post was extremely helpful! Thank you so much for putting it together.

    I noticed though that the percentage tracking wasn’t coming through. You will want to update the Trigger Configuration for the ‘youTubeTrack’ trigger with the following value:

    Play|Pause|Watch to End|25%|50%|75%

    Works like a charm!

    Thanks again 🙂

    1. Hi,

      This is great. Thank you so much for posting. This is just what I was looking for. Is there a way to get the name of the video reported back to GTM?

      Thanks,
      Mike

    2. Correct. Actually, it needs to include 10% and 90% as well. I have updated it in the post so it now says the following:

      Play|Pause|Watch to End|10%|25%|50%|75%|90%

Leave a Reply

Your email address will not be published. Required fields are marked *