TestWrangler is an a/b testing platform for Rails, leveraging Rack middleware and a Redis-backed persistence engine for performance.
TestWrangler is designed to integrate with a third party front or back end user tracking service like Mixpanel for metrics and conversion tracking.
TestWrangler also provides a basic CMS for managing its data.
Add this line to your application's Gemfile:
gem 'test_wrangler'And then execute:
$ bundle
Or install it yourself as:
$ gem install test_wrangler
Once TestWrangler is installed, it will inject its middleware and its helper into your application. The middleware is designed to operate independently of any other part of the Rails middleware stack, such that its order in the stack does not matter. TestWrangler will run and attempt to assign test selections to requests as long as the TEST_WRANGLER environment variable is set to 'on'.
TestWrangler exposes a number of configuration options, which should be set in an initializer.
Example initializer with default values explicitly:
require 'test_wrangler'
TestWrangler.config do |config|
config.exclude_paths []
config.redis Redis.new
config.root_key :test_wrangler
config.logger Rails.logger
config.username ENV["TEST_WRANGLER_USER"]
config.password ENV["TEST_WRANGLER_PASSWORD"]
config.verbose false
endTo use the TestWrangler CMS, you will have to mount its engine in your routes.rb file. TestWrangler currently only supports being mounted at /test_wrangler:
mount TestWrangler::Engine, at: '/test_wrangler'You will also have to set a username and password for the basic HTTP auth system used by the CMS. These can be configured using config method as shown above, or by directly setting the TEST_WRANGLER_USER and TEST_WRANGLER_PASSWORD environment variables.
exclude_paths(*paths)An array of strings or Regexps to exclude request paths from the middleware. Any string values will be turned into Regexps in the format of/^(string_pattern)/. Defaults to empty arrayredis(redis_instance)A redis instance to use as the base connection for TestWrangler. Defaults to whatever the result of callingRedis.newreturnsroot_key(key)The root key to use to namespace TestWrangler data. Defaults to:test_wrangler. May be a string or a symbollogger(logger_instance)An object that implements the ruby logger interface, to which all logging calls will be sent. Passing nil or false will disable all logging. Defaults to the Rails loggerverbose(boolean)When the value of verbose is set to a truthy value, additional logging will be performed from the assignment middleware for debugging. Defaults to false
It can be useful to force a test selection during development. To do so, you can set the selection in the query parameters. If the selection is valid, it will be set in the cookies and used for future requests in the session
Format:
https://my-site.com/?TW_SELECTION=cohort:experiment:variant
Some browsers will require that you URL encode the commas used to seperate the components of the selection
Cohorts and experiments are the models that TestWrangler uses to fragment data.
A cohort represents a segment of traffic. Cohorts can be segmented based on query parameters, cookie values, or user agent strings. The 'priority' of a cohort determines which cohort a request will be assigned to if it matches more than one cohort. Lower priority numbers mean higher priority in matching.
Each cohort can have one or more associated experiments to which it the cohort distributes its traffic. Each experiment may belong to one or more cohorts.
Each experiment in turn can have one or more variants, with each variant being given a weight to determine what proportion of the experiment's assigned traffic will be assigned to each variant.
Control variants are not automatically set, and variants will be given equal weight if no weights are specified for any variants. If specified weights add to more than 1, the weights are normalized to proportions of 1. If some variants are not specified, and the total specified weight is less than 1, the remainder is divided among the variants with unspecified weights. If the specified weights total to 1 or more, the variants with unspecified weights are given the lowest specified weight, and then all weights are normalized to a proportion of 1.
TestWrangler::Cohort.new(name, priority, matching_criteria)
Example:
cohort = TestWrangler::Cohort.new('mobile', 1, [{type: :user_agent, user_agent: [/Mobi/]}])
TestWrangler.save_cohort(cohort)matching_criteria should be an arry of configuration hashes for the rules a request must match to satisfy the cohort. The type key of the hash should have one of the following values :user_agent, :query_parameters, :cookies, :universal. The hash should also have a key that is the same as its type, with a value that is an array of values to match against. The :universal matcher type will match all requests, and does not need an array of matcher values.
If any of a cohort's matchers' '#match?(env)' method returns true for the request env, the cohort will register a match for the request.
Matcher Types
TODO: Add method for registering new matcher types
cookiesAccepts an array of hashes to match against the cookie. If all of the key/value pairs in any of the hashes matches the request cookies, a match is registered.user_agentAccepts an array of strings and/or Regexp objects to match against the user agent string. A string is considered a match if it is == to the user agent string. For Regexps, the =~ operator is used. The request is considered a match if it satisfies all of the strings and Regexps in the array. For this reason, string rules only make sense if a single rule is used.query_parametersAccepts an array of hases to match against the query parameters. If all of the key/value pairs in any of the hashes matches the request parameters, a match is registered.universalAutomatically matches any request passed to it. Any value passed as a matching rule in the config hash is discarded.
Example - Auto Split Variants Equally:
experiment = TestWrangler::Experiment.new('facebook_signup', [:control, :signup_on_cya])
TestWrangler.save_experiment(experiment)Example - Split Variants Manually:
experiment = TestWrangler::Experiment.new('facebook_signup', [{control: 0}, {signup_on_cya: 100}])
TestWrangler.save_experiment(experiment)Example:
TestWrangler.add_experiment_to_cohort('facebook_signup', 'mobile')Example:
TestWrangler.activate_experiment('facebook_signup')
TestWrangler.activate_cohort('facebook')TestWrangler provides two helper methods to all controllers and views, test_wrangler_selection, and complete_experiment.
test_wrangler_selection returns a hash with the test selection in the format { cohort: 'cohort_name', experiment: 'experiment_name', variant: 'variant_name'}. If no selection has been made all keys will be present but the values will be nil.
complete_experiment simply erases any TestWrangler cookie that may be set for the user, and returns the test selection as it stood before deleting the cookie. Once the cookie is cleared, the user will be enrolled in a new test on the next request.
TestWrangler uses RedisNamespace to isolate its data. All keys will begin with test_wrangler: unless the :root_key config value has been set, in which case that value will be used.
The top level key experiments is a set tracking all currently saved experiments. Each experiment also stores its data in the following structure:
experiments:experiment_name(Hash)variant_nameWeight of the particular variantvariant_name:participant_countNumber of participants in the particular variantparticipant_countOverall participant count for the experimentstateThe experiment state ('active' or nil)
experiments:experiment_name:cohortsSet containing the names of all cohorts associated with the experiment.
The top level key cohorts is a set tracking all currently saved cohorts. Each cohort also stores its priority, serialized matching criteria, experiment associations, and active experiments list in the following keys:
cohorts:cohort_name:criteriaMatching criteria serialized as JSONcohorts:cohort_name:priorityPriority of the cohortcohorts:cohort_name:experimentsSet containing all associated experiment namescohorts:cohort_name:active_experimentsList containing the names of all active associated experiments. The list is rotated on each assignment to determine which of the cohort's experiments will be assigned to a particular request.
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/test_wrangler.