Your Analytics is Breaking your Website

That may sound surprising, and even counter-intuitive, but for a certain percentage of your users, your Analytics implementation is breaking your website.

An example

Take this example, which is the standard way of tracking outbound links as described in the official Google Analytics documentation:

<script type="text/javascript">
    /**
     * Function that tracks a click on an outbound link in Analytics.
     * This function takes a valid URL string as an argument, and uses that URL 
     * string as the event label. Setting the transport method to 'beacon' lets 
     * the hit be sent using 'navigator.sendBeacon' in browser that support it.
     */
    function trackOutboundLink(url) {
        ga('send', 'event', 'outbound', 'click', url, {
            'transport': 'beacon',
            'hitCallback': function() {
                document.location = url;
            }
        });
    }
</script>


<!-- Example: -->
<a href="https://example.com" onclick="trackOutboundLink('https://example.com'); return false;">
   Check out example.com
</a>

The problem

The problem with this implementation is that a certain percentage of your users will never navigate away from your page: clicking on the outbound link will have absolutely no effect.

Why?

Some users take their privacy to heart, and use extensions to prevent tracking of their online activities (such as Ghostery, AdBlock Plus, NoScript or uBlock Origin). These tools prevent loading of tracking pixels, analytics scripts and other ad-serving resources which all allow third-parties to following of their online habits.

Is this really a concern?

Yes, it is. Here are some install stats for these tools, as of May 1st, 2015:

While in the grand scheme of things, this might not seem like a concern, it is important to keep in mind what would happen if a site-breaking bug prevents a users from completing an action on your site.

Think about all the places where you use such tracking snippets:

All these actions could be broken for users of these extensions. That means potential loss of revenue for you, but most importantly, a user leaving your site for a competitor because it prevents him/her from completing their task. Even worse, these bugs target specifically people mindful of their online privacy — punishing them for being concerned about their online presence is nothing more than shameful.

The critical part to understand here is that although these tools would have prevented you from tracking their presence on your website, they would not have prevented the user from making a purchase on your site (or completing any of your intended micro/macro-conversions). It is your very own implementation that is creating this roadblock.

The very definition of shooting yourself in the foot.

How to prevent your Analytics implementation from breaking your website

Luckily, there is a fix for this.

For Analytics, the function tracking outbound links can be fixed like this:

/**!
 * Function that tracks a click on an outbound link in Google Analytics.
 * This function takes a valid URL string as an argument, and uses that URL string as the 
 * event label.
 *
 * In the case where the "analytics.js" library is not loaded (either because the event
 * was triggered too early in the page lifecycle or because the library was blocked by an
 * extension), redirect the user to the intended destination.
 *
 * @param {String} url The URL to navigate to after recording a Custom Event in Analytics.
 * @return {void}
 *
 * @author Philippe Sawicki (https://philippesawicki.com)
 * @copyright Copyright Philippe Sawicki (https://philippesawicki.com)
 */
function trackOutboundLink(url) {
    if (ga.q) {
        // Analytics has not loaded yet or was blocked:
        window.location = url;
    } else {
        e.preventDefault();

        ga('send', 'event', 'outbound', 'click', url, {
            'transport': 'beacon',
            'hitCallback': function() {
                window.location = url;
            }
        });
    }
}

This uses the ga.q property of the Google Analytics tracking snipped used to queue Events before the analytics.js library is loaded. Once the library is available (after being async-loaded), the items pushed to ga() are dequeued and the ga.q property is removed. Thus, if ga.q is still defined, it is either because the event has fired too soon in the page lifecycle or because the library was blocked. In this case, we simply redirect the user to the original location instead of waiting for the confirmation from Analytics that the Event was recorded.

Using ga.q is safe to use as a reference in the long run, because the property is created locally in the tracking snippet of each page — i.e., it is not injected by an external file. For Google to remove support for this property, it would mean having to force every single Analytics User to update the tracking snippet on each page of their sites.

Not something easily done.

Bonus: The Mordern Way of Tracking

Using onClick callbacks directly in the markup feels dirty, and does not help maintainability. CSS selector-based tracking is more flexible and easier to debug.

Here’s how (assuming IE9+ support):

<script type="text/javascript">
    /**
     * Optional "Element.matches()" polyfill for Internet Explorer.
     */
    if (!Element.prototype.matches && Element.prototype.msMatchesSelector) {
        Element.prototype.matches = Element.prototype.msMatchesSelector;
    }

    /**!
     * Prevents Analytics implementation from breaking site features due to
     * user-installed extensions like Ghostery.
     * 
     * Use event delegation to register click listeners on links with the
     * "js-ga-outbound-link" CSS class to capture outbound link clicks.
     *
     * @author Philippe Sawicki (https://philippesawicki.com)
     * @copyright Copyright Philippe Sawicki (https://philippesawicki.com)
     */
    document.body.addEventListener('click', e => {
        if (e.target && e.target.matches('.js-ga-outbond-link')) {
            const destinationURL = e.target.getAttribute('href');
            if (ga && !ga.q && typeof destinationURL === 'string') {
                e.preventDefault();

                ga('send', 'event', 'outbound', 'click', destinationURL, {
                    'transport': 'beacon',
                    'hitCallback': () => {
                        window.location = destinationURL;
                    }
                });
            }
        }
    });
</script>


<!-- Example: -->
<a href="https://example.com" class="js-ga-outbound-link">
   Check out example.com
</a>