Inspecting and Debugging the Google Tag Manager dataLayer

the datalayer in action
ah data jokes…

The Data Layer Inspector and Debugger

This project began as a solution to a problem that I often faced setting up Google Tag Manager and specifically the data layer. The solution that I ended making was dataLayer logger, inspector, and debugger that offered the same features as the Google Analytics Debugger but for the data that was being held in or pushed to the dataLayer. It pretty-logs all the new contents of the dataLayer as it is pushed no matter if the dataLayer.push() happens on the page, in a script or in a custom HTML tag within GTM. Its all there as soon as it happens and it saves a fair bit of time.

datalayer debugger
Each object that is pushed to the dataLayer is logged like this.

One interesting difference between what I ended up with is that unlike the Chrome extension, this works across browsers (as far as I cared to test) and can be run as a standalone script, a custome HTML tag in GTM or even as bookmarklet. (Bare in mind I started this project about a month ago and, if you don’t already know, you will see why declaring that now makes sense shortly.) Also, Simo, if you ever read this, please integrate this into your Chrome extension.

The Google Tag Manager Debugging Problem

If you have ever worked with Google Tag Manager, you recognize that one of the most important, if not the most important feature of GTM is the dataLayer. This was a very conscious choice by the makers of GTM and other tag management companies to solve a problem that has arisen as websites have invariably become web apps with multiple players playing roles on a page, consuming and passing in data. That the data layer offered is a central stage for all the actors in the data game to meet and access the data that they require. It was an elegant solution to eliminated a lot of redundancy and has truly made data access and use a lot more agile.

The problem was that the Google Tag Manager interface, in its infancy was a bit less transparent than was ideal. GTM initially made claims that it would not require IT but that, for all but the most trivial cases was flat out untrue. In the beginning and still at the time that I write this, the process involves a fair bit of Javascript coding and hence, quality analysis and debugging that comes along with it. So if you have pushed to or pulled from the GTM dataLayer, you have entered this into your console more than once … many times more than one.

datalayer array
The tedious way to inspect the dataLayer.

You have inspected the dataLayer array and to see if that event was fired at the appropriate time or if all customer or event attributes made it through accurately. I did, quite often and that why I said, “These nine keystrokes, they can be automated.”

The Rabbit Hole of Problem Solving

Initially I thought it would be cool just to copy the style of the Google Analytics Chrome extension and just show every new push. But two things changed that initially course, this is not the same problem as that and I was getting really interested in the jQuery source. (Which is amazing if you every want to learn some really killer idioms.)

I learned about the Chrome Console API which as luck has it means I learned about the other cool Webkit browser, Firefox’s Console API. After that I realized I was half way there so I learned about decent but not quite as advanced Safari and IE console APIs. (Can you believe there are even cross browser considerations for the Javascript console!?)

So the main feature is just a nifty combo of the console.group method and string styling in the better two browsers to display the pushed object. This all goes on because the main change that the script makes is that it redefines the dataLayer.push method to wrap around the original GTM version of the dataLayer.push method. Once I got that far I had to ask myself why don’t I do this, why don’t I do that and so on until I ended up with a console API of sorts specifically for one, in my opinion conceptually significant, Javascript array. If you want to learn more about all the properties and methods of the Data Layer Debugger I will write a bit of an API reference for kick later. Otherwise the comments in the code do a decent job explaining the what everything does.

The Bookmarklet

If you want to try it out click the bookmarklet below and open up your Javascript console and click around a bit. You’ll see it working, hopefully as expected. If you want yours to keep, drag the bookmarklet into your bookmark bar and click it on any page with a dataLayer and watch the interaction take place.

dataLayer_debugger

The Custom HTML Script

Here is the GTM debugging script on GitHub. Its kind of useless now that the good folks at Google Tag Manager have baked this functionality in to the GTM interface. ( I like to think they saw my code on github and said, “that guy Trevor, he’s on to something…” Try it out, maybe you will find it useful.

(function (window) {

  'use strict'; // if you say so Mr. Crockford.

  // In case the dataLayer_debugger obj does not exist, make it.

  if (window.dataLayer_debugger === undefined) {

      // Set the value of the dataLayer_debugger variable
      // to the return value of an immediately evoked function.

      window.dataLayer_debugger = (function(){

      // Instantiate the main dldb object to be returned as mentioned above

      var dldb = {

        // Cache the dataLayer array.

        "dlCache" : window.dataLayer,

        // Cache the default dataLayer.push method.

        "pushCache" : window.dataLayer.push,

        // Acts as the on/of switch for on,off methods or book marklet.

        "keepState" : false,

        // Counts how many pushes occur.

        "pushCount" : window.dataLayer.length || 0,

        // time starts when GTM rule is fired or bookmarklet click

        "startTime" : new Date(),

        // to hold array of callback functions to call inside new .push method

        "callbacks" : [],

        // Does this debugger off cool logging features?

        "coolConsole" : navigator.userAgent.toLowerCase().match(/chrome|firefox/) ? true : false,

        // An object that holds all the current dataLayer values

        "current" : google_tag_manager.dataLayer

      };

      // Append methods to dldb object:

      // Returns time elapsed from startTime.
      // Used for timestamping dataLayer.push logs.

      dldb.now = function () {

        var now =  (new Date() - dldb.startTime) / 1000;

        return now;
      };

      // Returns whether the debugger is on or off.

      dldb.state = function () {

        var state = window.dataLayer_debugger.keepState ? "On" : "Off";

        return state;
      };


      // Turns the debugger on by changing the keepState variable,
      // changes the dataLayer.push method to add debugging functionality

      dldb.on = function () {

        dldb.keepState = true;
        window.dataLayer.push = window.dataLayer_debugger.push;

        console.log ("The dataLayer debugger is On");
        console.log ("Time is: " + window.dataLayer_debugger.now() + " To reset timer: dataLayer_debugger.reset('time')");

        for (var d = 0; d < window.dataLayer.length; d++){

          var obj = window.dataLayer[d],
          keys = Object.keys(obj);

          for (var k = 0; k < keys.length; k++){

            var key = keys[k],
            val = obj[key];

            // Set the new value of current

            //dldb.current[key] = val;
          }
        }

        window.dataLayer_debugger.log(window.dataLayer_debugger.current,"current dataLayer");

      };

      // Turns debugger off,
      // changes dataLayer.push method back to normal.

      dldb.off = function () {

        dldb.keepState = false;

        window.dataLayer = dldb.dlCache;
        window.dataLayer.push = dldb.pushCache;

        console.log("The dataLayer debugger is " + window.dataLayer_debugger.state() + ".");
        console.log("Current time is: " + window.dataLayer_debugger.now());
      };

      // Set one or many callback functions
      // to the debugging version of dataLayer.push method.

      dldb.setCallback = function (callback){

        window.dataLayer_debugger.callbacks.push(callback);
      };

      // Resets the timer, counter, and/or callbacks depending on arguments.
      // No arguments resets all- essentially the same as page refresh.

      dldb.reset = function () {

        for (var r = 0; r < arguments.length; r++){

          var arg = arguments[r];

          if (arg === "time") {

            dldb.startTime = new Date();

          }  if (arg === "count") {

            dldb.pushCount = 0;

          } else if (arg === "callbacks") {

            dldb.callbacks = [];

          } else {

            dldb.startTime = new Date();
            dldb.pushCount = 0;
            dldb.callbacks = [];

          }
        }
      };

      // Redefines the dataLayer.push method to add debugging functionality,
      // calls (applys) all the functions in the dldb.callbacks array,
      // calls the original dataLayer.push methon on all function arguments.

      dldb.push = function () {

        for (var a = 0; a < arguments.length; a++) {

          var obj = arguments[a];


          dldb.pushCount += 1;

          console.group( 'dataLayer.push: #' + dldb.pushCount + ", Time: " + window.dataLayer_debugger.now());

          window.dataLayer_debugger.log(obj,window.dataLayer_debugger.pushCount);

          console.groupEnd();
          console.dir(window.dataLayer);

          window.dataLayer_debugger.validate(obj, ['transactionTotal','transactionId'], ['transactionAffiliation', 'transactionShipping', 'transactionTax', 'transactionProducts'], "transaction");
          window.dataLayer_debugger.validate(obj, ['network','socialAction'],[], "social");

          // Call all callbacks within the context of the pushed object.

          if (window.dataLayer_debugger.callbacks){

            var callbacks = window.dataLayer_debugger.callbacks;

            for (var j = 0; j < callbacks.length; j++) {

              var callback = callbacks[j];

              callback.apply(obj);

            }
          }

          // Calls original cached version of dataLayer.push.

          dldb.pushCache(obj);
        }
      };

      // Pretty-logs an object's contents.

      dldb.log = function (object, optName){

        var ks = Object.keys(object).sort();

        for (var v = 0; v < ks.length; v++){

          if (ks[v] === 'event'){

            ks.unshift(ks.splice(v,1)[0]);
          }
        }

        console.group("object: " + (optName || ""));

        // Check for "event" property.
        // Put "event" property of pushed object first in list.

        try {

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

            var key = ks[i],
            val = object[key],
            space = 25 - key.length;


            var logMessage = key + new Array(space).join(' ') + ': ';

            if (window.dataLayer_debugger.coolConsole) {

              var valType = (typeof(val) === 'object') ? '%O' : '%s';

              console.log( logMessage + valType, val);

            } else {

              console.log( logMessage + (typeof(val) !== 'object' ? val : "") );

              if (typeof(val) === 'object') {

                console.dir(val);

              }
            }

            // Set the new value of current

            dldb.current[key] = val;
          }
        }

        catch (e) {

          console.log("dataLayer error: " + e.message);

          if (window.dataLayer_debugger.coolConsole) {

            console.log("object was %O", object);

          } else {

            console.log( "object was:" );
            console.dir(val);
          }
        }
        console.groupEnd();
      };

      // Validates an object against an array mandatory valid key
      // and a second optional array of optional keys.
      // The last argument is the is a string name of the type of objec. ie 'social'

      dldb.validate = function (testObj, validKeys, optKeys, type) {

        var checked,
        validKey,
        optKey,
        checkedKeys = [],
        checks = validKeys.length > optKeys.length ? validKeys : optKeys;


        for (var j = 0; j < checks; j++) {

          validKey = validKeys[j];

          if (testObj[validKey]){

            checked = validKeys.splice(j,1);

            checkedKeys.push(checked);
          }

          else if (optKeys && testObj[optKey]) {

            optKey = optKey[j];

            checked = optKeys.splice(j,1);

            checkedKeys.push(checked);

            if (optKey === "transactionProducts" ) {

              var products = testObj.transactionProducts;

              for (var p = 0; p < products.length; p++) {

                var product = products[p];

                window.dataLayer_debugger.validate(product, ["name","sku","price","quantity"],  "product");

              }
            }
          }
        }

        if (validKeys.length) {

          console.log("Invalid " + (type ? type + " " : "") + "object pushed to dataLayer. Missing: " + validKeys.join(", "));

          return false;

        } else if (optKeys.length) {

          console.log("Valid " + (type ? type + " " : "") + "object pushed to dataLayer. Optional keys not used: " + validKeys.join(", "));
        }

        return true;
      };

      // End method definitions and return the created object.

      return dldb;

    })();

  }

  // Logic responsible for turning dataLayer_debugger on/off.

  if (!window.dataLayer_debugger.keepState) {

    window.dataLayer_debugger.on();

  }
  else {

    window.dataLayer_debugger.off();
  }

}(window));

 

Leave a Comment

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