Fun With Google Tag Manager: Part 1

ShareShare on Google+46Tweet about this on Twitter64Share on LinkedIn37Share on Facebook9

It’s time to dig into my tip library for some pretty cool things you can do with Google Tag Manager to enhance your site tagging. Some of these are macro magic through and through, some of these are best practices, and some of these are things that will make your life easier while managing a tag management solution.
Google Tag Manager tips
I’ve split this post into two parts to make it more Hobbit and less Lord Of the Rings length-wise.

Here’s what you’ll find within this first part:

  1. Unbind jQuery
  2. Undefined fields are not sent to GA
  3. Track copy event
  4. Errors as events

Keep your eyes peeled for the second part.

I know, the anticipation is probably killing you.

1. Unbind jQuery

I’m not a huge fan of jQuery. I mean, it’s definitely useful, since it simplifies a lot of the difficult and error-prone syntax that complex JavaScript entails. At the same time, I enjoy complex challenges, and learning the hard way has definitely made me a better developer.

In my experience, and this is definitely a very marginal view, jQuery introduces a certain degree of, erm, sloppiness. I know the “it’s too easy” argument is infantile, but in some cases the lack of transparency to what’s actually happening in the code can invite some pretty poor development work. In a way, jQuery is more about reading a manual than about actually creating code, and that sucks. Especially with event handlers, some development decisions are clearly influenced by how easy it is to bind the handlers to elements.

For Google Tag Manager, this is all too familiar. An innocuous return false; or e.stopPropagation() will prevent your GTM listeners from ever retrieving the DOM event. Often the only reason propagation is cancelled like this is the fact that the code was copy-pasted from some Stack Overflow discussion completely unrelated to your website’s markup.

OK, sorry for the rant. Here’s what I suggest. Whenever your GTM listener doesn’t fire when it should, open the JavaScript console and type the following line of code there…

if(typeof(jQuery)!=='undefined'){jQuery('body').find('*').off();}

…and press enter. Then try clicking or form submitting again.

What this simple line does is it unbinds all jQuery event handlers from all HTML elements in the document body. If your GTM listener works after this, it means that there’s some interfering jQuery script. The next step is to find the problematic script, and ask your developer to fix it so that propagation isn’t prevented.

(Be sure to check out my Chrome extension, where this feature is handily one button click away!).

Or you can choose the hacker route. If you find the interfering script, and if you find the function that’s grabbing your clicks or submits and preventing propagation, you can use a Custom HTML tag to fix it.

NOTE! This is a hack. Be sure to test, test and TEST before publishing a container where you unbind jQuery. Also, it wouldn’t hurt to consult with someone who has an understanding of just what event handlers are used on the site.

Let’s say you have a Back To Top button which is not sending clicks to GTM’s listeners. You find the code in one of the page assets, and it looks something like this:

jQuery('.top-of-page, a[href="#top"]').click(function () {
  jQuery('html, body').animate({
    scrollTop: 0
  }, 400);
  return false;
});

As you can see, the dreaded return false; is there. That’s what’s stopping the events from climbing up the DOM tree.

To fix this, create a new Custom HTML tag with the following code within:

<script>
  jQuery('.top-of-page, a[href="#top"]').off('click');
  jQuery('.top-of-page, a[href="#top"]').click(function(e) {
    e.preventDefault();
    jQuery('html, body').animate({
      scrollTop: 0
    }, 400);
  });
</script>

If jQuery and the problematic script are loaded in the <head> of your template, you can have this tag fire on {{url}} matches RegEx .*. If they’re loaded elsewhere (like in the footer of your page), you might need some other event rule such as {{event}} equals gtm.load.

On the first line, I unbind the original click handler. Then, I add e as an argument of the new click function. This e contains the event object, and I can then use its preventDefault() method to stop the original action of the click without stopping its propagation. Finally, I remove the return false; from the end of the function.

Naturally, you’ll need to test, test, test and then test some more whether this hack breaks down your entire site or not.

Of course, it’s not just jQuery that might interfere with your listeners. It might be vanilla JavaScript or some other library. This chapter focuses on jQuery simply because it’s so widely spread that most of the time it really is the source of the problem.

Back To Top

2. Undefined fields are not sent to GA

Here’s a fun fact about the Google Analytics API: if a field such as Custom Dimension is undefined when the GA endpoint is called, the Custom Dimension is left out of the payload. How is this fun? Well, it introduces an opportunity to make your tags a bit more dynamic.

It would be nice to get a list of all the fields that behave this way, and also how they behave with other return values such as false or 0.

So, let’s say you have a Custom Dimension such as blog author that you want to send with your page view tags. Naturally, you only set the blog author on blog pages. You don’t want to send a dummy value like “empty” or “n/a” on other pages on your site, so you’re absolutely certain that you need two tags: one when the blog author is populated (and can be sent as a Custom Dimension), and one on all the other pages (where the Custom Dimension will be left out of the tag).
undefined-custom-dimension
However, thanks to how the GA API works, you only need one tag. Just make sure that the macro which returns the Custom Dimension value resolves to undefined type when the dimension doesn’t have a value. There are a bunch of ways you can do this:

function() {
  var t;
  var y = "";
  return t; // returns undefined type
  return; // returns undefined type
  return undefined; // returns undefined type
  return false; // returns boolean type
  return y; // returns string type
  return x; // throws ReferenceError
}

As you can see, returning false, an empty string, or a variable which hasn’t been declared will not do.

The cool thing about GTM is that if you have a Data Layer Variable Macro which doesn’t resolve, it will be automatically set to undefined type, meaning a Custom Dimension which refers to this macro will not get sent. So the best way to navigate through this particular scenario is to have blog-author declared in the dataLayer declaration before the container snippet on the page template. If the page isn’t a blog page, then this declaration can just be left out.

<script>
  var dataLayer = [{'blog-author': 'Simo Ahava'}]; // Blog author declared, dimension will get sent
</script>
...
<script>
  var dataLayer = []; // Blog author not declared, dimension will not get sent
</script>

Then, when you create a Data Layer Variable Macro, on pages with blog-author in the array, it will fire the Custom Dimension with the blog author’s name as the value. On all other pages the dimension will not get sent.

Once you familiarize yourself with JavaScript and understand all the different ways how GTM and JavaScript resolve variables to undefined type, you can get really creative with this feature, making your tags even more leaner.

Back to top

3. Track copy event

Ever wondered how often people copy text using CTRL+C or some other means? Well, wonder no more, because now you can track it as an event!

Create this simple Custom HTML tag:

<script>
  var c = document.getElementById("entry-content");
  if(typeof(c)!=='undefined') {
    c.addEventListener('copy', function(evt) {
      dataLayer.push({'event': 'copy'});
    });
  }
</script>

This is a very simple script that pushes the dataLayer event ‘copy’ whenever someone copies something on your page within the HTML element with ID entry-content (e.g. <div id="entry-content"></div>).

Be sure to change the element whose content you want to track with the event listener to match the markup on your website.

You can make it more versatile by exploring the contents of the evt argument, which is the event object. Its target property contains the HTML element which was the target of the event. Here are some ideas:

...
dataLayer.push({'event': 'copy', 'copy-id': evt.target.id, 'copy-content': evt.target.innerHTML});
...

And so on. Note that evt.target stores the element where the copy event started. So if the copy action takes place over multiple paragraphs, headers, or spans, for example, you’ll only be able to observe the properties of the first element targeted by the action.

Back to top

4. Errors as events

Google Tag Manager has this cool thing called the JavaScript Error Listener. This fires whenever an unchecked exception occurs on the site. For example, consider the following:

var myText = "Hello!";
alert(myTxt); // Unchecked ReferenceError, will fire GTM's error listener

// Exception caught, will not fire GTM's error listener
try {
  alert(myTxt);
} catch(e) {
  console.log(e.message);
}

With GTM’s error listener, you can send each uncaught error as an event to Google Analytics. You can then analyze these events to find problems with your scripts. If you have a huge site, you might get hundreds of these errors, so you’ll need to adjust the code to perhaps only listen for certain pages or errors.
JavaScript errors on site
I’m hoping at some point we can have the listener fire only for errors propagated by GTM’s tags and macros. This way you can debug your GTM installation without having to worry about all the other errors on your website (even though you definitely should!).

To get the listener up and running, do the following:

  1. Create a new JavaScript Error Listener tag
  2. Have it fire on all pages (or some other rule of your choice)
  3. Create a new rule Event – gtm.pageError, where {{event}} equals gtm.pageError
  4. Create three new Data Layer Variable macros:
    1. {{Error – Message}}, where variable name is gtm.errorMessage
    2. {{Error – Line}}, where variable name is gtm.errorLineNumber
    3. {{Error – URL}}, where variable name is gtm.errorUrl
  5. Create a new Event tag and configure it to your liking
  6. Make sure the Event tag fires on the rule you created in (3)

JavaScript Error Event Tag
Remember to set the Non-Interaction Hit parameter of the Event tag to true. You don’t want to kill your bounce rate with errors on your site!

If you’re seeing a lot of Script error. hits, it means that the errors occur in JavaScript resources loaded from other domains. Due to browser / cross-domain security policies the actual error name and message are obfuscated.

Back to top

Conclusions

The second part of this post is on its way. I didn’t want to puke too much text into a simple tips & tricks post, but this one is 1800+ words already, so… :) Topics in the next post are:

  1. dataLayer declaration vs. dataLayer.push()
  2. How to stop a GTM timer listener
  3. Adopting a proper naming convention
  4. Annotating container versions with names and notes

Anyway, I love nothing better than to explore the versatility of Google Tag Manager. There’s so much you can do with JavaScript, DOM API, and the Google Tag Manager library!

All that’s required is a bit of know-how with JavaScript and the Document Object Model, a lot of creativity, and a lot of testing, previewing and debugging.

ShareShare on Google+46Tweet about this on Twitter64Share on LinkedIn37Share on Facebook9

Comments

  1. says

    Great post, Simo! Just one slight correction I’d suggest in the JavaScript error section. In step 4, you have “{{Error – URL}}, where variable name is gtm.errorUrl”, however you aren’t using it in your event tag. You’re using the {{url path}} instead. I believe you intended to use the {{Error – URL}} macro you created as it’s theoretically possible that the {{Error – URL}} could differ from the {{url path}}.

    • says

      Thanks Dan, good point, I fixed the image.

      However, I actually have the {{url path}} in my error tag because I just can’t get gtm.errorUrl to work. I’ve sent a query about this to the devs, so I’m expecting some resolution as soon as possible. As soon as I can confirm it actually works for me as well, I’ll change my own tag to what’s now in the screenshot.

      So let’s call it a fake for now, but under creative licence :)

    • says

      Whoops!

      Just tested this and realized that gtm.errorUrl actually reflects the Script URL when it’s an external script. This is more useful than {{url path}} which reflects the page the user was on when the script error occurred.

  2. says

    Hi Simo,
    Thanks a lot for the post and your blog in general: a gem!

    I believe you have a typo:
    gtm.errorLineNumer

    Should be:
    gtm.errorLineNumber

    Keep up the great work :)
    Max

  3. says

    This is how I implemented the text copy event:
    I like to know more about the parent tag of the text, concatenation the tagname, id and it’s classes.

    var c = document.getElementById(“entry-content”);
    if(typeof(c)!==’undefined’) {
    c.addEventListener(‘copy’, function(evt) {
    var classListArray = evt.srcElement.parentElement.classList;
    var arrayLength = classListArray.length;
    var classesString = ”;
    for (var i = 0; i < arrayLength; i++) {
    classesString = '.' + classListArray[i];
    }
    copy_parent = evt.srcElement.parentElement.tagName + ':#' + evt.srcElement.parentElement.id + '.' + classesString;
    dataLayer.push({'event': 'copy', 'copy-parent': copy_parent, 'copy-content': evt.target.innerText});
    });
    }

    I tried to loop through the evt.srcElement.parentElement.classList with .loop() but it didn’t work inside the tag manager code.

    Don’t forget the Google Analytics Event Tracking tag, rule and some macros to make this work …

    • says

      Hey Iwan,

      That’s a fun idea. I have some comments about the code:

      typeof(c) will never return undefined in this code, since document.getElementById always returns either an object reference or null. So the correct way to do this check and thus avoid reference errors is

      if(c) {…

      srcElement is proprietary to IE and even though it works with modern browsers you should use evt.target… instead as a best practice and to ensure best cross-browser functionality (you use evt.target later in the code).

      Shouldn’t the operation in the for…loop be classString += ‘.’+classListArray[i];? Otherwise your classString will end up having just the last class name in the classList array.

      loop() isn’t a native JS function so it wouldn’t work unless you have a library loaded which supports it or you create the function yourself.

      • says

        this function returns the selected text. I have change it a little to return just the text without html tags

        function getSelectionHtml() {
        var html = “”;
        if (typeof window.getSelection != “undefined”) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
        var container = document.createElement(“div”);
        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
        container.appendChild(sel.getRangeAt(i).cloneContents());
        }
        html = container.innerHTML; // change to .innerText for pure text content
        }
        } else if (typeof document.selection != "undefined") {
        if (document.selection.type == "Text") {
        html = document.selection.createRange().htmlText;
        }
        }
        return html;
        }

  4. Eric Goldsmith says

    I want to pull out a quote from your post to provide context for my comment:

    “you have a Custom Dimension such as blog author that you want to send with your page view tags. Naturally, you only set the blog author on blog pages. You don’t want to send a dummy value like “empty” or “n/a” on other pages on your site, … if you have a Data Layer Variable Macro which doesn’t resolve, it will be automatically set to undefined type, meaning a Custom Dimension which refers to this macro will not get sent.”

    An important reporting side-effect of this approach is that you cannot report on the absence of a custom dimension. For example, you can’t generate a list of pages that “don’t” contain a blog author custom dimension, by looking for the absence of the dimension – neither the reporting UI nor API will allow that kind of query.

    This has been my experience. I would love someone to prove me wrong and find a way!

    • says

      Hey Eric,

      Fair point, and very relevant for hit-level analyses. If the dimension is session-based, you can use GA custom segments to filter out sessions where the custom dimension matches RegEx .*

  5. says

    saludos no he podido implementar el codigo en tag manager

    si me puede colaborar con un paso a paso a ver donde esta el error

    dataLayer = [{
    ‘pageCategory': ‘signup’,
    ‘visitorType': ‘high-value’
    }];

    (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({‘gtm.start':
    new Date().getTime(),event:’gtm.js’});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!=’dataLayer’?’&l=’+l:”;j.async=true;j.src=
    ‘//www.googletagmanager.com/gtm.js?id=’+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,’script’,’dataLayer’,’GTM-xxxxx’);

    no me deja pasar muestra en este sector, como un error ‘dataLayer’?’&l=’

    • says

      You need to replace the container default code ‘GTM-xxxxx’ with your actual container code. Also, make sure that you don’t use unconventional single quotes (´) but rather proper, plain text single quotes (‘). It might help to copy-paste the code via a plain text editor.

      Also, please note that this is an English blog, and I have to resort to translation tools to understand Spanish. So please write comments in English only. Thank you.

  6. Iwan says

    After some weeks of data collection:
    “Track copy event” is pretty nice to understand the needs of your visitors – where you can write better or more extensive content to satisfy the needs and make them stay on your website.

  7. says

    What is the line of code in #1 that unbinds all jQuery event handlers from all HTML elements in the document body? It is no longer visible. Thanks!

    • says

      That’s strange, I see the code just fine.. Anyway, here’s the line:

      if(typeof(jQuery)!==’undefined’){jQuery(‘body’).find(‘*’).off();}

Leave a Reply

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

Please do not write HTML or other formatted code in your comments!