My first week at Mixpanel, or how I didn’t take down the Internet
During my first week at Mixpanel I was asked to design, implement and deploy a highly requested feature in our core javascript library. I had just started as the new intern and I hit the ground running. Our customers wanted a simple method to track link clicks without having to hassle with browser incompatibilities or fiddle with event models. The new functionality would also lay the groundwork for future enhancements such as form integration. I got to work right away.
The Design Process
When designing a new feature like this, I aim to create a simple interface. I started the process by analyzing how a user currently implemented link tracking. The recommended pattern was the following:
<a href="http://example.com" class="my_link">Click Me</a>
<script type="text/javascript"> $('.my_link').click(function(event) { href = $(this).attr('href'); event.preventDefault(); mpmetrics.track('my event', {"my": "property"}, function() { window.location = href; }); });</script>This pattern has a couple of problems:
- It requires that the user knows to call preventDefault() on the event object
- It requires a selection library to be installed, in addition to our library
- It increases the time required to integrate with our library, and confuses some users
Next, I wrote up what I wanted the new interface to be:
<a href="http://example.com" id="my_link">Click Me</a>
<ul class="menu"> <li class="menu_item"> <a class="menu_link" href="about.html">menu link 1</a> </li> <li class="menu_item"> <a class="menu_link" href="contact.html">menu link 2</a> </li> <li class="menu_item"> <a class="menu_link" href="pricing.html">menu link 3</a> </li></ul>
<script type="text/javascript"> // properties should automatically include the href tag of the clicked link // additional properties should be optional
// interface should support a single id mpmetrics.track_links('#my_link', 'my event', {'my': 'property'});
// interface should support selecting by class mpmetrics.track_links('.menu_link', 'clicked menu item');
// we don't want to embed a full selection library, so let's support jQuery mpmetrics.track_links($('.menu a.menu_item'), 'clicked menu item');
// some people use other selection libraries than jQuery, so lets support nodeLists var all_links = document.getElementsByTagName('a'); mpmetrics.track_links(all_links, 'clicked link', {"a": "property"});</script>
I was careful to consider different ways a user might want to hook up link tracking, while also ensuring that we didn’t bloat the javascript with too many features. Because of our goal to have a slim library, I evaluated the cost of each additional feature to determine how to get the most functionality from the least amount of code.
One major design choice was to not include a full featured dom querying library. This choice meant that the interface would only be able to select by a single class/id without the help of jQuery/other selector engine. To help me make this decision, I considered embedding a number of selection engines including SizzleJS, Qwery and the Essential Selector Library. I then worked out the code required to implement a super simple selection engine (only selects by single class or single id):
function to_array(obj) { var l, i = 0, ret = []; if(obj === null || obj === undefined) { return []; } try { return Array.prototype.slice.call(obj, 0); } catch (err) { if (typeof(obj.length) === "number") { for (l = obj.length; i < l; i++) { ret[i] = obj[i]; } } else { while (obj[i] !== undefined) { ret[i] = obj[i]; i++; } } return ret; }}
function get_elements_by_class_name(className) { if (typeof(document.getElementsByClassName) === "undefined") { var hasClassName = new RegExp("(?:^|\\s)" + className + "(?:$|\\s)"); var allElements = document.getElementsByTagName("*"); var results = [];
var element, i; for (i = 0; (element = allElements[i]) != null; i++) { var elementClass = element.className; if (elementClass && elementClass.indexOf(className) != -1 && hasClassName.test(elementClass)) { results.push(element); } }
return results; } else { var elements = document.getElementsByClassName(className); return to_array(elements); }}
function get_element_by_id(q) { if (typeof(q) !== 'string') { return q; } if (document.getElementById === undefined) { if (document.all !== undefined) { return document.all[q]; } if (document.layers !== undefined) { return document.layers[q]; } return null; } else { return document.getElementById(q); }}
Based on the minimal amount of code required to implement our own simple selection library, I decided to go that route and allow users to pass in jQuery objects if they wanted to select something more complex.
Implementation
When writing code for a javascript library that is distributed across thousands of websites, cross-browser support is paramount. Because of this I had to make a second major decision: how I would handle the click event. After looking at event models in various browsers I decided to go with the traditional model. The primary reason behind this conclusion is that it is supported across every browser that I tested against. A very useful resource to use when dealing with the various javascript event models is the Introduction to Events chapter on Quirksmode.org.
To implement the actual mpmetrics.track_links() method I wrote an argument based router. The idea was to catch errors as early as possible, and fail gracefully. The router parses the arguments variable, and proxies the arguments to one of two functions based on the first argument. Javascript makes this pattern very elegant with language features such as apply().
var arg = (arguments.length > 0) ? arguments[0] : undefined;if (arg === undefined) { console.error("invalid arguments for track_links:", arguments);} else if (typeof(arg) === 'string' && is_dom_query(arg)) { return track_links_query.apply(this, arguments);} else if (is_list(arg)) { arguments[0] = to_array(arg); return track_dom_links.apply(this, arguments);} else { console.error("invalid arguments for track_links:", arguments);}
Another way our library fails gracefully is by ensuring that we wait only 300 ms for the Mixpanel servers to respond. In this way our user’s sites stay responsive even if our servers are having problems. I chose 300 ms based on a browser benchmark I wrote to evaluate the response time for the mpmetrics.track() method.
// callback is a function that transitions to the new url// the setup below ensures that we call the callback as soon as we hear// back from mixpanel, or if 300 ms has passed, whichever happens first.
var t = window.setTimeout(callback, 300);var cb = function() { // we need to clear the timeout if we hear back from mixpanel // otherwise we could trigger two page loads window.clearTimeout(t); callback();};
// we send the link href along with the eventif(typeof(properties.url) === 'undefined' && element.href) { properties.url = element.href;}
metrics.track(event_name, properties, cb);// prevent_default() ensures that the browser doesn't fire the default// click event (or it would go to the page without waiting for mixpanel)return prevent_default(e);
101 Big Ones

Go Time.
After a final code review with our lead front-end engineer, Tim Trefren, it was time to deploy. Seconds after our deploy scripts finished, thousands of browsers around the world downloaded and ran my code, no Internet apocalypse in sight. I never imagined that I would be pushing something of this magnitude out in my first week, let alone as an intern. Mixpanel has welcomed me as a core member of the team from day one, putting just as much trust on me as everyone else. I know that every day I come into the office I will be making a difference rather than just fixing bugs and writing tests. For a rewarding internship contact me and help us change the world of analytics.
“Enjoy the little things, for one day you may look back and realize they were the big things.”
~Robert Brault
Great post!
Is there a way to guarantee the tracking of the link with the asynchronous version of MixPanel? (i.e. mpq instead of mpmetrics)
Adam
21 Jul 11 at 3:17 pm