• Courses
    • Courses
    • Course bundles
  • Blog
  • Services
  • Resources
    • Youtube channel
    • E-books and Guides
    • GTM Recipes
    • View All Resources
    • GTM Community
    • GA4 community
  • About
    • About
    • Contact
  • Login
  • Courses
    • Courses
    • Course bundles
  • Blog
  • Services
  • Resources
    • Youtube channel
    • E-books and Guides
    • GTM Recipes
    • View All Resources
    • GTM Community
    • GA4 community
  • About
    • About
    • Contact
  • Login

January 1, 2022

Track HTML5 Video with Google Analytics 4 and Google Tag Manager

If your website is using some no-name video player, there is a very high chance that this is a generic HTML5 video player. If you want to track its engagement with Google Tag Manager, you will need to do some additional configuration.

In this blog post, I will show how to track HTML5 video player with Google Analytics 4 and Google Tag Manager.

 

Table of Contents

– Hide table of contents –

  • GTM recipe
  • Custom JavaScript variable + Auto-event listener
  • Test the listener
  • Data Layer Variables, Custom Event Trigger
  • GA4 event tag
  • Test the setup
  • Register custom dimensions
  • Wait for up to 24 hours
  • Where can I see data in GA4 reports?
  • Final words

 

GTM recipe

If you are in a hurry, I have prepared a GTM container template that you can import, configure and then it will start tracking embedded HTML5 video players + send the data to GA4.

 

Custom JavaScript variable + Auto-event listener

The most important part of this setup is a custom code that is designed to keep looking for HTML5 video interactions. Originally, this solution was created by David Vallejo but I have added some modifications to make the tracking more convenient with Google Analytics 4. But before we create this tag, we need to create a Custom JavaScript variable.

The listener that we are going to use consists of 100+ lines of code and if you are working with GTM, you should always try to optimize the setup so that it affects the page loading/performance as little as possible.

Instead of firing the listener on every page, we will be firing it only on those pages where the HTML5 player is actually present. That can be done with the Custom JavaScript variable.

In Google Tag Manager, go to Variables > New > Custom JavaScript and paste the following code:

function () {
var video = document.getElementsByTagName('video').length;
	if (video > 0) {
      return true;
    }
	else {
      return false;
    }
}

Name this variable: cjs – is HTML5 Video on a page.

Then go to Triggers > New > Window Loaded and enter the following settings:

This trigger will be activated when the page completely loads and the HTML5 video player is present on a page. How will we know if the player is on a page? That Custom JavaScript variable will return true if the player is actually available on a page.

Now, it’s time to create a Custom HTML tag that will contain the listener’s code. Just to remind you, this tag will be looking for HTML5 video interactions and if it spots one, we will be able to see it in the GTM preview mode, thus fire the GA4 event tag.

In GTM, go to Tags > New > Custom HTML and paste the following code:

<script>
// Let's wrap everything inside a function so variables are not defined as globals 
(function() {
    // This is gonna our percent buckets ( 25%-75% ) 
    var divisor = 25;
    // 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': 'video',
                    'video_status': 'progress',
                    'video_provider' : 'generic html5 video player',
                    'video_percent': videos_status[e.target.id].greatest_marker,
                    // We are sanitizing the current video src string, and getting just the video name part
                    'video_title': 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': 'video',
                'video_status' : 'play',
                'video_provider' : 'generic html5 video player',
                'video_percent': videos_status[e.target.id].greatest_marker,
                'video_title': 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':
            if (videos_status[e.target.id].greatest_marker < '75') {
            dataLayer.push({
                'event': 'video',
                'video_status' : 'pause',
                'video_provider' : 'generic html5 video player',
                'video_percent': videos_status[e.target.id].greatest_marker,
                'video_title': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1])
            });
            }
            break;
            // If the user ends playing the video, an Finish video will be pushed ( This equals to % played = 100 )  
        case 'ended':
            dataLayer.push({
                'event': 'video',
                'video_status' : 'complete',
                'video_provider' : 'generic html5 video player',
                'video_percent': '100',
                'video_title': 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);
        videos[i].addEventListener("ended", eventHandler, false);
    }
})();
</script>

Add the previously created Window Loaded trigger to this tag.

 

Test the listener

Save all changes in your GTM container and then click the Preview button in the top-right corner of the GTM interface and enable the preview mode.

First of all, in the preview mode, you should see that your Custom HTML tag has fired once the page loaded.

Then go to your website and interact with the HTML5 video player. Play it, watch for a bit (at least 25% of the video length), pause. Go back to the Preview mode of GTM. You should see a bunch of video events there.

Click one of them and expand the dataLayer.push. You will see some information about the interaction.

If you are seeing all of this as I am, then we can continue.

 

Data Layer Variables, Custom Event Trigger

Even though we have that video data in the Data Layer, we cannot use it until we create variables for each data point that we plan to use. In our case, we are going to use:

  • video_status
  • video_provider
  • video_percent
  • video_title

For each one of them, we need to create a separate Data Layer Variable. Let’s go to Variables > New > Data Layer Variable and enter the following settings:

Do the same thing for the rest of the data points you want to use (video_provider, video_percent, video_title).

Then we need to create a trigger. Every time a “video” event is pushed to the data layer (and we see it in the preview mode), we want to fire a GA4 tag (that we will create later). That tag will send the video data to Google Analytics. The only way how can we fire a tag is by using a trigger.

Since we are getting a custom named “video” event in the preview mode, we have to use a Custom Event trigger in GTM. Go to Triggers > New > Custom Event and enter the word “video” (without quotation marks, all lowercase).

It’s important that you enter the word “video” exactly the same way as you see it here:

 

GA4 event tag

Now, it’s time to send the HTML5 video data to Google Analytics 4 as an event. In GTM, go to Tags > New > GA4 event and enter the following settings:

  • You need to select your existing GA4 config tag (if you don’t know what it is, read this guide)
  • The event’s name is video_ and then I inserted the Data Layer variable that returns the video_status . As a result, the final output of the event name will be video_play, video_pause, video_progress, or video_complete. I’m just trying to follow Google’s naming convention.
  • Then I inserted other data layer variables that will be sent as event parameters video_provider, video_percent, and video_title (again, I’m just following Google’s naming convention here)
  • Finally, I linked the Custom Event trigger to this GA4 tag.

 

Test the setup

Click the Preview button in the GTM interface to refresh it. Then go to your website and interact with the HTML5 video player (play, watch for a bit, etc.). Then go back to the GTM preview mode and you should see this:

Click on any of those events and check if your GA4 tag has fired. Now click another event and do the same thing. Tag fired? That’s good.

Now go to Google Analytics > DebugView and find your device:

Once you do that, you should see the event stream and several video events (e.g. video_play). Click them and check the values of their parameters.

If everything is working as expected, it’s time to publish the container. Hit SUBMIT in the top-right corner and follow all the necessary steps. After that, your changes will go live to all visitors of your website.

 

Register custom dimensions

I don’t know whether this will change in the future, but right now (if you want to use/see parameters like video_title or video_percent in your regular GA4 reports and Analysis Hub), you have to register those parameters as custom dimensions.

In GA4, go to Custom Definitions and click Create Custom Dimension.

Then enter the following settings if you want to use/see the video_title parameter. Want to use others as well? You will need to register all of them one by one here. Dimension scope should be “event” for all the dimensions/parameters in this case.

 

Wait for up to 24 hours

Now, you have to be a bit patient and wait. The next day, you should start seeing your video events and their data in your GA4 reports.

 

Where can I see video event data in Google Analytics 4?

This is answered in my guide on event tracking with Google Analytics 4. I have a dedicated section that explains in which reports can you find video data. So if you want to learn more, go here.

 

Track HTML5 Video with Google Analytics 4: Final words

That’s it for this time. In this blog post, I explained the process of how to track embedded videos with GTM (that are not Youtube videos). In fact, if you try to implement something similar for other players, the principle is quite the same:

  • You find a custom auto-event listener built specifically for that video player (if you can’t find it, then most likely, you won’t be able to track the video player)
  • You create a Custom JS variable that returns true if the player is on a page
  • You fire a listener (Custom HTML tag) only if the player is present on a page
  • Then you create Data Layer Variables, Custom Event trigger
  • Finally, you create a GA4 event tag that fires on the custom event trigger and sends the values of data layer variables

Also, don’t forget to try the GTM recipe and implement this kind of tracking faster.

 

Julius Fedorovicius
In Google Analytics Tips Google Tag Manager Tips
13 COMMENTS
Cory
  • Jun 11 2021
  • Reply

Hi Julius,
Is there a way to filter out the autoplay HTML5 videos so that the only videos being tracked are the ones being clicked?

    Mike
    • Sep 8 2021
    • Reply

    You can achieve that by adjusting trigering options for the tag.

Billiam
  • Dec 2 2021
  • Reply

Hello. Thank you for your tutorials. I got as far as the video listener test in GTM preview but I can't get it to fire. In addition to, "cjs – is HTML5 Video on a page" there is another filter appended: _event equals gtm.load that causes it to fail. Do you know where this parameter is coming from to correct/remove?

    Billiam
    • Dec 3 2021
    • Reply

    My fault... the tag is fired on 'Window Loaded' not from the video click itself.

Asa
  • Dec 9 2021
  • Reply

Brilliant intro to Tag Manager, GA4 and HTML5 Video tracking :-)

Chris
  • Feb 26 2022
  • Reply

Will this track videos that auto-play when the page loads?

JM
  • Mar 24 2022
  • Reply

Is there a way to send data from the application? Push new arguments to the dataLayer for example, like an ID. If so, how can that be accomplished?

Liam Fletcher
  • Apr 5 2022
  • Reply

Hi Juliius. Great post thanks. Along with all your other posts and videos I have now a basic understanding of GTM and a fairly good setup. I have followed this post and am now tracking video interactions with one issue. I am using JW player as our video player, and I want to pass the video title properly. I assume I need to change this part of the custom HTML tag:

'video_title': decodeURIComponent(e.target.currentSrc.split('/')[e.target.currentSrc.split('/').length - 1])

..to pass in the correct video title coming from JW player, but I have no idea what it should be. Are you able to help with this?

    Julius Fedorovicius
    • Apr 13 2022
    • Reply

    Just google "JW player google tag manager" and you will find a code that is created specifically for JW player

Hafiz Ahmad Ashfaq
  • Jun 6 2022
  • Reply

Do you have a solution for vidalytics video percentage watched tracking?

There's a lot of userbase switching from wistia to vidalytics, can't find anything specific for vidalytics.

Pritam Choudhary
  • Jun 22 2022
  • Reply

Thanks for this article.

Can we track HTML video which is getting played on model popup? It is working fine for video which is not on the model popup, but video on model popup is not getting traced in GTM.

Can you share your thought.

Pritam Choudhary
  • Jun 22 2022
  • Reply

Video is also not getting tracked in GTM if we play video on click event.

Philip DiPatrizio
  • Jun 23 2022
  • Reply

Is there a reason you chose event names that differ from Google's enhanced measurement events for video engagement (video_start, video_progress, video_complete)?

Just wondering if you didn't think of doing so, or if you considered it but decided to use different names for a particular reason.

Thanks!

Leave a comment Cancel reply

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

 

Hi, I'm Julius Fedorovicius and I'm here to help you learn Google Tag Manager and Google Analytics. Join thousands of other digital marketers and digital analysts in this exciting journey. Read more
Analytics Mania
  • Google Tag Manager Courses
  • Google Tag Manager Recipes
  • Google Tag Manager Resources
  • Google Tag Manager Community
  • Login to courses
Follow Analytics Mania
  • Subscribe to newsletter
Recent Posts
  • Track Videos with Google Analytics 4 and Google Tag Manager
  • Debugging Incorrect & Missing Google Analytics 4 Transactions
  • A Guide to Referrer in Google Tag Manager
Analytics Mania - Google Tag Manager and Google Analytics Blog | Privacy Policy
Manage Cookie Settings