Tracking Events in Google Analytics

Update (1/31/10): Scroll to the end of the post for an updated version of the code.

A few weeks ago I wrote a post about tracking events in Google Analytics with jQuery. The solution I proposed worked but there was definitely room for improvement. The idea was to tag the links you wanted to track in Analytics with certain classes. But why not just track them all automatically?

There are a number of jQuery plug-ins that can do this but I wanted something with as little code and as little overhead as possible.  After comparing the performance of a few different options for handling events, I decided to go with the lightweight non-jQuery event delegation option.

Here’s what I’m now using to track external, mailto, and download links in Analytics.

document.onclick = function(event) {
 
    event = event || window.event;
    var target = event.target || event.srcElement;
    var targetElement = target.tagName.toLowerCase();
 
    if (targetElement == "a") {
        var href = target.getAttribute("href");
        eventCheck(href);
    }
 
    function eventCheck(href){
        if ((href.match(/^https?\:/i)) && (!href.match(document.domain))) {
            _gaq.push(['_trackEvent', 'External', 'click', href]);
        } else if (href.match(/^mailto\:/i)) {
            _gaq.push(['_trackEvent', 'Email', 'click', href.substr(7)]);
        } else if (href.match(/^.*\.(pdf|jpg|png|gif|zip|mp3|txt|doc|rar)$/i)) {
            _gaq.push(['_trackEvent', 'Download', 'click', href]);
        }
    }
};

This listens for click events at the document level, retrieves the node type of the target, and then checks to see if it’s a link.  If so, we grab the href value and perform some simple regex on it.  If it matches any of our conditions for external, mailto, or download links, the event category and href value get pushed onto Google Analytics’ _gaq array.

Note that I’m using Analytics’ Asynchronous Tracking implementation.  If you’re using the standard tracking implementation you just have to replace these three lines:

_gaq.push(['_trackEvent', 'External', 'click', href]);
...
_gaq.push(['_trackEvent', 'Email', 'click', href.substr(7)]);
...
_gaq.push(['_trackEvent', 'Download', 'click', href]);

With these:

pageTracker._trackEvent('External', 'click', href);
...
pageTracker._trackEvent('Email', 'click', href.substr(7));
...
pageTracker._trackEvent('Download', 'click', href);

Remember that event delegation relies on event propagation – the event has to bubble up to the document level (or whatever ancestral level you’ve assigned your event handler to).  If you have another click handler on a parent node that stops the propagation (by using jQuery’s return false, for example), the event will not make its way to your document level handler.

One more thing to mention is that this assumes the event target is always the thing you want to track.  If you have something like <a href="#"><span>link text</span></a>, the span element will get passed as the event target and your link will not be tracked.  If you find yourself in this situation you can use jQuery’s .live() method, which gets around this by walking up the DOM to search for the nearest “interesting” element.

If you see anything I’ve overlooked or if you have any suggestions for improvement, please let me know in the comments.

Update (1/31/10)

In the comments Victor pointed out that file downloads hosted on external domains would be recorded as external links, not downloads.  To correct this I made a minor change to the code, forking the external URL check to first check if the URL looks like a download link.

Another minor flaw in the previous code was that internal links with different subdomains would be treated as external links.  Regex for matching all possible subdomains (assuming all possible TLDs) can get quite complicated.  So I haven’t solved that problem completely but I did improve it by allowing for both www and non-www internal links to be treated the same.

Of course you could get around that problem completely by hard-coding your main domain in the function, or wrapping it all in a function and passing your domain name in as a parameter.  Maybe I’ll work on that next.

Update (10/6/10)

Thanks to Jonas Trollvik for pointing out that this script causes a Javascript error on pages containing <A> elements without HREF attributes. This is fixed by checking for an HREF value before running the main function. Updated code below.

document.onclick = function(event) {
 
    event = event || window.event;
    var target = event.target || event.srcElement,
        targetElement = target.tagName.toLowerCase();
 
    if (targetElement == "a") {
        var href = target.getAttribute("href"),
            urlHost = document.domain.replace(/^www\./i,""),
            urlPattern = "^(?:https?:)?\/\/(?:(?:www)\.)?" + urlHost + "\/?";
        if (href) {
            eventCheck(href,urlPattern);
        }    
    }
 
    function eventCheck(href,urlPattern){
        if ((href.match(/^https?\:/i)) && (!href.match(urlPattern))){
            if (href.match(/^.*\.(pdf|jpg|png|gif|zip|mp3|txt|doc|rar|js|py)$/i)) {
                _gaq.push(['_trackEvent', 'Download', 'click', href]);
            } else {
                _gaq.push(['_trackEvent', 'External', 'click', href]);
            }
        } else if (href.match(/^mailto\:/i)) {
            _gaq.push(['_trackEvent', 'Email', 'click', href.substr(7)]);
        } else if (href.match(/^.*\.(pdf|jpg|png|gif|zip|mp3|txt|doc|rar|js|py)$/i)) {
            _gaq.push(['_trackEvent', 'Download', 'click', href]);
        }
    }
};
Both comments and pings are currently closed.

Discussion

Rob, thanks for another great post.

I just wanted to mention a plugin I recently released that might help, if you feel the need to throttle Google Analytics tracking requests in instances where many of them occur in rapid succession.

http://benalman.com/projects/jquery-message-queuing-plugin/

I also have a plugin that facilitates testing the internal- or external-ness of URLs, which might help identify which Google Analytics tracking requests to fire on a per-link basis.

http://benalman.com/projects/jquery-urlinternal-plugin/

And the reason that I mention these plugins is because I’ve used them before, in conjunction with the exact approach you’re using in this post, to manage Google Analytics tracking requests!

Hi Ben,
Thanks for the links! Your plugin for checking URL “externalness” is making me re-think (and feel insecure about) my somewhat crude (href.match(/^https?\:/i)) && (!href.match(document.domain)). =)

Rob:

Been looking for this. Thanks for sharing. Are you planning on updating it because of Rob’s code? Does yours work without any problems? One thing that struck me was, what if you have an internal link – for instance, I have a link to download my resume where the file is hosted on my own server. Looks like this wont get captured as a download but rather a click through to an external link based on the order in which your code is structured.

@Victor

I’ve been using this exact code since the beginning of the month without any problems. It’s true that the regex match for the external link could be a little more robust. For example, if you have internal links to different subdomains (including different www/non-www URLs) they will register as external links. This hasn’t been a problem for me (I’m pretty strict with using canonical URLs) but I understand it might be a concern for others.

You’re right that a download link on a different domain will be captured as an external link. In Analytics I usually don’t pay much attention to the event categories, only the labels, so this didn’t bother me. But it’s easy enough to fix by inserting one more if statement. I’ll take a look at the code and post an update, hopefully tonight. Thanks for the feedback.

Just checking in .. to see if you have your new shiny robust code! Thanks for your effort.

@Victor

Sorry for the delay. I just updated the post. Thanks for your patience!

David pavlicko

handy post- thanks for sharing.

I like the non-jquery solution you’ve done… one question though – Will it potentially suffer from a race condition?

I can’t see a delay anywhere and i’m guessing that any link will still be followed whilst this onclick code is being executed – meaning that there’s a risk the next page starts loading before __utm.gif is returned by GA.

Normally i’d add a
setTimeout(‘document.location = “‘ + link.href + ‘”‘, 150); to my clicktracking function and then a return false; to the onclick in the tag. Is that solution compatible with yours?

/James.

Hi James,
You’re correct about the potential race condition, but I think just about every tracking script is exposed to a potential race condition, no? (FWIW, I don’t think I mentioned it in the post but in my implementation of the script I’ve placed it inside the DOM ready function.) GA’s Async implementation allows activity to be recorded even before the GA script has loaded by storing the events in the _gaq array. So the race is between the execution of the click tracking function and the user’s clicking.

I believe your setTimeout method is compatible but I think there’s a better solution.

I noticed this code is not tracking file names with multiple dots. For example, I’m trying to track a large set of Fw PNG downloads on my site. The file names are like this:

ico.usb.fw.png

Is there a way around this?

Peter

Where shall I put your code in order for google analytics to record clicks?

thanks!

Recently, I extended Google Analytics to add download tracking for Google’s new Asynchronous Google Analytics Model. I call the extension Entourage.js:

http://techoctave.com/c7/posts/58-entourage-js-automatic-download-tracking-for-asynchronous-google-analytics

My approach was to handle each link’s onclick event. The intent was to reduce (if not eliminate) any cross-browser compatibility issues since the onclick event is fairly consistently interpreted.

It’s framework agnostic. Like you, I wanted developers to have the freedom to use the extension and still use whatever JavaScript framework they want.

I love jQuery, but I’ve always had much respect for the MooTools and Prototype.js community too. Didn’t want them to have to load jQuery just to track file downloads.