Why you 𝒅𝒐𝒏’𝒕 need to check if GTM’s window.dataLayer was blocked by an ad blocker

The standard pattern for logging Marketo form fills to GTM Data Layer is like so:

MktoForms2.whenReady(function(readyForm){
  readyForm.onSuccess(function(submittedValues, followUpUrl) {
    window.dataLayer.push({
        "event": "mkto.form.success",
        "mkto.form.values": submittedValues,
        "mkto.form.followUpUrl": followUpUrl,
        "eventCallback": function () {
            document.location.href = followUpUrl;
        },
        "eventTimeout": 3000
    });
    return false;
  });
});

As you probably know, returning false from the onSuccess listener stops Marketo from immediately redirecting. Then GTM handles the redirect when its work is done. (That’s assuming you want to go to the original followUpUrl. If not, tweak the eventCallback to do something fancier.)

The eventTimeout means if GTM doesn’t get confirmation of the Data Layer update in 3s, run the eventCallback anyway. That way an internal GTM problem won’t cause a user-facing error.

What’s notable is this snippet doesn’t have a bug. Quite the opposite: the code is resilient, even if GTM is blocked by an ad blocker. Which, when you think about it, doesn’t make sense!

If the main gtm.js doesn’t load, how the heck does the eventCallback fire?

It still fires because ad blockers include their own “callback only” version of window.dataLayer.

To deploy GTM, you include the standard GTM bootstrap (i.e. embed code) on your page:

<!-- Google Tag Manager -->
<script>(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=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-BLAHBLAH');</script>
<!-- End Google Tag Manager -->

The bootstrap creates a dumb version of the window.dataLayer variable, which is just a JavaScript array. This version doesn’t do anything but store events for the real GTM library to process when it’s ready.[1] It’s incapable of running callbacks or timing out. Basically, it sits there waiting for gtm.js.

Not all ad blockers block the core gtm.js library: as of this writing, AdBlock Plus doesn’t block it, but uBlock Origin and Ghostery do.[2] When they do block www.googletagmanager.com, you’ll never see Data Layer Events (that’s the intent of blocking it, after all). But this section of the uBlock Origin extension code shows how they keep things otherwise running:

    const dl = w.dataLayer;
    if ( dl instanceof Object ) {
        if ( dl.hide instanceof Object && typeof dl.hide.end === 'function' ) {
            dl.hide.end();
            dl.hide.end = ()=>{};
        }
        if ( typeof dl.push === 'function' ) {
            const doCallback = function(item) {
                if ( item instanceof Object === false ) { return; }
                if ( typeof item.eventCallback !== 'function' ) { return; }
                setTimeout(item.eventCallback, 1);
                item.eventCallback = ()=>{};
            };
            dl.push = new Proxy(dl.push, {
                apply: function(target, thisArg, args) {
                    doCallback(args[0]);
                    return Reflect.apply(target, thisArg, args);
                }
            });
            if ( Array.isArray(dl) ) {
                const q = dl.slice();
                for ( const item of q ) {
                    doCallback(item);
                }
            }
        }
    }

They check whether the “dumb” version of window.dataLayer exists — that is, whether you included the GTM bootstrap. If so, using a JavaScript Proxy, they substitute a more capable “callback only” version that just runs any eventCallback after a millisecond. Pretty cool, I think!

Notes

[1] window.dataLayer.push() works immediately after the bootstrap code, even though gtm.js isn’t loaded yet, because you’re calling native Array.prototype.push! gtm.js eventually replaces window.dataLayer with another array whose push property is overwritten with a custom function.

[2] Ghostery uses uBlock Origin’s open source code internally, so not surprising.