Tracking HTML5 Videos with GTM

Almost all new browsers support the video playing using HTML5 including mobiles and tablets, so it may be way to embed videos that can be considerated when publishing videos on our pages. HTML5 has support for most of the videos format used on internet, even YouTube has been running an HTML5 version of their site for a long. So we’re going to teach you how you can track those videos embed’s using the built-in API and Event Listeners. We have setup a working page example at: http://stage.tags.ninja/html5.php Insert an HTML5 video player on a page, is as simple as adding this code:

<video controls="controls" width="320" height="240">
<source src="my_video_name.mp4" type="video/mp4">
</video>

So let’s start tracking our videos step by step, what features is going to have this tracking:

  • Tracking of video percent played, the play button clicks, the pause button clicks, and video viewing
  • It will keep a track of what % buckets of the video have been already sent to save hits.
  • It will support multiple video embeds on the same page

Tracking Flow

  1. Check if  there is any <video> tag in the current page. ( We don’t want to get the user’s browser executing this code if it’s not going to do anything )
  2. Wait till page’s DOM has been fully loaded ( gtm.dom )
  3. Fire the HTML5 Video Tracking Tag.

Tags, Rules and Macros

Tag

html5_video_gtm_tag

You can scroll down to see a fully comented code for this tag. The tag will have just 1 rule that will check for gtm.dom event from Google Tag Manager, and for the <video> tags presence that will be read using a Macro.

Rule

html5_video_gtm_rule

Macro

html5_video_gtm_macro

Thanks flys to Eliyahu Gusovsky from Analytics Ninja from who I learnt the markers piece of code.

Source Code

<script>
// Let's wrap everything inside a function so variables are not defined as globals 
(function() {
    // This is gonna our percent buckets ( 10%-90% ) 
    var divisor = 10;
    // We're going to save our players status on this object. 
    var videos_status = {};
    // This is the funcion that is gonna handle the event sent by the player listeners 
    function eventHandler(e) {
        switch (e.type) {
            // This event type is sent everytime the player updated it's current time, 
            // We're using for the % of the video played. 
        case 'timeupdate':
            // Let's set the save the current player's video time in our status object 
            videos_status[e.target.id].current = Math.round(e.target.currentTime);
            // We just want to send the percent events once 
            var pct = Math.floor(100 * videos_status[e.target.id].current / e.target.duration);
            for (var j in videos_status[e.target.id]._progress_markers) {
                if (pct >= j && j > videos_status[e.target.id].greatest_marker) {
                    videos_status[e.target.id].greatest_marker = j;
                }
            }
            // current bucket hasn't been already sent to GA?, let's push it to GTM
            if (videos_status[e.target.id].greatest_marker && !videos_status[e.target.id]._progress_markers[videos_status[e.target.id].greatest_marker]) {
                videos_status[e.target.id]._progress_markers[videos_status[e.target.id].greatest_marker] = true;
                dataLayer.push({
                    'event': 'gaEvent',
                    'gaEventCategory': 'HTML5 Video',
                    'gaEventAction': 'Progress %' + videos_status[e.target.id].greatest_marker,
                    // We are using sanitizing the current video src string, and getting just the video name part
                    'gaEventLabel': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1])
                });
            }
            break;
            // This event is fired when user's click on the play button
        case 'play':
            dataLayer.push({
                'event': 'gaEvent',
                'gaEventCategory': 'HTML5 Video',
                'gaEventAction': 'Play',
                'gaEventLabel': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1])
            });
            break;
            // This event is fied when user's click on the pause button
        case 'pause':
            dataLayer.push({
                'event': 'gaEvent',
                'gaEventCategory': 'HTML5 Video',
                'gaEventAction': 'Pause',
                'gaEventLabel': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1]),
                'gaEventValue': videos_status[e.target.id].current
            });
            break;
            // If the user ends playing the video, an Finish video will be pushed ( This equals to % played = 100 )  
        case 'ended':
            dataLayer.push({
                'event': 'gaEvent',
                'gaEventCategory': 'HTML5 Video',
                'gaEventAction': 'Finished',
                'gaEventLabel': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1])
            });
            break;
        default:
            break;
        }
    }
    // We need to configure the listeners
    // Let's grab all the the "video" objects on the current page     
    var videos = document.getElementsByTagName('video');
    for (var i = 0; i < videos.length; i++) {
        // In order to have some id to reference in our status object, we are adding an id to the video objects
        // that don't have an id attribute. 
        var videoTagId;
        if (!videos[i].getAttribute('id')) {
            // Generate a random alphanumeric string to use is as the id
            videoTagId = 'html5_video_' + Math.random().toString(36).slice(2);
            videos[i].setAttribute('id', videoTagId);
        }// Current video has alredy a id attribute, then let's use it <img draggable="false" class="emoji" alt="?" src="https://s.w.org/images/core/emoji/2/svg/1f642.svg">
        else {
            videoTagId = videos[i].getAttribute('id');
        }
        // Video Status Object declaration  
        videos_status[videoTagId] = {};
        // We'll save the highest percent mark played by the user in the current video.
        videos_status[videoTagId].greatest_marker = 0;
        // Let's set the progress markers, so we can know afterwards which ones have been already sent.
        videos_status[videoTagId]._progress_markers = {};
        for (j = 0; j < 100; j++) {
            videos_status[videoTagId].progress_point = divisor * Math.floor(j / divisor);
            videos_status[videoTagId]._progress_markers[videos_status[videoTagId].progress_point] = false;
        }
        // On page DOM, all players currentTime is 0 
        videos_status[videoTagId].current = 0;
        // Now we're setting the event listeners. 
        videos[i].addEventListener("play", eventHandler, false);
        videos[i].addEventListener("pause", eventHandler, false);
        videos[i].addEventListener("ended", eventHandler, false);
        videos[i].addEventListener("timeupdate", eventHandler, false);
    }
})();
</script>

Comments

31 responses to “Tracking HTML5 Videos with GTM”

  1. Les Faber Avatar

    Hi David: Wondering if you have tested this with Vimeo videos?

  2. Max Erickson Avatar
    Max Erickson

    Hey,

    I have been using your script and it has been working really well. Thank you very much for posting this.

    I do have a dilemma you may be able to help solve, or have already solved yourself.

    We recently implemented a video that plays on a loop. Your script only captures the first time around. I am curious if there is something I can add to the script to get tracking the second time the video is played, and the third time, and the fourth time, etc.

    Looking forward to your response.

    1. TDR Avatar
      TDR

      Max, I’m having a problem getting the code to register in Google Analytics. While in preview mode, I can see the “gaEvent,” but they never register within Google Analytics. Any help that you could provide would be appreciated.

    2. priyanka prakash Avatar
      priyanka prakash

      I am running to the same issue. Let me know if you found a solution to this! thank you

  3. Mattias Ström Avatar
    Mattias Ström

    Thnx David for you blog it have been a great deal of help! Would love to see a vimeo solution 🙂

  4. Mattias Ström Avatar
    Mattias Ström

    Thnx David for you blog it have been a great deal of help! Would love to see a vimeo solution 🙂
    Have a solution for yt videos that works out uf the box if you and some other stuff that im currently are trying out if u are interested:-)

  5. osouthgate Avatar

    Hi, great post and works very well so thanks for that.

    Has anyone managed to track outbound clicks to youtube?

    ‘Watch on Youtube.com’

    Thanks

  6. Dave Lindberg Avatar

    Hi David,

    Thanks for the very helpful post.

    I’m trying to translate it to the latest version of GTM — have set up the custom variable of “isHTML5VideoPresent”, added new target that fires when Event equals gtm.dom and isHTML5VideoPresent equals true. I’ve also created the new Custom HTML tag, using the code above.

    So far no luck. Any suggestions on how to debug this?

    1. Emily Patterson Avatar
      Emily Patterson

      Hi Dave — After some messing around, I got this work. First, go though the code above and rename each event, giving each (Play, Pause etc.) a unique name. (I changed “gaEvent” to “gaEventPlay,” “gaEventPause” etc.) Then, you need to set up a new GTM tag. The type to use is “Universal Analytics — Event.” You enter in you GA account tracking ID and your event parameters (yeah, I know they are already in the code above. but I don’t know how to get them into GA.) Then, make a new “Rule.” Set up the “Rule” to fire when the macro “{{event}}” “equals” the event name you set above (ie gaEventPlay, gaEventPause). That did the trick for me.

      1. Samuele Fabbri Avatar
        Samuele Fabbri

        When setting up the following code as a new customized javascript, it doesn’t work. Why? Could you give some tips?

        Function ()
        {

        return (document.getElementsByTagName (‘video’).length > 0) ? true: false
        }

        Then, if I were right, I’d need to add a “trigger” and a new UA tags by setting up customized HTML.

        1. DataEnthusiast Avatar
          DataEnthusiast

          Try a lower case ‘f’ in Function

      2. Johannes Avatar
        Johannes

        YOU ARE AMAZING!

        You totally made it work!!! This method works even in the newest version of GTM. Sorry to bring this thread up but you saved my skin. I used a Custom HTML tag with David’s code but GA wasn’t receiving anything in the Custom Dimensions I had set up (by pasting the custom dimension javascript code before the function in the custom HTML tag). To clarify on Emily’s wonderful tip: David’s code works by setting up custom Events. Once those events fire, you can set up your own tags for Play, Progress and Finish. Those tags need data layer variables also though.

        THANKS AGAIN EMILY YOU’RE AN ANGEL

    2. tomicek Avatar
      tomicek

      Hi David

      Have you found a solution with teh new GTM?

      regards

  7. Emily Patterson Avatar
    Emily Patterson

    Thanks so much for this! This is just what I’ve been looking for. Do I need to add my GA tracking ID number into this set up? When I’ve used the Link Click Listener in the past, I’ve had to enter my account ID when I set up the accompanying GA event. This code obviously has the GA events, but how does it get fired into my account?

  8. TDR Avatar
    TDR

    David,

    Thanks for putting this together, but I’m having trouble getting Google Analytics to register the events. I’m at a loss.

    If anyone can help by pointing me in the right direction that would be much appreciated.

  9. Aurore Mignot Avatar

    Thanks for these explanation, we’re trying to follow the activity around the videos of our website, and as using GTM is not always easy, we followed your explanation from A to Z… but at the end, in preview mode, our tag is not firing, do you have any explanation of why it doesn’t?
    Thanks a lot for your help,

  10. Tobi Avatar

    Hello,

    is this the explanation for the actually gtm?

    With Regards
    Tobi

  11. NM Avatar
    NM

    Thanks David, Emily.
    I have to implement Emily’s suggestion to get the data into the GA.
    For those who are going to implement this,
    – After doing David’s code
    – Modify the gaEvents to give unique names
    – Set up the data layer variables
    – Set up the Tag for each Event – for that need separate Custom Event triggers as well.

    GetElementByTagName didn’t work for me, so I had to use ByClassName to get it working.

  12. Dinesh Gopal Chand Avatar
    Dinesh Gopal Chand

    Hi David,
    Thanks This code worked well, but there is one problem in the code , please rectify it.
    In the last line you had to use closing script tag but it is opening script tag()

    1. David Vallejo Avatar

      Hi Dinesh, thanks for the notice. I’ve already updated the snippet

  13. nick guebhard Avatar

    Hi David

    What would I need to do if I wanted to fix the video duration to 10, 25, 50, 75, 90?

    I’ve tried changing the divisor to “5” but then it only fires at 5 and then at 50, 55, 60, etc.

    Thanks

  14. Michal Avatar
    Michal

    Hi David

    Thanks for the snippet. I think there is a bug – you’re adding an event listener for ‘ended’ twice.

  15. Guy Avatar
    Guy

    DO you have kind of similar solution for html5 audio ?

  16. Giovanni Avatar
    Giovanni

    Why you have two ended listener at the bottom?

    videos[i].addEventListener(“play”, eventHandler, false);
    videos[i].addEventListener(“pause”, eventHandler, false);
    videos[i].addEventListener(“ended”, eventHandler, false);
    videos[i].addEventListener(“timeupdate”, eventHandler, false);
    videos[i].addEventListener(“ended”, eventHandler, false);

    Very nice piece by the way, thankyou very much.

    1. David Vallejo Avatar

      Because copy&paste may be evil. Just typo. Already fixed. Thanks for notice.

  17. Jimalytics Avatar

    This works perfectly – thank you, sir!

  18. Rajat Avatar
    Rajat

    Thanks for this wonderful article. I just need help on how to track additional information like Video Mute and UnMute for the video. Can you help on that

  19. Vivek M Avatar
    Vivek M

    Hi,

    I want to show User defined Title in Event Category inside analytics event report. I like to do this on my aspx page level using javascript. Can you please assist how to do this. Current in report showing “HTML5 Video”, in place of this i want to show User defined title.
    Please help.

    Rgds,
    Vivek M.

  20. […] I’m talking about David Vallejo’s HTML5 video listener that is used to track almost any other video player that is not Youtube or Vimeo (or anything else […]

  21. Alban L Avatar
    Alban L

    Hi David, thanks for this great listener, I noticed one issue.
    I am testing with this video snippet :

    Votre navigateur ne gère pas l’élément video.

    Systematically at the end of the video, I have a “Pause” event even though I didn’t click pause.
    I am not quite sure why I have this problem.
    In the meantime, I solved the issue by wrapping the “Pause“ datalayer push like this:
    if (Math.floor(100 * videos_status[e.target.id].current / e.target.duration) < 100) {….pause datalayer push….}

    Note: This is a great use case to make a GTM community template, just saying 🙂

    1. David Vallejo Avatar

      Hi Alban, There’s a new improved code here: https://github.com/thyngster/html-media-elements-tracking-library , which also works for audio elements. I’m planning to update the original post at some point 🙂 .

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.