PyCon 2013 Coding Challenge Roundup

Thumbtack sunglasses and mug

This year’s PyCon was again, extraordinary. Our heartfelt thanks go out to the entire community for such an amazing event. We especially enjoyed getting to know other engineers from all over the world.

In fact, we’ve found that one of the best ways to start a conversation with an engineer is to start by talking about code. Thumbtack’s second annual code challenge gave us a great opportunity to do this.

Last year’s challenge was to determine whether there was a winner on a given Connect 4 game board. This year’s challenge required looking for anagrams of word pairs in large bodies of text. Big thanks to Jeff Kramer, who posted the problem description online.

In response to feedback from last year’s challenge, this year’s anagram challenge had two components: an easier challenge that required finding anagrams in a paragraph of text, and a harder challenge that looked for anagrams in the full corpus of Alice in Wonderland.

We offered two prize sets to match the two difficulty levels: a mug and sunglasses for the easier challenge, and a beer mug and shot glass for the harder challenge.

Solutions

The general form of solutions to this year’s challenge was fairly consistent. In fact, there were no solutions that deviated from this general pattern:

  1. Parse the input text to filter and transform input words
  2. Remove duplicate words (can be done at this point or later)
  3. Find all possible anagram pairs of those words (often using itertools.combinations())
  4. Create a data structure to hold matching anagram pairs, usually keyed off the sorted letters of the pair

We received 53 total (working) solutions to the Alice In Wonderland challenge.

The best solutions were all on the order of O(N2) (where N is the number of unique words in the corpus), while slower solutions were often O(N4). The difference was almost entirely due to the data structures used in step 4: faster solutions used a dictionary (search is constant time), while slower solutions used a list (search takes time O(N2), where search is linear on the number of word pairs).

We tested all the solutions on a modern Mac laptop and pulled out some of the fastest solutions to highlight here. O(N2) solutions generally run in the 15-40s range. We see a massive spike (literally, off this chart) for slower solutions.

anagram challenge speeds

Hall of Fame

Chris Moultrie’s solution was the fastest of all the solutions we received. On our testing machine, his solution to the Alice problem routinely ran in under 12 seconds. There are several clever optimizations specific to the challenge, so, while this is not a general-purpose solution, it is extremely quick. (Find Chris on GitHub and Twitter.)

Second place for speed goes to Łukasz Balcerzak. His solution routinely took ~13.5s. This is a more general-purpose solution that is also well-factored and readable. (Find Łukasz on GitHub and Twitter.)

Samuel Merritt’s solution is also commendable for its speed (ranked in the top 5) as well as its clarity and excellent word choice. (Find Samuel on GitHub and Twitter.)

Congratulations to everyone who participated! If you submitted your solution but were not able to claim your prize at PyCon, please email us and we’ll try to get your prize to you.

We had a great time with this challenge and hope to see you all again at next year’s PyCon in Montreal.

The usability experts were right: 5 changes we made to increase conversion by 50%

At Thumbtack we use A/B testing on just about everything we do. Integrating A/B testing into your product workflow allows you to quickly iterate and get feedback to create an awesome product. With each added feature, you know what effect it had on conversion, time on site, click-through, or whatever other metric you care about, which helps you focus in on the important factors and leads to great results. Over the past couple months, we’ve used A/B testing on a prominent form on our site to increase conversion by more than 50%. Through this process we’ve learned a lot about what makes a good form and what to avoid.

The form in question is what every user has to get through to place a request on our site. It asks several category-specific questions regarding the service you want and then asks for your contact information. Here’s what it might look like if you were trying to hire a DJ:

In this post, I will outline the top five changes that came up in our testing and can be generally applied to any form.

1. Use standard form elements

When creating any interactive elements, there are many reasons you may want to make your own GUI controls that better support that particular interaction. Jakob Nielsen lists this as his number 1 application-design mistake, saying,

Users will most likely fail if you deviate from expectations on something as basic as the controls to operate a UI. And, even if they don’t fail, they’ll expend substantial brainpower trying to operate something that shouldn’t require a second thought

And now we have our own data to back that up.

‘Faux’ Selects

Standard Form Elements

With the previous version of the form, we attempted to make the form take up less screen space by creating ‘faux’ select inputs that relied on some fancy Backbone views to do their magic. The rationale behind this was that a smaller form would be less daunting and encourage more users to place a request.

However, when we started running A/B tests on the form we found that replacing these ‘faux’ selects with standard form elements increased conversion site-wide, with some pages’ conversion going up 20%.

Why were the standard elements so much better? With the faux selects we failed to take into account the difficulty of learning a new interaction model. Our interaction was especially tricky because it looked like the standard select buttons that people had seen before, but behaved differently. This difficulty isn’t necessarily noticeable on a conscious level, but every little frustration decreases your user’s reservoir of goodwill (see: Don’t Make Me Think) and their chance of converting.

The standard form elements also have the benefit of being easier to maintain, so this was a clear winner for us.

2. Keep it simple

Your site may offer many different services, but once a user is filling out a form it is usually pretty clear what they want to do. You should make it simple for them to complete that task and avoid distracting them. This is on Steve Krug’s (Don’t Make Me Think) list of things that increase goodwill: “know the main things that people want to do on your site and make them obvious and easy.”

We have tried adding several features to the form that we thought would be helpful, but ended up decreasing conversion or showing no significant change. One such feature, outlined in red above, was intended to give the user more context while they were requesting a specific service pro. The problem with most of these is that they distracted from the flow of the form. We don’t keep features like these unless they show an obvious performance increase, since each new feature complicates the user’s experience. Our most successful changes have been changes that integrate naturally into the flow of placing a request.

3. Optimize for mobile

Mobile traffic now constitutes a significant percentage of total traffic for many websites and will only continue to grow. Not optimizing for mobile makes the mobile experience difficult and clumsy, preventing many users from converting on your site. Even if you don’t see much mobile traffic yet, Luke Wroblewski warns, “don’t wait too long to change as the shift from desktop to mobile can happen faster than you think.”

Since we introduced a mobile-optimized version of our form, we’ve seen a 40% increase in conversions on mobile. Furthermore, designing for mobile forces you to rethink your form and simplify the experience, which leads to ideas for improving the desktop version as well. Many of the tests we ran on the desktop form were inspired by our experience designing the mobile version.

If a significant portion of your traffic is mobile, this is a no-brainer.

4. Only ask for what you need

Users are hesitant to give away personal information unless it is clear why you need it. Think carefully about what information you need and then present the fields in such a way that it is clear why you need it. “Not Indicating How Info Will Be Used” is number 9 on Jakob Nielsen’s list of top application-design mistakes and “Asking me for information you don’t really need” appears on Don’t Make Me Think’s list of things that diminish goodwill.

We used to require a phone number for every request, even if the user opted not to share their phone number with our service pros. The phone number was useful to verify the user’s identity, but from a user’s perspective we were asking for information that was irrelevant to their request.

We tested this against a version where the phone number was optional and a dynamic version where the phone number field would only appear if they opted to receive quotes by phone. Incredibly, the dynamic version showed a 15% increase in requests while the optional phone number only showed a 4% increase. Although both of the new versions effectively made the phone number optional, the dynamic version created an intuitive interface that made it clear to the user why they were being asked for this information.

5. Test everything

We like to A/B test everything. There were many features that we thought were obvious improvements but turned out to decrease conversion or make no difference. We have to remind ourselves that our users are not us; they often use things differently than we do, so we not only test everything, but we also try to understand why particular changes increase or decrease conversion.

Since we use A/B testing so extensively, we built a tool called ABBA for analyzing the results of A/B tests. This lets us check exactly how each test affects our metrics and tells us how confident we are that the observed changes aren’t just due to chance. The source is also available.

Of course, conversion isn’t everything. You also need to focus on the quality of the request you’re receiving and the quality of the user’s experience. We have tested features that showed increases in conversion but ended up pulling them because we believed they compromised the quality of the user’s experience and the tradeoff wasn’t worth it. These are difficult decisions to make, but A/B testing will at least give you the data you need to make an educated decision.

Conclusion

Since we began working on the latest version of our request form, we’ve seen our conversion rate increase by more than 50%. We hope that these tips will help you create better forms and see similar improvements on your site.

How to improve A/B testing with Mixpanel

Introduction

We enjoy using Mixpanel and enjoy A/B testing. In this post we show how to use Mixpanel’s API and our own open-sourced A/B testing statistics package to easily create A/B tests in Mixpanel.

TL;DR

Setting up an A/B test with Mixpanel

Setting up an experiment in Mixpanel is easy. Simple add any experiment properties to the events you’re triggering.

Taking a cue from the Mixpanel docs, let’s say we’re tracking signup events on new users:

mixpanel.track('signup', {
    'name': 'Joe Schmoe',
    ‘email’: ‘joe@schmoe.com’
});

To add experiment data, we add a property to the event indicating which version of the experiment (“baseline” or “alpha” in this case) the user encountered:

mixpanel.track('signup', {
    'name': 'Joe Schmoe',
    ‘email’: ‘joe@schmoe.com’,
    ‘experiments:signup_form_version’: ‘alpha’
});

Now, in Mixpanel funnels, we will be able to open the relevant funnel and slice the data based on the experiment key:

(Note: this is not real data)

This sliced view is great. We can see that these two buckets are both seeing a lot of traffic, and the “alpha” version seems to be performing quite well.

The trick with A/B testing is understanding the statistical properties of any experiment. Are we confident that the “alpha” bucket is actually winning, or is our mind playing tricks on us? Maybe that “alpha” bucket just got lucky.

At Thumbtack, we built a tool called ABBA to understand the results of A/B experiments. In the example of the signup form, we can diligently copy over the numbers and see a pretty chart. It would seem that the “alpha” signup form is performing extremely well. The low p-value indicates we can be very confident in our choice to redesign that form.

A Python script to make things easier

This whole process can be very tedious if you have a long-running experiment, or are running many experiments at the same time. (At Thumbtack, our team might easily have dozens of experiments running at any given time.)

Thankfully, Mixpanel has an API. The Python script below uses the API to pull funnel data for a given period of days, slices that data by the experiment key in question, then opens a browser with a visualization of the experiment results.

Example usage, to look at the past 10 days of the signup form experiment:

./experiment.py 123456 experiments:signup_form_version 10

The funnel ID can be found in the Mixpanel URL for any any funnel using the funnels/list endpoint

https://mixpanel.com/docs/api-documentation/data-export-api#funnels-list

Alternatively: you can find the funnel ID in the URL:

And the script itself:

Conclusion

We hope this little script can be useful for you as you run your own A/B tests with Mixpanel.

Updates

10/17/2012 – Updated funnel ID screenshot/commentary to match Mixpanel’s recent URL changes.

Welcome our newest engineer, Tommy

We’re excited that Tommy Saylor is joining the engineering team. When he’s not getting introspective in park-bench photographs, you can find him pondering webby buzzwords like HTML5 and CSS3 over on his blog. You can also find him at his portfolio, Github, and Twitter.

In a past life, he claims he was a cartoonist and a musician. Whether you’ll see his illustrations on this blog remains an open debate, but we’re confident the Thumbtack Band will be much improved with Tommy’s mean ukelele.

Tommy is returning to his native California to join Thumbtack after many years in Indiana and Texas. Welcome home Tommy!

Welcome our newest engineer, Ben

Ben is our second Carl in 7 total engineers, having graduated from Carleton College this spring. He majored in math and was especially interested in combinatorics. Thumbtack is officially a hot destination for Carleton graduates, in the past 3 years we’ve scooped up nearly 2% of all Math/CS graduates! While at Carleton, Ben not only focused on the technical realm, but also taught swing dance classes.

He hasn’t yet been indoctrinated into the silicon valley twitter phenomenon, but when he does you will find his nuggets of wisdom at @itsbanderson.

Welcome to the team!

Welcome our newest engineer, Jeremy

Jeremy just graduated from Appalachian State this spring, and moved across the US from Boone, NC to San Francisco to work at Thumbtack. He is an avid photographer (check out his blog for some of his work), and we are all excited to see how many different ways he can photograph the San Francisco fog. Jeremy is a Linux user, and unusually enough is both a vim and emacs user. He switches between them for different languages and tasks and says he uses whichever one is best suited for the task at hand.

In addition to all of his technical skills, we are very happy that Jeremy will be able to anchor the defense of our engineering basketball team, as he stands at a lofty 6’4″ and has taken over the title of tallest Thumbtack-er by a couple of inches.

Welcome to the team!

Know your latency: a simple hack using Graphite and Memcache

“You know,” said Arthur, “it’s at times like this, when I’m trapped in a Vogon airlock with a man from Betelgeuse, and about to die of asphyxiation in deep space that I really wish I’d listened to what my mother told me when I was young.”

“Why, what did she tell you?”

“I don’t know, I didn’t listen.”

- The Hitchhiker’s Guide to the Galaxy

Application profiling is good


Total page latency measure in milliseconds shows time from initial HTTP GET to window.onLoad event. Mobile browsers suffer the worst slowdowns due to slower processors and network latency.

At Thumbtack, we have a healthy obsession with tracking. We track everything. Hardware usage, network throughput, database queries, logins, signups, template rendering times, email send volume, and much more. During every deploy we closely monitor a wide range of metrics to ensure that the numbers are looking good across the board. When you’re working on a large web application, it’s impossible to have complete test coverage, and we rely on these metrics to help us quickly catch and diagnose problems.

To make all this possible, we first had to build a monitoring system. For this, we took inspiration from Etsy’s statsd tool. We built a similar tracking system called Tycho, written in Python and fully distributable across all our servers in various datacenters, EC2, etc. Like Etsy, we added a front-end to our tracking system that is based on Graphite. Ours is called Observatory, and is a simple wrapper around Graphite that serves a collection of dashboards.

Profiling the whole application, Javascript included

Now, the interesting part. At Thumbtack, we’ve long tracked our server response times. But what we really wanted to know was how long it took a user to go from an initial request to having a fully rendered page, domReady and all necessary Javascript loaded up from the CDN.

Our answer is to combine Memcached and some jQuery callbacks to track total application responsiveness.

There are three simple steps.

  1. Store a unique key + timestamp in Memcache when the server starts responding to a new GET request.
  2. When the page is fully rendered, issue an AJAX request containing the same unique key back to the server.
  3. Look up the initial timestamp based on the key, and track the total time from GET to page load.

Here’s some pseudo-code.

start_time = now()
request_id = unique_id()
template = new Template('homepage.html')
template.set('request_id', request_id)
http.respond(template.render())
end_time = now()

// now, persist the data so we can grab it later
// assumes a reasonable TTL - 5 minutes
memcache = new Memcache()
memcache.save(request_id, {
     page_type: 'homepage',
     start: start_time,
})

// do any traditional server-side tracking
tracking = new Tracking()
tracking.track('homepage', 'server_time', end_time - start_time)

Now, in the homepage.html template, we’ll trigger an AJAX call once the page is loaded. Note that you probably don’t need to do this on domReady (though tracking that would certainly be interesting), but on a later event that indicates the page is fully loaded and the user can start interacting with it.

<script>
$(window).load(function() { // replace with something more appropriate to your application
    $.post('/responsiveness', {request_id: request_id});
});
</script>

The /responsiveness endpoint looks up the timing information we saved previously, then tracks the new timing information:

memcache = new Memcache()
tracking = new Tracking()
data = memcache.fetch(request_id)
tracking.track(data.page_type, 'total_time_to_window_onload', now() - data.start_time)

And there you go: a simple hack to track total responsiveness for your application.

Why Memcache? We chose Memcache instead of another data storage option for several reasons. We needed temporary persistence with a short TTL (a few minutes). Transient failures are acceptable since responsiveness tracking is purely best-effort. The amount of data we need to store is small: a key (a unique ID for each page), and a value (a timestamp). A relational database or NoSQL storage would both be overkill for this feature, and don’t support temporary storage as easily or as efficiently. Memcache was the much better solution for all these reasons.

Conclusion

What have we learned at Thumbtack? We’re learning to tune our Javascript to better perform in different browsers. We’ve learned that mobile browsers are slower at executing Javascript than we’d realized, and we’re working on solutions for that. Sometimes we find bugs.


Tracking response times proves that caching is king

We’ve learned the landscape of page performance across the site, and have identified places with particularly weak performance. For example, the “welcome” page for new service providers is one of the slowest performers, even though it is one of our “simpler” pages.

As Thumbtack’s Javascript codebase grows, tracking JS responsiveness helps us understand when new client-side code disrupts the user experience. For example, we recently updated some of our maps to use the Google Static Maps API rather than the Javascript API; we tested this and discovered a substantial speed improvement.

This approach has some nice perks:

  • Integrates server and client side processing times to give a more accurate picture of the full user experience.
  • Easily expandable to track a variety of additional metrics especially on the client side.
  • Lightweight, minimal interference with existing code on both the server and the client.

Interested in working on interesting problems like client-side Javascript performance? Thumbtack is hiring product engineers to help build amazing user interfaces.

Welcome our newest engineer, Alex

Alex took quite the journey to arrive here at Thumbtack.  He was born in Azerbaijan, studied in Russia, became an Australian citizen, who was living in Malaysia when he joined Thumbtack and just moved to San Francisco.  Alex is an active open source contributor; he is the co-maintainer of Banshee and a frequent contributor to many other projects.  He is our first engineer who uses FreeBSD, a proud Emacs user, and utilizes his mouse less than you ever thought was possible.  You can see more details about his work on his blog.

His first project is writing a service to automatically fix misspelled queries on our search and making sure we direct consumers to the right service professionals (like fixing “huse cleaner” to “house cleaner”).  Bad spellers of the world rejoice!

A primer on Python decorators

Python allows you, the programmer, to do some very cool things with functions. In Python, functions are first-class objects, which means that you can do anything with them that you can do with strings, integers, or any other objects. For example, you can assign a function to a variable:

>>> def square(n):
...     return n * n
>>> square(4)
16
>>> alias = square
>>> alias(4)
16

The real power from having first-class functions, however, comes from the fact that you can pass them to and return them from other functions. Python’s built-in map function uses this ability: you pass it a function and a list, and map creates a new list by calling your function individually for each item in the list you gave it. Here’s an example that uses our square function from above:

>>> numbers = [1, 2, 3, 4, 5]
>>> map(square, numbers)
[1, 4, 9, 16, 25]

A function that accepts other function(s) as arguments and/or returns a function is called a higher-order function. While map simply made use of our function without making any changes to it, we can also use higher-order functions to change the behavior of other functions.

For example, let’s say we have a function which we call a lot that is very expensive:

>>> def fib(n):
...     "Recursively (i.e., dreadfully) calculate the nth Fibonacci number."
...     return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

We would like to save the results of this calculation, so that if we ever need to calculate the value for some n (which happens very often, given this function’s call tree), we don’t have to repeat our hard work. We could do that in a number of ways; for example, we could store the results in a dictionary somewhere, and every time we need a value from fib, we first see if it is in the dictionary.

But that would require us to reproduce the same dictionary-checking boilerplate every time we wanted a value from fib. Instead, it would be convenient if fib took care of saving its results internally, and our code that uses it could simply call it as it normally would. This technique is called memoization (note the lack of an ‘r’).

We could build this memoization code directly into fib, but Python gives us another, more elegant option. Since we can write functions that modify other functions, we can write a generic memoization function that takes a function and returns a memoized version of it:

def memoize(fn):
    stored_results = {}

    def memoized(*args):
        try:
            # try to get the cached result
            return stored_results[args]
        except KeyError:
            # nothing was cached for those args. let's fix that.
            result = stored_results[args] = fn(*args)
            return result

    return memoized

This memoize function takes another function as an argument, and creates a dictionary where it stores the results of previous calls to that function: the keys are the arguments passed to the function being memoized, and the values are what the function returned when called with those arguments. memoize returns a new function that first checks to see if there is an entry in the stored_results dictionary for the current arguments; if there is, the stored value is returned; otherwise, the wrapped function is called, and its return value is stored and returned back to the caller. This new function is often called a “wrapper” function, since it’s just a thin layer around a different function that does real work.

Now that we have our memoization function, we can just pass fib to it to get a wrapped version of it that won’t needlessly repeat any of the hard work it’s done before:

def fib(n):
    return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
fib = memoize(fib)

By using our higher-order memoize function, we get all the benefits of memoization without having to make any changes to our fib function itself, which would have obscured the real work that function did in the midst of the memoization baggage. But you might notice that the code above is still a little awkward, as we have to write fib three times in the above example. Since this pattern – passing a function to another function and saving the result back under the name of the original function – is extremely common in code that makes use of wrapper functions, Python provides a special syntax for it: decorators.

@memoize
def fib(n):
    return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

Here, we say that memoize is acting decorating fib. It’s important to realize that this is only a syntactic convenience. This code does exactly the same thing as the above snippet: it defines a function called fib, passes it to memoize, and saves the result of that as fib. The special (and, at first, a bit odd-looking) @ syntax simply cuts out the redundancy.

You can stack these decorators on top of each other, and they will apply in bottom-out fashion. For example, let’s say we also have another higher-order function to help with debugging:

def make_verbose(fn):
    def verbose(*args):
        # will print (e.g.) fib(5)
        print '%s(%s)' % (fn.__name__, ', '.join(repr(arg) for arg in args))
        return fn(*args) # actually call the decorated function

    return verbose

The following two code snippets then do the same thing:

@memoize
@make_verbose
def fib(n):
    return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
def fib(n):
    return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
fib = memoize(make_verbose(fib))

Interestingly, you’re not restricted to simply writing a function name after the @ symbol: you can also call a function there, letting you effectively pass arguments to a decorator. Let’s say that we aren’t content with simple memoization, and we want to store the function results in memcached. If we’ve written a memcached decorator function, we could (for example) pass in the address of the server as an argument:

@memcached('127.0.0.1:11211')
def fib(n):
    return n if n in [0, 1] else fib(n - 2) + fib(n - 1)

Written without decorator syntax, this expands to:

fib = memcached('127.0.0.1:11211')(fib)

Python comes with some functions that are very useful when applied as decorators. For example, Python has a classmethod function that creates the rough equivalent of a Java static method:

class Foo(object):
    SOME_CLASS_CONSTANT = 42

    @classmethod
    def add_to_my_constant(cls, value):
        # Here, `cls` will just be Foo, but if you called this method on a
        # subclass of Foo, `cls` would be that subclass instead.
        return cls.SOME_CLASS_CONSTANT + value

Foo.add_to_my_constant(10) # => 52

# unlike in Java, you can also call a classmethod on an instance
f = Foo()
f.add_to_my_constant(10) # => 52

Sidenote: Docstrings

Python functions carry more information than just code: they also carry useful help information, like their name and docstring:

>>> def fib(n):
...     "Recursively (i.e., dreadfully) calculate the nth Fibonacci number."
...     return n if n in [0, 1] else fib(n - 2) + fib(n - 1)
...
>>> fib.__name__
'fib'
>>> fib.__doc__
'Recursively (i.e., dreadfully) calculate the nth Fibonacci number.'

This information powers Python’s built-in help function. But when we wrap our function, we instead see the name and docstring of the wrapper:

>>> fib = memoized(fib)
>>> fib.__name__
'memoized'
>>> fib.__doc__

That’s not particularly helpful. Luckily, Python includes a helper function that will copy this documentation onto wrappers, called functools.wraps:

import functools
def memoize(fn):
    stored_results = {}

    @functools.wraps(fn)
    def memoized(*args):
        # (as before)

    return memoized

There’s something very satisfying about using a decorator to help you write a decorator. Now, if we were to retry our code from before with the updated memoize, we see the documentation is preserved:

>>> fib = memoized(fib)
>>> fib.__name__
'fib'
>>> fib.__doc__
'Recursively (i.e., dreadfully) calculate the nth Fibonacci number.'

Thumbtack is hiring engineers! Come work with us on making it easy to hire service professionals, and enjoy our in-house chef and sweet San Francisco office.

Gambling with the devil: A/B tests done right

“Designing an experiment is like gambling with the devil: Only a random strategy can defeat all his betting systems.” (R. A. Fisher)

Abba previewThese days just about everyone does some form of A/B testing to optimize pages. But as Ronald Fisher knew, A/B testing is loaded with traps, and the only way to avoid them is through careful use of randomization and statistics.

There are plenty of free tools out there that make A/B testing easy and accessible, but not all tools are created equal. Google’s Website Optimizer is one of the most complete and polished, designed to gather events directly from your site and display results in a nice report. GWO is a great way to get started with A/B testing and is how we ran our first few tests at Thumbtack.

Pretty soon, however, we started to outgrow to limited event gathering and reporting features of GWO. As data-driven decision making runs strong in our DNA, we decided to develop our own event tracking and reporting system. One component of this system was an A/B test report inspired by GWO.

As the months have gone by, we’ve come to realize just how valuable this tool is, and naturally we wanted to share it with the world. As such, we’ve packaged the code up into a nice, reusable Javascript library with a demo app that lets you enter your test results and get a handy report. It’s simple to use, runs entirely in the browser, lets you pass links to others, and uses some nifty statistics under the hood.

So give Abba a spin, check out the source, and let us know what you think! The demo page has a full FAQ where you can find plenty of details about how to interpret the report and how everything works under the hood.

http://www.thumbtack.com/labs/abba/