views:

1415

answers:

5

We just now started doing the A/B testing for our Django based project. Can I get some information on best practices or useful insights about this A/B testing.

Ideally each new testing page will be differentiated with a single parameter(just like Gmail). mysite.com/?ui=2 should give a different page. So for every view I need to write a decorator to load different templates based on the 'ui' parameter value. And I dont want to hard code any template names in decorators. So how would urls.py url pattern will be?

+6  A: 

If you use the GET parameters like you suggsted (?ui=2), then you shouldn't have to touch urls.py at all. Your decorator can inspect request.GET['ui'] and find what it needs.

To avoid hardcoding template names, maybe you could wrap the return value from the view function? Instead of returning the output of render_to_response, you could return a tuple of (template_name, context) and let the decorator mangle the template name. How about something like this? WARNING: I haven't tested this code

def ab_test(view):
    def wrapped_view(request, *args, **kwargs):
        template_name, context = view(request, *args, **kwargs)
        if 'ui' in request.GET:
             template_name = '%s_%s' % (template_name, request.GET['ui'])
             # ie, 'folder/template.html' becomes 'folder/template.html_2'
        return render_to_response(template_name, context)
    return wrapped_view

This is a really basic example, but I hope it gets the idea across. You could modify several other things about the response, such as adding information to the template context. You could use those context variables to integrate with your site analytics, like Google Analytics, for example.

As a bonus, you could refactor this decorator in the future if you decide to stop using GET parameters and move to something based on cookies, etc.

Update If you already have a lot of views written, and you don't want to modify them all, you could write your own version of render_to_response.

def render_to_response(template_list, dictionary, context_instance, mimetype):
    return (template_list, dictionary, context_instance, mimetype)

def ab_test(view):
    from django.shortcuts import render_to_response as old_render_to_response
    def wrapped_view(request, *args, **kwargs):
        template_name, context, context_instance, mimetype = view(request, *args, **kwargs)
        if 'ui' in request.GET:
             template_name = '%s_%s' % (template_name, request.GET['ui'])
             # ie, 'folder/template.html' becomes 'folder/template.html_2'
        return old_render_to_response(template_name, context, context_instance=context_instance, mimetype=mimetype)
    return wrapped_view

@ab_test
def my_legacy_view(request, param):
     return render_to_response('mytemplate.html', {'param': param})
Justin Voss
But this involves modifying all the previous existant views right? I dont want to do that. Is there an alternative way, so that my old code stays as it and this ab-testing stands on top of that?
Maddy
You could write your own version of render_to_response, I suppose, that would interact with this decorator to accomplish what you want. I'll add a code sample to my answer to demonstrate.
Justin Voss
Justin: both of your suggestions are excellent. Maddy: if you want to A/B test an entire site, then no, you're going to have to bite the bullet somewhere in your request/response chain and programmatically, or declaratively, indicate which templates match which 'ui' numbers.
Jarret Hardie
Maddy: ... or use a template naming convention, which will required biting *that* bullet if you have already created templates which don't follow that convention. Sorry :-)
Jarret Hardie
On further reflection, dynamically changing templates is the _WRONG_ way to go about A/B testing. For one, you are breaking all caching on that view. Secondly, by putting in a different template, you are probably changing more than one thing. In the strict sense A/B testing is about changing a single variable. Instead the optimum solution would use a templatetag to introduce different "combinations" of a page. This templatetag would render to JavaScript which would fetch the correct content for that combination. That way the page can be cached and all combinations can still be tested.
jb
On further reflection of the reflection - it might be true that sometimes you just want to test one page vs. another (eg. new home page vs old one). In that case, yes your right - but the issue of caching still comes up - you'd have to put some thought into a Middleware level hash function that could direct users to the correct cached page.
jb
+1  A: 

Justin's response is right... I recommend you vote for that one, as he was first. His approach is particularly useful if you have multiple views that need this A/B adjustment.

Note, however, that you don't need a decorator, or alterations to urls.py, if you have just a handful of views. If you left your urls.py file as is...

(r'^foo/', my.view.here),

... you can use request.GET to determine the view variant requested:

def here(request):
    variant = request.GET.get('ui', some_default)

If you want to avoid hardcoding template names for the individual A/B/C/etc views, just make them a convention in your template naming scheme (as Justin's approach also recommends):

def here(request):
    variant = request.GET.get('ui', some_default)
    template_name = 'heretemplates/page%s.html' % variant
    try:
        return render_to_response(template_name)
    except TemplateDoesNotExist:
        return render_to_response('oops.html')
Jarret Hardie
That would work, but I think if you're going to test more than one view then moving that code into a decorator would save a lot of typing.
Justin Voss
Yup, I agree, Justin. Editing my response because we're so similar, yet yours is earlier.
Jarret Hardie
This suggests that I need to write an individual decorator for every view. Which is what I dont want. If ui=2 mentioned all across the site(meaning any url ending with ?ui=2 ),it loads version 2 of the entire site. So a generic decorator which loads version 2 templates across any url is required. Do I make myself clear? So changing templates dynamically(with ui=3, ui=3) for every view should happen in a similar way. So one decorator should do. Is that possible?
Maddy
+18  A: 

It's useful to take a step back and abstract what A/B testing is trying to do before diving into the code. What exactly will we need to conduct a test?

  • A Goal that has a Condition
  • At least two distinct Paths to meet the Goal's Condition
  • A system for sending viewers down one of the Paths
  • A system for recording the Results of the test

With this in mind let's think about implementation.

The Goal

When we think about a Goal on the web usually we mean that a user reaches a certain page or that they complete a specific action, for example successfully registering as a user or getting to the checkout page.

In Django we could model that in a couple of ways - perhaps naively inside a view, calling a function whenever a Goal has been reached:

    def checkout(request):
        a_b_goal_complete(request)
        ...

But that doesn't help because we'll have to add that code everywhere we need it - plus if we're using any pluggable apps we'd prefer not to edit their code to add our A/B test.

How can we introduce A/B Goals without directly editing view code? What about a Middleware?

    class ABMiddleware:
      def process_request(self, request):
          if a_b_goal_conditions_met(request):
            a_b_goal_complete(request)

That would allow us to track A/B Goals anywhere on the site.

How do we know that a Goal's conditions has been met? For ease of implementation I'll suggest that we know a Goal has had it's conditions met when a user reaches a specific URL path. As a bonus we can measure this without getting our hands dirty inside a view. To go back to our example of registering a user we could say that this goal has been met when the user reaches the URL path:

/registration/complete

So we define a_b_goal_conditions_met:

     a_b_goal_conditions_met(request):
       return request.path == "/registration/complete":

Paths

When thinking about Paths in Django it's natural to jump to the idea of using different templates. Whether there is another way remains to be explored. In A/B testing you make small differences between two pages and measure the results. Therefore it should be a best practice to define a single base Path template from which all Paths to the Goal should extend.

How should render these templates? A decorator is probably a good start - it's a best practice in Django to include a parameter template_name to your views a decorator could alter this parameter at runtime.

    @a_b
    def registration(request, extra_context=None, template_name="reg/reg.html"):
       ...

You could see this decorator either introspecting the wrapped function and modifying the template_name argument or looking up the correct templates from somewhere (like a Model). If we didn't want to add the decorator to every function we could implement this as part of our ABMiddleware:

    class ABMiddleware:
       ...
       def process_view(self, request, view_func, view_args, view_kwargs):
         if should_do_a_b_test(...) and "template_name" in view_kwargs:
           # Modify the template name to one of our Path templates
           view_kwargs["template_name"] = get_a_b_path_for_view(view_func)
           response = view_func(view_args, view_kwargs)
           return response

We'd need also need to add some way to keep track of which views have A/B tests running etc.

A system for sending viewers down a Path

In theory this is easy but there are lot of different implementations so it's not clear which one is best. We know a good system should divide users evenly down the path - Some hash method must be used - Maybe you could use the modulus of memcache counter divided by the number of Paths - maybe there is a better way.

A system for recording the Results of the Test

We need to record how many users went down what Path - we'll also need access to this information when the user reaches the goal (we need to be able to say what Path they came down to met the Condition of the Goal) - we'll use some kind of Model(s) to record the data and either Django Sessions or Cookies to persist the Path information until the user meets the Goal condition.

Closing Thoughts

I've given a lot of pseudo code for implementing A/B testing in Django - the above is by no means a complete solution but a good start towards creating a reusable framework for A/B testing in Django.

For reference you may want to look at Paul Mar's Seven Minute A/Bs on GitHub - it's the ROR version of the above! http://github.com/paulmars/seven_minute_abs/tree/master


Update

On further reflection and investigation of Google Website Optimizer it's apparent that there are gaping holes in the above logic. By using different templates to represent Paths you break all caching on the view (or if the view is cached it will always serve the same path!). Instead, of using Paths, I would instead steal GWO terminology and use the idea of Combinations - that is one specific part of a template changing - for instance, changing the <h1> tag of a site.

The solution would involve template tags which would render down to JavaScript. When the page is loaded in the browser the JavaScript makes a request to your server which fetches one of the possible Combinations.

This way you can test multiple combinations per page while preserving caching!


Update

There still is room for template switching - say for example you introduce an entirely new homepage and want to test it's performance against the old homepage - you'd still want to use the template switching technique. The thing to keep in mind is your going to have to figure out some way to switch between X number of cached versions of the page. To do this you'd need to override the standard cached middleware to see if their is a A/B test running on the requested URL. Then it could choose the correct cached version to show!!!


Update

Using the ideas described above I've implemented a pluggable app for basic A/B testing Django. You can get it off Github:

http://github.com/johnboxall/django-ab/tree/master

jb
Thank you Jb, I really appreciate your solution. Let me try and implement it and I will let you know my feedback on that.
Maddy
This is one of the best posts I've seen in the year I've been on this site. Great stuff.
theycallmemorty
+5  A: 

Django lean is a good option for A/B Testing

http://bitbucket.org/akoha/django-lean/wiki/Home

sebastian serrano
Don't reinvent the wheel. Use django-lean
Henrik Joreteg
A: 

A code based on the one by Justin Voss:

def ab_test(force = None):
    def _ab_test(view):
        def wrapped_view(request, *args, **kwargs):
            request, template_name, cont = view(request, *args, **kwargs)
            if 'ui' in request.GET:
                request.session['ui'] = request.GET['ui']
            if 'ui' in request.session:
                cont['ui'] = request.session['ui']
            else:
                if force is None:
                    cont['ui'] = '0'
                else:
                    return redirect_to(request, force)
            return direct_to_template(request, template_name, extra_context = cont)
        return wrapped_view
    return _ab_test

example function using the code:

@ab_test()
def index1(request):
    return (request,'website/index.html', locals())

@ab_test('?ui=33')
def index2(request):
    return (request,'website/index.html', locals())

What happens here: 1. The passed UI parameter is stored in the session variable 2. The same template loads every time, but a context variable {{ui}} stores the UI id (you can use it to modify the template) 3. If user enters the page without ?ui=xx then in case of index2 he's redirected to '?ui=33', in case of index1 the UI variable is set to 0.

I use 3 to redirect from the main page to Google Website Optimizer which in turn redirects back to the main page with a proper ?ui parameter.

Merlin