Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3719af3
Showing
12 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Copyright (c) 2011 [name of plugin creator] | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
SplitTester | ||
=========== | ||
|
||
This is a plugin that I wrote for doing split testing in a rails application with Google Analytics as your dashboard. The idea is to create an easy to use bucket testing system that wouldn't require another dashboard just for looking at the data. The logic and system are a simplified version of bucket testing based on what I've learned while bucket testing experiments on Yahoo! Search, one of the highest traffic pages on the internet. | ||
|
||
With this system, you don't need to muddy up your views, controllers or language files. The test elements are kept separate and can be turned on and off via percentage allocations of your traffic. It is a cookie based system, so users will have a consistent experience even if they end their session and return later. You can also run as many tests at the same time as you would like, only limited by the amount of traffic you have. | ||
|
||
I have also built in support for action caching so that you can keep your application fast and awesome. | ||
|
||
The code quality isn't as high as I would like and I have taken some shortcuts. I would love any help or input. :) | ||
|
||
A new test is a made up of a locale.yml file and or a collection of new views. The local file can override any translations that are in use and the views are direct replacements for views in the core app (BASELINE). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
require 'rake' | ||
require 'rake/testtask' | ||
require 'rake/rdoctask' | ||
|
||
desc 'Default: run unit tests.' | ||
task :default => :test | ||
|
||
desc 'Test the split_tester plugin.' | ||
Rake::TestTask.new(:test) do |t| | ||
t.libs << 'lib' | ||
t.libs << 'test' | ||
t.pattern = 'test/**/*_test.rb' | ||
t.verbose = true | ||
end | ||
|
||
desc 'Generate documentation for the split_tester plugin.' | ||
Rake::RDocTask.new(:rdoc) do |rdoc| | ||
rdoc.rdoc_dir = 'rdoc' | ||
rdoc.title = 'SplitTester' | ||
rdoc.options << '--line-numbers' << '--inline-source' | ||
rdoc.rdoc_files.include('README') | ||
rdoc.rdoc_files.include('lib/**/*.rb') | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Size represents the percent of traffic you want for each test | ||
# The total of all sizes should equal 100% of traffic. | ||
# It's easiest to think of it as being based out of 10 or 100. | ||
BASELINE: | ||
description: The baseline. This is a version running the default code. | ||
size: 10 | ||
FirstTest: | ||
description: This is a sample test. Replace this with your actual test. | ||
size: 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Include hook code here | ||
require 'action_controller' | ||
require 'split_tester' | ||
#require 'split_tester/class_methods' | ||
|
||
#SplitTester::Base.init |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Install hook code here | ||
require 'fileutils' | ||
|
||
#Copy the Javascript files | ||
FileUtils.copy(File.dirname(__FILE__) + '/files/split_tests.yml', File.dirname(__FILE__) + '/../../../config/') | ||
|
||
FileUtils.mkdir_p(File.dirname(__FILE__) + '/../../../test/split/') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
require 'action_controller' | ||
|
||
module ActionController #:nodoc: | ||
class Base | ||
|
||
SPLIT_TESTS = YAML.load_file("#{RAILS_ROOT}/config/split_tests.yml") | ||
|
||
class << self | ||
def custom_view_path(name) | ||
name == "views" ? "app/views" : "test/split/#{name}/views" | ||
end | ||
|
||
def self.random_test_key | ||
split_test_map.sample | ||
end | ||
end | ||
|
||
before_filter :setup_split_testing | ||
|
||
# preprocess some pathsets on boot | ||
# doing pathset generation during a request is very costly | ||
@@preprocessed_pathsets = begin | ||
SPLIT_TESTS.keys.reject { |k| k == 'BASELINE' }.inject({}) do |pathsets, slug| | ||
path = ActionController::Base.custom_view_path(slug) | ||
pathsets[path] = ActionView::Base.process_view_paths(path).first | ||
pathsets | ||
end | ||
end | ||
|
||
@@split_test_map = begin | ||
tm = {} # test map | ||
SPLIT_TESTS.each { |k, v| tm[k] = v['size'].to_i } | ||
tm.keys.zip(tm.values).collect { |v,d| (0...d).collect { v }}.flatten | ||
end | ||
|
||
cattr_accessor :preprocessed_pathsets, :split_test_map | ||
|
||
# If a split_test_key other than BASELINE exists, add the proper | ||
# view path to the load paths used by ActionView | ||
def setup_split_testing | ||
return unless is_split_test? | ||
split_test_path = preprocessed_pathsets[ActionController::Base.custom_view_path(@split_test_key)] | ||
prepend_view_path(split_test_path) if split_test_path | ||
end | ||
|
||
# Get the existing split_test_key from the session or the cookie. | ||
# If there isn't one, or if the one isn't a running test anymore | ||
# assign the user a new key and store it. | ||
# Don't assign a key if it is a crawler. (This doesn't feel right) | ||
def get_split_test_key | ||
return params[:force_test_key] if params[:force_test_key] # just for testing | ||
return session[:split_test_key] if session[:split_test_key] && SPLIT_TESTS.has_key?(session[:split_test_key]) | ||
return session[:split_test_key] = cookies[:split_test_key] if cookies[:split_test_key] && SPLIT_TESTS.has_key?(cookies[:split_test_key]) | ||
if (request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i) | ||
session[:split_test_key] = nil | ||
else | ||
session[:split_test_key] = ActionController::Base.random_test_key | ||
cookies[:split_test_key] = session[:split_test_key] | ||
end | ||
return session[:split_test_key] | ||
end | ||
|
||
def current_split_test_key | ||
@split_test_key ||= get_split_test_key | ||
end | ||
|
||
def is_split_test? | ||
current_split_test_key && current_split_test_key != 'BASELINE' | ||
end | ||
|
||
helper_method :is_split_test?, :current_split_test_key | ||
end | ||
end | ||
|
||
# Change the namespace for caching if the current request | ||
# is a split test so that caches don't get mixed together | ||
module ActionController #:nodoc: | ||
module Caching | ||
module Fragments | ||
def fragment_cache_key(key) | ||
namespace = is_split_test? ? "views-split-#{current_split_test_key}" : :views | ||
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, namespace) | ||
end | ||
end | ||
end | ||
end | ||
|
||
# Overwrite the translate method so that it tries the bucket translation first | ||
# TODO: There is probably a better way to write this | ||
module ActionView | ||
module Helpers | ||
module TranslationHelper | ||
def translate(key, options = {}) | ||
key = scope_key_by_partial(key) | ||
if is_split_test? | ||
# normalize the parameters so that we can add in | ||
# the current_split_test_key properly | ||
scope = options.delete(:scope) | ||
keys = I18n.normalize_keys(I18n.locale, key, scope) | ||
keys.shift | ||
key = keys.join('.') | ||
|
||
# Set the standard key as a default to fall back on automatically | ||
if options[:default] | ||
options[:default] = [options[:default]] unless options[:default].is_a?(Array) | ||
options[:default].unshift(key.to_sym) | ||
else | ||
options[:default] = [key.to_sym] | ||
end | ||
|
||
key = "#{current_split_test_key}.#{key}" | ||
end | ||
translation = I18n.translate(key, options.merge!(:raise => true)) | ||
if html_safe_translation_key?(key) && translation.respond_to?(:html_safe) | ||
translation.html_safe | ||
else | ||
translation | ||
end | ||
rescue I18n::MissingTranslationData => e | ||
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) | ||
content_tag('span', keys.join(', '), :class => 'translation_missing') | ||
end | ||
alias t translate | ||
end | ||
end | ||
end | ||
|
||
# Add the split test language files to the load path | ||
I18n.load_path += Dir[Rails.root.join('test', 'split', '*', 'locale.{rb,yml}')] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module SplitTester | ||
module ClassMethods | ||
def self.random_test_key | ||
split_test_map.sample | ||
end | ||
|
||
private | ||
|
||
def self.split_test_map | ||
@@split_test_map | ||
end | ||
|
||
def self.preprocessed_pathsets | ||
@@preprocessed_pathsets | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
module SplitTester | ||
module Controller | ||
|
||
def self.included(controller) | ||
controller.extend ClassMethods | ||
end | ||
|
||
# --------------------------------------------------------------- | ||
# Before Filters | ||
# --------------------------------------------------------------- | ||
|
||
# If a split_test_key other than BASELINE exists, add the proper | ||
# view path to the load paths used by ActionView | ||
def setup_split_testing | ||
@split_test_key = get_split_test_key | ||
return if @split_test_key == 'BASELINE' || @split_test_key.nil? | ||
split_test_path = preprocessed_pathsets[SplitTester::Base.custom_view_path(@split_test_key)] | ||
prepend_view_path(split_test_path) if split_test_path | ||
end | ||
|
||
# Get the existing split_test_key from the session or the cookie. | ||
# If there isn't one, or if the one isn't a running test anymore | ||
# assign the user a new key and store it. | ||
# Don't assign a key if it is a crawler. (This doesn't feel right) | ||
def get_split_test_key | ||
return params[:force_test_key] if params[:force_test_key] # just for testing | ||
return session[:split_test_key] if session[:split_test_key] && SPLIT_TESTS.has_key?(session[:split_test_key]) | ||
return session[:split_test_key] = cookies[:split_test_key] if cookies[:split_test_key] && SPLIT_TESTS.has_key?(cookies[:split_test_key]) | ||
if (request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i) | ||
session[:split_test_key] = nil | ||
else | ||
session[:split_test_key] = ApplicationController.random_test_key | ||
cookies[:split_test_key] = session[:split_test_key] | ||
end | ||
return session[:split_test_key] | ||
end | ||
|
||
# For caching, we need to add something to the cache_path | ||
# so that it caches each version of the page seperately. | ||
# Ideally, this would be added into the ActionCaching | ||
# module directly so that you don't need to specify anything | ||
# in the caches_action command | ||
def custom_cache_path | ||
path = ActionCachePath.new(self).path | ||
path += ":#{@split_test_key}" if @split_test_key && @split_test_key != 'BASELINE' | ||
path | ||
end | ||
|
||
# --------------------------------------------------------------- | ||
# Class Variables | ||
# --------------------------------------------------------------- | ||
|
||
# preprocess some pathsets on boot | ||
# doing pathset generation during a request is very costly | ||
@@preprocessed_pathsets = begin | ||
SPLIT_TESTS.keys.reject { |k| k == 'BASELINE' }.inject({}) do |pathsets, slug| | ||
path = SplitTester::Base.custom_view_path(slug) | ||
pathsets[path] = ActionView::Base.process_view_paths(path).first | ||
pathsets | ||
end | ||
end | ||
|
||
@@split_test_map = begin | ||
tm = {} # test map | ||
SPLIT_TESTS.each { |k, v| tm[k] = v['size'].to_i } | ||
tm.keys.zip(tm.values).collect { |v,d| (0...d).collect { v }}.flatten | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
require 'test_helper' | ||
|
||
class SplitTesterTest < ActiveSupport::TestCase | ||
# Replace this with your real tests. | ||
test "the truth" do | ||
assert true | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require 'rubygems' | ||
require 'test/unit' | ||
require 'active_support' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Uninstall hook code here | ||
puts "To clean up, just remove config/split_tests.yml and test/split/" |