Skip to content

Commit 20acb0a

Browse files
committedJan 24, 2011
Convert the whole system to a gem. Lots of repackaging for Railties support
1 parent 6e82b0e commit 20acb0a

File tree

8 files changed

+194
-113
lines changed

8 files changed

+194
-113
lines changed
 

‎Rakefile

+29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
require 'rubygems'
12
require 'rake'
23
require 'rake/testtask'
4+
require 'rake/gempackagetask'
35
require 'rake/rdoctask'
46

57
desc 'Default: run unit tests.'
@@ -20,4 +22,31 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
2022
rdoc.options << '--line-numbers' << '--inline-source'
2123
rdoc.rdoc_files.include('README')
2224
rdoc.rdoc_files.include('lib/**/*.rb')
25+
end
26+
27+
PKG_FILES = FileList[
28+
'[a-zA-Z]*',
29+
'lib/**/*',
30+
'rails/**/*',
31+
'files/*',
32+
'test/**/*'
33+
]
34+
35+
spec = Gem::Specification.new do |s|
36+
s.name = "split_tester"
37+
s.version = "0.2"
38+
s.author = "Jeremy Hubert"
39+
s.email = "jhubert@gmail.com"
40+
s.homepage = "http://jeremyhubert.com/"
41+
s.platform = Gem::Platform::RUBY
42+
s.summary = "Provides A/B split testing functionality for Rails"
43+
s.files = PKG_FILES.to_a
44+
s.require_path = "lib"
45+
s.has_rdoc = false
46+
s.extra_rdoc_files = ["README"]
47+
end
48+
49+
desc 'Turn this plugin into a gem.'
50+
Rake::GemPackageTask.new(spec) do |pkg|
51+
pkg.gem_spec = spec
2352
end

‎init.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
# Include hook code here
2-
require 'action_controller'
3-
require 'split_tester'
1+
require File.join(File.dirname(__FILE__), "lib", "split_tester")

‎lib/split_tester.rb

+35-110
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,54 @@
1-
require 'action_controller'
1+
require 'split_tester/railtie'
2+
require 'split_tester/controller'
3+
require 'split_tester/translation_helper'
4+
require 'split_tester/caching'
25

3-
module ActionController #:nodoc:
6+
module SplitTester
47
class Base
5-
6-
SPLIT_TESTS = YAML.load_file("#{RAILS_ROOT}/config/split_tests.yml")
7-
8-
class << self
9-
def custom_view_path(name)
10-
name == "views" ? "app/views" : "test/split/#{name}/views"
11-
end
12-
13-
def self.random_test_key
14-
split_test_map.sample
8+
# Doesn't have access to Rails.root here
9+
SPLIT_TESTS = YAML.load_file("config/split_tests.yml")
10+
11+
def self.setup
12+
# Add the split test language files to the load path
13+
I18n.load_path += Dir[Rails.root.join('test', 'split', '*', 'locale.{rb,yml}')]
14+
15+
@@preprocessed_pathsets = begin
16+
SPLIT_TESTS.keys.reject { |k| k == 'BASELINE' }.inject({}) do |pathsets, slug|
17+
path = custom_view_path(slug)
18+
pathsets[path] = ActionView::Base.process_view_paths(path).first
19+
pathsets
20+
end
1521
end
16-
end
1722

18-
before_filter :setup_split_testing
19-
20-
# preprocess some pathsets on boot
21-
# doing pathset generation during a request is very costly
22-
@@preprocessed_pathsets = begin
23-
SPLIT_TESTS.keys.reject { |k| k == 'BASELINE' }.inject({}) do |pathsets, slug|
24-
path = ActionController::Base.custom_view_path(slug)
25-
pathsets[path] = ActionView::Base.process_view_paths(path).first
26-
pathsets
23+
@@split_test_map = begin
24+
tm = {} # test map
25+
SPLIT_TESTS.each { |k, v| tm[k] = v['size'].to_i }
26+
tm.keys.zip(tm.values).collect { |v,d| (0...d).collect { v }}.flatten
2727
end
2828
end
2929

30-
@@split_test_map = begin
31-
tm = {} # test map
32-
SPLIT_TESTS.each { |k, v| tm[k] = v['size'].to_i }
33-
tm.keys.zip(tm.values).collect { |v,d| (0...d).collect { v }}.flatten
30+
def self.split_test_map
31+
@@split_test_map
3432
end
3533

36-
cattr_accessor :preprocessed_pathsets, :split_test_map
37-
38-
# If a split_test_key other than BASELINE exists, add the proper
39-
# view path to the load paths used by ActionView
40-
def setup_split_testing
41-
return unless is_split_test?
42-
split_test_path = preprocessed_pathsets[ActionController::Base.custom_view_path(@split_test_key)]
43-
prepend_view_path(split_test_path) if split_test_path
44-
end
45-
46-
# Get the existing split_test_key from the session or the cookie.
47-
# If there isn't one, or if the one isn't a running test anymore
48-
# assign the user a new key and store it.
49-
# Don't assign a key if it is a crawler. (This doesn't feel right)
50-
def get_split_test_key
51-
return params[:force_test_key] if params[:force_test_key] # just for testing
52-
return session[:split_test_key] if session[:split_test_key] && SPLIT_TESTS.has_key?(session[:split_test_key])
53-
return session[:split_test_key] = cookies[:split_test_key] if cookies[:split_test_key] && SPLIT_TESTS.has_key?(cookies[:split_test_key])
54-
if (request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i)
55-
session[:split_test_key] = nil
56-
else
57-
session[:split_test_key] = ActionController::Base.random_test_key
58-
cookies[:split_test_key] = session[:split_test_key]
59-
end
60-
return session[:split_test_key]
34+
def self.preprocessed_pathsets
35+
@@preprocessed_pathsets
6136
end
6237

63-
def current_split_test_key
64-
@split_test_key ||= get_split_test_key
38+
def self.custom_view_path(name)
39+
name == "views" ? "app/views" : "test/split/#{name}/views"
6540
end
6641

67-
def is_split_test?
68-
current_split_test_key && current_split_test_key != 'BASELINE'
42+
def self.active_test?(key)
43+
SPLIT_TESTS.has_key?(key)
6944
end
7045

71-
helper_method :is_split_test?, :current_split_test_key
72-
end
73-
end
74-
75-
# Change the namespace for caching if the current request
76-
# is a split test so that caches don't get mixed together
77-
module ActionController #:nodoc:
78-
module Caching
79-
module Fragments
80-
def fragment_cache_key(key)
81-
namespace = is_split_test? ? "views-split-#{current_split_test_key}" : :views
82-
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, namespace)
83-
end
46+
def self.view_path(key)
47+
preprocessed_pathsets[custom_view_path(key)]
8448
end
85-
end
86-
end
87-
88-
# Overwrite the translate method so that it tries the bucket translation first
89-
# TODO: There is probably a better way to write this
90-
module ActionView
91-
module Helpers
92-
module TranslationHelper
93-
def translate(key, options = {})
94-
key = scope_key_by_partial(key)
95-
if is_split_test?
96-
# normalize the parameters so that we can add in
97-
# the current_split_test_key properly
98-
scope = options.delete(:scope)
99-
keys = I18n.normalize_keys(I18n.locale, key, scope)
100-
keys.shift
101-
key = keys.join('.')
10249

103-
# Set the standard key as a default to fall back on automatically
104-
if options[:default]
105-
options[:default] = [options[:default]] unless options[:default].is_a?(Array)
106-
options[:default].unshift(key.to_sym)
107-
else
108-
options[:default] = [key.to_sym]
109-
end
110-
111-
key = "#{current_split_test_key}.#{key}"
112-
end
113-
translation = I18n.translate(key, options.merge!(:raise => true))
114-
if html_safe_translation_key?(key) && translation.respond_to?(:html_safe)
115-
translation.html_safe
116-
else
117-
translation
118-
end
119-
rescue I18n::MissingTranslationData => e
120-
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
121-
content_tag('span', keys.join(', '), :class => 'translation_missing')
122-
end
123-
alias t translate
50+
def self.random_test_key
51+
split_test_map.sample
12452
end
12553
end
126-
end
127-
128-
# Add the split test language files to the load path
129-
I18n.load_path += Dir[Rails.root.join('test', 'split', '*', 'locale.{rb,yml}')]
54+
end

‎lib/split_tester/caching.rb

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Change the namespace for caching if the current request
2+
# is a split test so that caches don't get mixed together
3+
module SplitTester #:nodoc:
4+
module Caching
5+
def self.included(base)
6+
base.class_eval {
7+
def fragment_cache_key(key)
8+
namespace = is_split_test? ? "views-split-#{current_split_test_key}" : :views
9+
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, namespace)
10+
end
11+
}
12+
end
13+
end
14+
end

‎lib/split_tester/controller.rb

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module SplitTester #:nodoc:
2+
module Controller
3+
def self.included(base)
4+
base.send(:include, InstanceMethods)
5+
base.class_eval {
6+
helper_method :is_split_test?, :current_split_test_key
7+
8+
before_filter :setup_split_testing
9+
}
10+
end
11+
12+
module InstanceMethods
13+
# If a split_test_key other than BASELINE exists, add the proper
14+
# view path to the load paths used by ActionView
15+
def setup_split_testing
16+
return unless is_split_test?
17+
split_test_path = SplitTester::Base.view_path(current_split_test_key)
18+
prepend_view_path(split_test_path) if split_test_path
19+
end
20+
21+
# Get the existing split_test_key from the session or the cookie.
22+
# If there isn't one, or if the one isn't a running test anymore
23+
# assign the user a new key and store it.
24+
# Don't assign a key if it is a crawler. (This doesn't feel right)
25+
def get_split_test_key
26+
return params[:force_test_key] if params[:force_test_key] && SplitTester::Base.active_test?(params[:force_test_key]) # just for testing
27+
return session[:split_test_key] if session[:split_test_key] && SplitTester::Base.active_test?(session[:split_test_key])
28+
return session[:split_test_key] = cookies[:split_test_key] if cookies[:split_test_key] && SplitTester::Base.active_test?(cookies[:split_test_key])
29+
if (request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i)
30+
session[:split_test_key] = nil
31+
else
32+
session[:split_test_key] = SplitTester::Base.random_test_key
33+
cookies[:split_test_key] = session[:split_test_key]
34+
end
35+
return session[:split_test_key]
36+
end
37+
38+
def current_split_test_key
39+
@split_test_key ||= get_split_test_key
40+
end
41+
42+
def is_split_test?
43+
current_split_test_key && current_split_test_key != 'BASELINE'
44+
end
45+
end
46+
end
47+
end

‎lib/split_tester/railtie.rb

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'split_tester'
2+
3+
module SplitTester
4+
if defined? Rails::Railtie
5+
require 'rails'
6+
class Railtie < Rails::Railtie
7+
initializer "split_tester.init" do
8+
SplitTester::Railtie.insert
9+
end
10+
end
11+
end
12+
13+
class Railtie
14+
def self.insert
15+
ActionController::Base.send(:include, SplitTester::Controller)
16+
ActionController::Caching::Fragments.send(:include, SplitTester::Caching)
17+
ActionView::Helpers::TranslationHelper.send(:include, SplitTester::TranslationHelper)
18+
19+
SplitTester::Base.setup
20+
end
21+
end
22+
end
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
require 'action_view'
2+
3+
module SplitTester
4+
module TranslationHelper
5+
def self.included(base)
6+
# Using class_eval here because base.send(:include, InstanceMethods)
7+
# wasn't overwriting the translate method properly. Not sure why.
8+
base.class_eval {
9+
def translate(key, options = {})
10+
key = scope_key_by_partial(key)
11+
if is_split_test?
12+
# normalize the parameters so that we can add in
13+
# the current_split_test_key properly
14+
scope = options.delete(:scope)
15+
keys = I18n.normalize_keys(I18n.locale, key, scope)
16+
keys.shift
17+
key = keys.join('.')
18+
19+
# Set the standard key as a default to fall back on automatically
20+
if options[:default]
21+
options[:default] = [options[:default]] unless options[:default].is_a?(Array)
22+
options[:default].unshift(key.to_sym)
23+
else
24+
options[:default] = [key.to_sym]
25+
end
26+
27+
key = "#{current_split_test_key}.#{key}"
28+
end
29+
translation = I18n.translate(key, options.merge!(:raise => true))
30+
if html_safe_translation_key?(key) && translation.respond_to?(:html_safe)
31+
translation.html_safe
32+
else
33+
translation
34+
end
35+
rescue I18n::MissingTranslationData => e
36+
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
37+
content_tag('span', keys.join(', '), :class => 'translation_missing')
38+
end
39+
alias t translate
40+
}
41+
end
42+
end
43+
end

‎rails/init.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Include hook code here
2+
require 'split_tester/railtie'
3+
SplitTester::Railtie.insert

0 commit comments

Comments
 (0)
Please sign in to comment.