Google Tag Manager – Triggers Breakdown and Javascript Alternatives

  • report
    Disclaimer
    Click for Disclaimer
    This Post is over a year old (first published about 5 years ago). As such, please keep in mind that some of the information may no longer be accurate, best practice, or a reflection of how I would approach the same thing today.
  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    May. 02, 2019
    Last Updated:
    May. 02, 2019
  • classTags
    classClick for Tags

Google Tag Manager (GTM) is a great tool for digital marketing, devops, A/B website testing, analytics, you name it. With just a few clicks, you can easily assign tags to fire based on complex combinations of rules – such as firing a conversion pixel when a user submits a contact form with a certain dropdown field value – without having to write Javascript (JS) by hand, or update the actual source code of the site. However, I also think that GTM gets used a little too often, without marketers understanding the underlying technology they are using, which can lead to tunnel vision and an inability to come up with solutions when GTM cannot be used for a given problem.

This post will attempt to explain how GTM works on multiple levels, and serve as a guide on how you might replicate certain GTM functionalities with pure vanilla Javascript.


Table of Contents:


WARNING: Conflicting terminology

First, something that has to be addressed right away; certain words carry a different meaning within the Javascript world that powers GTM, vs the marketing world that uses it. At the top of the list is the word “event”.

For the duration of this post, I’ll try to be very specific about which type of event I’m talking about. For Browser DOM Javascript Events, I’ll say “JS Events” or just “Events”. For marketing events, I’ll always say which platform it goes with, such as “Google Analytics Events”.

Marketing Events vs JS Events:

For marketers, an “event” firing usually means a Google Analytics event, Facebook Pixel event, or any other platform recording some sort of user action occurring. Usually these events are not triggered unless specifically set up, and in fact, that is a common use of GTM; use a trigger, such as a button click, to fire a Google Analytics Event.

However, in the Javascript web development world, “event” means something completely different. In JS, an event, or more accurately a “DOM event“, is a signal that is emitted by the browser’s JS APIs, which you can then “listen” for. A large amount of them are fired automatically – for example, if you submit a form, the form “submit” event is fired automatically. Browser DOM events are a vast and complicated topic, but the important things to remember for the context of this post is that:

  • DOM events happen whether or not you are listening for them
  • You cannot capture events that are outside the scope of where your code is running
  • A lot of GTM functionality is built upon JS events and listening for them

To visualize just how common and automatic JS events are, I’ve setup a small demo (opens in new tab). On the right side of the demo, you can see a stream of JS events as they happen in real time. On the left side, you can trigger these events to occur by moving your mouse, clicking on buttons, typing in fields, etc. For the full demo source, visit the codepen.


Triggers:

Page View:

Help page – link.

Trigger What it Does Javascript
Page View

Fires ASAP – basically as soon as GTM starts loading.

Since this fires ASAP, this would be the same as just putting JS code in the same place, or instead of your GTM embed snippet.

DOM Ready

Fires when the basic DOM structure of the page has been parsed. Images, styles, and other things could still be loading.

window.addEventListener('DOMContentLoaded',function(evt){ /* Do Something */ });

Or, since this won’t fire if the page has *already* hit DOM ready before this code ran, you can wrap it like this:

(function(){
    function handleEvent(){
        /* Do Something */
    }
    if (document.readyState==="interactive"){
        handleEvent();
    }
    else {
        window.addEventListener('DOMContentLoaded',handleEvent);
    }
})();
Window Loaded

Fires when the page has been fully loaded (waits for images, scripts, etc). Can take a while!

window.addEventListener('load',function(evt){ /* Do Something */ });

Or, since this won’t fire if the page has *already* hit DOM load before this code ran, you can wrap it like this:


(function(){
    function handleEvent(){
        /* Do Something */
    }
    if (document.readyState==="complete"){
        handleEvent();
    }
    else {
        window.addEventListener('load',handleEvent);
    }
})();

Click

Trigger What it Does Javascript
All Elements

Listens for any click DOM event.


document.addEventListener("click",function(evt){/* Do Something */});

Or, listen to a specific element



var elem = document.querySelector("#MyElement");
elem.addEventListener("click",function(evt){/* Do Something */});
Just Links

Listens for click events, but “pre” filters to click events that have fired on links.


var links = document.querySelectorAll("a");
for (var x=0; x<links.length; x++){
    links[x].addEventListener("click",function(evt){/* Do Something */});
}

User Engagement

Trigger What it Does Javascript
Element Visibility

This is an advanced trigger that really shows off the power of GTM. There are a lot of different options with this trigger, but the basics of what it does is checks a few different things about the element to see if it is visible: getComputedStyle to see if the element has the display property set to hidden, and getBoundingClientRect to see if the boundaries of the element are within the current view of the user.

This is not trivial to code out by hand, in a way that works across browsers and reliably. One option is to use JQuery and check visiblity whenever the page is scrolled:


// Note - this fires whenever the element comes into view, regardless if already fired.
(function(){
    function testVisible(){
        if($("#MyElement").is(":visible")){ /* Do something */ }
    }
    testVisible();
    $(window).on("scroll",testVisible);
})();

Or, you could follow one of these guides: here or here

Form Submission

Fires whenever a <form></form> is submitted. Listens for the “submit” JS event.


document.addEventListener("submit",function(evt){/* Do Something */});

More robust option – listen for specific form, and don’t submit it UNTIL the marketing tag has fired.



var myForm = document.querySelector("#ContactForm");
myForm.addEventListener("submit",function(evt){
    // Prevent form from being submitted right away
    evt.preventDefault();
    // Do something
    /* sendGoogleAnalyticsEvent("Form Submitted"); */
    // Use a delay to ensure the above marketing tag has finished
    setTimeout(function(){
        // Submit the form
        myForm.submit();
    },1000);
});
Scroll Depth

This allows you to configure tags to fire once the user has scrolled a certain amount down the page. Uses scrollTop and scrollLeft to determine offset from origin and whether or not the threshold has been reached.

A simple version that just waits until a threshold has been hit might look something like this:


(function(){
    // 50% percent as float
    var threshold = 0.5;
    var fired = false;
    function checkVerticalScroll(){
        if (!fired){
            var totalHeight = parseInt(getComputedStyle(document.body).height.replace("px","").replace("%",""),10);
            if (document.body.scrollTop > (totalHeight * threshold)){
                /* Do Something */
                fired = true;
            }
        }
    }
    document.addEventListener("scroll",checkVerticalScroll);
})();
YouTube Video

This trigger checks the page for any embedded YouTube Videos, and if they are there, allows you to fire tags based on a video play event (like start, pause, etc).

The way this works is a little complicated, since YouTube embeds are usually iFrames. First, it requires that all YouTube embed Iframes contain enablejsapi=1, which is why they have the checkbox “Add JavaScript API support to all YouTube videos”, which automatically adds that string to all embeds. Once an embed has that flag, the video will start sending cross-origin messages (via window.postMessage()) with event data, which GTM is listening for. Finally, GTM checks the event that is passed via postMessage, and parses what type of YouTube event it maps to (play, pause, etc.) and determines if your tag should fire.

Here is some demo code that should work as long as your embeds have the enablejsapi=1 flag turned on.


(function(){
    // Change to match your YouTube Iframe embed
    var ytEmbedId = "myvideo";

    if (document.querySelectorAll('script[src="https://www.youtube.com/iframe_api"]').length == 0){
        var tag = document.createElement('script');
        tag.src = "https://www.youtube.com/iframe_api";
        document.body.appendChild(tag);
    }
    window.onYouTubeIframeAPIReady = function(){
        console.log("YT Ready");
        var player = new YT.Player(ytEmbedId);
        player.addEventListener("onStateChange",function(evt){
            switch (evt.data) {
                case YT.PlayerState.UNSTARTED:
                    // Video start
                    /* Fire video start event */
                    break;
                case 0:
                    // Video Ended
                    /* Fire Video End Marketing Tag */
                    break;
                case 1:
                    // Video playing
                    break;
                case 2:
                    // Video paused
                    /* Fire Video Paused Marketing Tag */
                    break;
                default:
                    break;
            }
        });
    }
})();

Other

Trigger What it Does Javascript
Custom Event

This trigger is very specific to GTM – it can be used to fire a tag when a specific named event is pushed to the GTM DataLayer – which is kind of like a shared stream of events that exists in the page. Often, this kind of trigger is used when multiple vendors are working together to implement something.

So, for example, a shopping cart vendor can push an event to the DataLayer for when a user adds an item to the cart, and then the ad agency vendor can listen for that event with this custom event trigger and fire a GA conversion event when it is trigger.

If you are not using GTM in the first place, you wouldn’t really need a JS alternative to this. Just use native custom JS events! For multiple vendors working together, one vendor can use dispatchEvent() to send the event, and another can use addEventListener to listen for it.

However, if you do not have access to GTM, but another vendor *does*, and they want to share events with you by pushing to the dataLayer, you could still listen for their dataLayer despite not having a GTM login / access. You would basically replicate this trigger with some code like this:


(function(){
    // Hold ref to original push fn
    var originalPushFn = window.dataLayer.push;
    // Replace with custom push fn
    window.dataLayer.push = function(){
        // Pass arguments to orginal function - VERY IMPORTANT
        originalPushFn.apply(this,arguments);
        // Our own processing
        for (var x=0; x<arguments.length; x++){
            if (typeof(arguments[x])==="object"){
                filterDataLayerPush(arguments[x]);
            }
        }
        
    }
    function filterDataLayerPush(pushObj){
        // Handle
        // Example - waiting for event named "addToCart"
        if (pushObj["event"] === "addToCart"){
            /* Fire GA Add To Cart Marketing Tag */
        }
    }
})();
History Change

Lets you fire a tag when the URL changes in a non-standard way, without the browser actually navigating pages. The first way is through a “URL fragment” change. Most people know this as what follows the “hash mark” – e.g. example.com/subpage#subsectionalpha – where #subsectionalpha could change to #subsectionbeta without the page reloading. This is common on single-page-applications (SPAs). The second thing it listens for is the URL being changed through the HTML5 pushstate API. This is something newer and less common, but becoming more common with the rise of PWA (progress-web-apps).

For URL fragment / hash changes, the code is pretty simple. There is built in window JS event you can listen for:


window.addEventListener("hashchange",function(evt){
    if (location.hash === "#subsectionalpha"){
        /* Do Something */
    }
});

Unfortunately, there is not yet a good built-in JS event to listen for as an indicator of a pushState happening. The easiest current solution is to basically replace the global history.pushState function with your own function that scans the event and then passes it to the original function. If you look at the source code for GTM, you can see that is exactly what Google wrote their GTM JS code to do!

JavaScript Error

This gets triggered whenever an uncaught Javascript error occurs. The main use of this trigger is to capture the error data, and relay it to another platform that consolidates JS errors into some sort of dashboard, such as Sentry.io or Airbrake

Most error collection dashboards probably have SDKs or plug-n-play libraries to make capturing client errors easy, so I doubt you would have to do much hand-coding in terms of catching and relaying JS errors.

However, if you need to, there is a global window JS event that fires on uncaught JS errors for some browsers, which is aptly named “error“. Or refer to this site on a cross-browser approach.

Timer

This is pretty simple – fires a tag after a set amount of time has passed. Or fires it every __x__ amount of time.

Here is a quick demo of how you could implement this in JS.


(function(){
    // Configure time in MS and max fires
    var interval = 2000;
    var maxFires = 4;

    var timer;
    var fires = 0;
    timer = setInterval(function(){
        if (fires < maxFires){
            /* Do Something */
            fires++;
        }
        else {
            clearInterval(timer);
        }
    },interval);
})();
Trigger Group

A trigger group is just a way in GTM to combine multiple triggers together into one rule. Normally, when you assign triggers to a tag, they function in an “OR” configuration, meaning that the tag will fire if ANY of the triggers get triggered. However, if you assign a trigger group, the triggers in that group function in an “AND” configuration, meaning that ALL of the triggers must have been triggered in order for the tag to fire.

Example: You want to fire a “high value user” tag if a user scrolls below 50% of the page, AND spends at least 60 seconds on the site. You could enforce this by creating a trigger group that contains both a Timer Trigger as well as a Scroll Depth Trigger, and then assign that trigger group to the tag you want to fire.

This is specific to GTM, so there is no need for a JS equivalent. JS developers should already be familiar with basic logic, such as AND vs OR – a GTM trigger group is basically the same as doing IF(TRIGGER_A && TRIGGER_B && …){/* Do Something */}

GTM Extra: All ____ vs Some ___ (conditional operator)

On most triggers, there is an extra option to control firing that enables you to filter to a subset of the trigger. For example, on the “Click” trigger, there is an option that lets you pick between “All Clicks” and “Some Clicks”.

When you turn on the “Some” filter, you can narrow down by many built in variables and conditions. The list is pretty extensive (link to built-in variables), but here are some of the most useful ones and how they map to Javascript:

Condition Can be filtered on: What it Does Javascript
matches CSS selector

Element (Click,Form)

A CSS selector is a way to target an element based upon its properties that are mappable to how CSS gets applied to the element. An analogy could be selecting people based upon a description – “Pick all men in the room with blonde hair, glasses, and red shoes”. In the context of GTM, you might use this option to do something like filter link clicks to just click-to-call links, so you can fire a phone call marketing conversion tag. To do so, you could filter by “matches CSS selector”, and then put the value as ‘a[href^=”tel:”]’

Most browsers support the .matches() method, so this is easy in JS:


// Attach to all links
document.querySelectorAll("a").forEach(function(elem){elem.addEventListener("click",filterLinkClicks)});
// ...elsewhere in code
function filterLinkClicks(evt){
    console.log(evt);
    if(evt.target.matches('a[href^="tel:"]')){
        /* Do Something */
        // Fire click-to-call marketing conversion tag
    }
}
Contains / starts with / equals / etc.

Pretty much anything…

Since this can be used as an operator with pretty much any variable, this can mean a lot of different things. For example, when used with “Click Text” or “Form Text”, GTM is basically comparing all the text contained within the HTML element to the value you enter to compare against.

Again, there are tons of different combinations that this filter allows for, but none of them are very complicated to implement in JS. For example, no matter how many elements are nested within another element, you can get all the text that is contained within and check if it contains a string using element.innerText:


var searchString = "Find Me!";

// Case sensitive "contains"
if(myElement.innerText.includes(searchString)){
    /* Do Something */
}

// Alternative case-sensitive approach
if (myElement.innerText.indexOf(searchString) !== -1){
    /* Do Something */
}

// Case in-sensitive
if (myElement.innerText.toLowerCase().indexOf(searchString.toLowerCase()) !== -1){
    /* Do Something */
}

GTM Extra: Wait for Tags:

Another “extra” option that appears in multiple spots within GTM is the option to “wait for tags”. This is useful because sometimes a trigger, such as a link click, will actually take the user to a different website, and if that happens before the tag has a chance to fire, then the marketing event never gets processed. By turning on “wait for tags”, GTM will actually STOP the user-initiated event from happening (such as navigating to a different site via a link) UNTIL your tags have fired.

From a developer perspective, the way this is usually done is through event.preventDefault() and calling stopPropogation(). Then you do whatver you want (fire tags, log something), then resume the event by either re-emitting it without stopping it again, or calling the native handle (e.g. form.submit())

The risk, or downside to turning on this feature, is that interrupting the flow of JS events can cause issues with third-party libraries or your own codebase, depending on how various pieces of code are listening for events. Example.

Here is a demo of how you might fire off a tag when a user clicks a link to an external site, and stop them navigating to the new site until your tag has fired:


document.querySelectorAll("a[href]").forEach(function(link){
    // Only for external links
    if (link.hostname !== document.location.hostname){
        link.addEventListener("click",function(evt){
            // Only preventDefault on links opening in same tab
            if (link.getAttribute("target")!== "_blank"){
                // Stop browser from navigating to link
                evt.preventDefault();
                // Continue onwards to link destination after a delay of 0.25 seconds
                setTimeout(function(){
                    document.location.href = link.href;
                },250);
            }
            // If the link opens in the same tab, the code below has 250 ms to execute before it is too late (see above code)
            /* Do Something */
            // Fire Marketing Tag
        });
    }
});

Leave a Reply

Your email address will not be published.