Skip to content

Instantly share code, notes, and snippets.

@bwiggs
Created January 12, 2012 03:10
Show Gist options
  • Save bwiggs/1598333 to your computer and use it in GitHub Desktop.
Save bwiggs/1598333 to your computer and use it in GitHub Desktop.
MVC Split AB Test Controller
using System;
using System.Web;
using System.Web.Mvc;
public abstract class SplitABController : Controller
{
private static readonly Results results = new Results();
private const string B_TEST_SUFFIX = "_B";
private const string SPLIT_TEST_VIEW_COOKIE_NAME = "SPLIT_TEST_VIEW_COOKIE";
private static ViewFilesCache cache;
public static Results GetSplitTestResults() { return results; }
protected over ride void OnActionExecuting(ActionExecutingContext filterContext)
{
AddSplitTestResultsToResultsMap(filterContext);
base.OnActionExecuting(filterContext);
}
private void AddSplitTestResultsToResultsMap(ActionExecutingContext filterContext)
{
// If last request was not for a split test view then just return
if (Request.Cookies[SPLIT_TEST_VIEW_COOKIE_NAME] == null) return;
// Add this controller/action to the results of the split test
ResultRow rr = ResultRow.FromString(Request.Cookies[SPLIT_TEST_VIEW_COOKIE_NAME].Value);
results.RemoveOneFromResults(rr);
rr.ToControllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
rr.ToAction = filterContext.ActionDescriptor.ActionName;
results.AddOneToResults(rr);
}
/// <summary>
/// If the view has a _B counterpart then we mark this action as a 'Split Test' we
/// then record the results of this split view in the next request (OnActionExecuting).
///
/// This method also determines if we should supply the A or B view depending on the
/// 'ShouldRequestUseBView' algorithm.
/// </summary>
protected override ViewResult View(string viewName, string masterName, object model)
{
if (String.IsNullOrEmpty(viewName)) { viewName = (string) RouteData.Values["action"]; }
bool isSplitTestingView = IsSplitTestingView(masterName, viewName);
// If this view does not have a _B counterpart then we mark this request as non split test (by
// removing the 'SPLIT_TEST_VIEW_COOKIE_NAME' cookie) and just send control to base.View
if (!isSplitTestingView)
{
Response.Cookies.Remove(SPLIT_TEST_VIEW_COOKIE_NAME);
return base.View(viewName, masterName, model);
}
// Wether to use the A or B view depending on the 'ShouldRequestUseBView' algorithm
bool useb = ShouldRequestUseBView();
// Lets create a results row and store it in the cookie (SPLIT_TEST_VIEW_COOKIE_NAME). This will let the
// next request (OnActionExecuting) know that we just hit a split test view.
ResultRow rr = new ResultRow {FromController = this, FromAction = viewName, UsedBView = useb};
Response.Cookies.Add(new HttpCookie(SPLIT_TEST_VIEW_COOKIE_NAME, rr.ToString()));
results.AddOneToResults(rr);
// Display the appropriate view (A or B)
return useb
? base.View(viewName + B_TEST_SUFFIX, masterName, model)
: base.View(viewName, masterName, model);
}
/// <summary>
/// Returns wether the specified view has a _B counterpart.
/// </summary>
private bool IsSplitTestingView(string masterName, string viewName)
{
if (cache == null) { lock (GetType()) { if (cache == null) { cache = new ViewFilesCache(B_TEST_SUFFIX); } } }
return cache.HasSplitTestingAlternative(this, masterName, viewName);
}
/// <summary>
/// If odd IP then use 'B' view. This will give a ~50% A / B split.
/// </summary>
private bool ShouldRequestUseBView()
{
return Int32.Parse(Request.UserHostAddress.Substring(Request.UserHostAddress.LastIndexOf('.') + 1)) % 2 == 1;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment