Using callbacks with Google Analytics, the safe way

Many users do not permit Google Analytics to track their every move on the internet. Does your site fall over for these users?

I spend a considerable amount of time writing code with Google Analytics (GA) to comprehensively track a user's interaction with my projects. Many implementation specific interactions often force your site logic to be tightly bound with your tracking code. We, might, be able to control our own site's code but what happens if GA doesn't function properly?

A common example of this is tracking users as they exit our site, such as a click on an Instagram link like the one featured on my site. I've seen a lot of developers attempt to quickly shoot off a request to GA before the page moves:

// Standard method
[].forEach.call(document.getElementsByClassName('track-social-instagram'), function (link) {
    link.addEventListener('click', function (e) {
         analytics.view('/external-link/social/instagram', null, function () {
            window.location.href = link.href;
        });
    });
});

Unfortunately, we have no control over when this code is run. This is important. If the request to GA isn't sent before the browser changes scope it will be dropped. GA won't know about that click and the statistics you're trying to gather are inaccurate at best.

If this statistic is important to your product design (if you're going to the effort to track it then it probably is) we need a solution. "So just wait until the tracking is done, then move on, right?". Luckily GA provides a callback method which will be called as soon as a send is safely tracked.

Note: this is also supported in the old ga.js, but all examples here are written for the newer analytics.js

The hitCallback can be applied to any 'send' track:

ga('send', 'page view', { 
    path: '/your/click/path', 
    hitCallback: function () { 
        window.location.href = 'http://instagr.am/jimmyhillis';
    });

Awesome, so we're done? Not quite. What we've done here is tie our website's functionality directly within the GA API. We are now reliant that their callback works for our site to function. If a user decides to block Google Analytics for privacy or organisation concerns (ghostery, firewalls, etc.) our entire site falls over. Whoops.

As smart people have recommended writing a wrapper for GA is a really great idea. There are existing libraries for this this out there, but I don't feel pulling an entire library is required for something like this.

I've written Tracker: a wrapper class, using vanilla JS, that encapsulates the basics. The important part here is the callback wrapper.

Tracker.prototype.callbackFallback = function (callback) {
    var self = this
      , is_done = false;
    if (typeof callback !== 'function') {
        return null;
    }
    // In `this.timeout` time force the callback
    setTimeout(function () {
        if (!is_done) {
            is_done = true;
            callback();
        }
    }, this.timeout);
    // Provide GA a safe callback method
    return function () {
        if (!is_done) {
            is_done = true;
            callback();
        }
    }
};

The way the fallback works is to assuming GA will return successfully, however in the situation where it does not we're going to allow our callback to run anyway. The wrapper function will run your callback after a timeout (500ms-1000ms should be fine for most situations) regardless of the response. This gives GA the chance to return naturally, but not block users without tracking support.

The method features an is_done flag to ensure the callback is never run more than once. This would save us when a tracking callback is used for something that naturally happens and does not stop our scripts from continuing.

Using a GA wrapper is always a very good idea. The code I've provided should be easy enough to adapt into any tracking implementation that you use and will make maintenance much nicer. Moving from analytics.js to ga.js in the last year would have been a lot easier if we had done this with every project.