split_tester 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,39 @@
1
+ SplitTester
2
+ ===========
3
+
4
+ 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.
5
+
6
+ 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.
7
+
8
+ I have also built in support for action caching so that you can keep your application fast and awesome.
9
+
10
+ The code quality isn't as high as I would like and I have taken some shortcuts. I would love any help or input. :)
11
+
12
+ 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).
13
+
14
+ Usage
15
+ ==========
16
+
17
+ As a plugin:
18
+
19
+ rails plugin install git://github.com/jhubert/rails-split-tester.git
20
+
21
+ As a gem:
22
+
23
+ Place this in your Gemfile:
24
+
25
+ gem 'split_tester', :git => 'git://github.com/jhubert/rails-split-tester.git'
26
+
27
+ then run:
28
+
29
+ bundle install
30
+
31
+ and then run the generator:
32
+
33
+ rails g split_tester_install
34
+
35
+ A file named split_tests.yml will be created in your config folder. This is where you will define the tests you want to run and what percentage of the traffic they should receive.
36
+
37
+ A folder named "split" will be created in your test folder. This is where the actual configuration of the split tests will go.
38
+
39
+ Please see my demo app for an example: https://github.com/jhubert/split-tested-app-demo
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/rdoctask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the split_tester plugin.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Generate documentation for the split_tester plugin.'
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = 'SplitTester'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README')
24
+ 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.3"
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.description = "Split Tester provides support for A/B Split testing your pages with integration into Google Analytics."
44
+ s.files = PKG_FILES.to_a
45
+ s.require_path = "lib"
46
+ s.has_rdoc = false
47
+ s.extra_rdoc_files = ["README"]
48
+ end
49
+
50
+ desc 'Turn this plugin into a gem.'
51
+ Rake::GemPackageTask.new(spec) do |pkg|
52
+ pkg.gem_spec = spec
53
+ end
@@ -0,0 +1,9 @@
1
+ # Size represents the percent of traffic you want for each test
2
+ # The total of all sizes should equal 100% of traffic.
3
+ # It's easiest to think of it as being based out of 10 or 100.
4
+ BASELINE:
5
+ description: The baseline. This is a version running the default code.
6
+ size: 10
7
+ FirstTest:
8
+ description: This is a sample test. Replace this with your actual test.
9
+ size: 0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "lib", "split_tester")
data/install.rb ADDED
@@ -0,0 +1,7 @@
1
+ # Install hook code here
2
+ require 'fileutils'
3
+
4
+ #Copy the Javascript files
5
+ FileUtils.copy(File.dirname(__FILE__) + '/files/split_tests.yml', File.dirname(__FILE__) + '/../../../config/')
6
+
7
+ FileUtils.mkdir_p(File.dirname(__FILE__) + '/../../../test/split/')
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Generates the initial files for the Split Tester
3
+
4
+ Example:
5
+ rails generate split_tester
6
+
7
+ This will create:
8
+ config/split_tests.yml
9
+ test/split/
@@ -0,0 +1,37 @@
1
+ require 'rails_generator'
2
+ require 'rails_generator/commands'
3
+
4
+ module SplitTester #:nodoc:
5
+ module Generator #:nodoc:
6
+ module Commands #:nodoc:
7
+ module Create
8
+ def setup
9
+ file("split_tests.yml", "definition.txt")
10
+ end
11
+ end
12
+
13
+ module Destroy
14
+ def setup
15
+ file("split_tests.yml", "definition.txt")
16
+ end
17
+ end
18
+
19
+ module List
20
+ def setup
21
+ file("split_tests.yml", "definition.txt")
22
+ end
23
+ end
24
+
25
+ module Update
26
+ def setup
27
+ file("split_tests.yml", "definition.txt")
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ Rails::Generator::Commands::Create.send :include, SplitTester::Generator::Commands::Create
35
+ Rails::Generator::Commands::Destroy.send :include, SplitTester::Generator::Commands::Destroy
36
+ Rails::Generator::Commands::List.send :include, SplitTester::Generator::Commands::List
37
+ Rails::Generator::Commands::Update.send :include, SplitTester::Generator::Commands::Update
@@ -0,0 +1,11 @@
1
+ require 'rails/generators'
2
+
3
+ class SplitTestGenerator < Rails::Generators::NamedBase
4
+ argument :size, :type => :string, :default => '0', :banner => "size"
5
+ argument :description, :type => :string, :default => 'Your Test Description', :banner => "description"
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def manifest
9
+ append_to_file('config/split_tests.yml', "#{name}:\n description: #{description}\n size: #{size}\n")
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails/generators'
2
+
3
+ class SplitTesterInstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def manifest
7
+ empty_directory('test/split/')
8
+ template('split_tests.yml', 'config/split_tests.yml')
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # Size represents the percent of traffic you want for each test
2
+ # The total of all sizes should equal 100% of traffic.
3
+ # It's easiest to think of it as being based out of 10 or 100.
4
+ BASELINE:
5
+ description: The baseline. This is a version running the default code.
6
+ size: 10
7
+ FirstTest:
8
+ description: This is a sample test. Replace this with your actual test.
9
+ size: 0
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,70 @@
1
+ require 'split_tester/railtie'
2
+ require 'split_tester/controller'
3
+ require 'split_tester/translation_helper'
4
+ require 'split_tester/caching'
5
+
6
+ module SplitTester
7
+ class Base
8
+ # Doesn't have access to Rails.root here
9
+ begin
10
+ SPLIT_TESTS = YAML.load_file("config/split_tests.yml")
11
+ SPLIT_TESTS.keys
12
+ LOADED = true
13
+ rescue LoadError
14
+ puts "[SplitTester] Missing config/split_tests.yml"
15
+ LOADED = false
16
+ rescue
17
+ puts "[SplitTester] Invalid config/split_tests.yml"
18
+ LOADED = false
19
+ end
20
+
21
+ def self.setup
22
+ if LOADED
23
+ # Add the split test language files to the load path
24
+ I18n.load_path += Dir[Rails.root.join('test', 'split', '*', 'locale.{rb,yml}')]
25
+
26
+ @@preprocessed_pathsets = begin
27
+ SPLIT_TESTS.keys.reject { |k| k == 'BASELINE' }.inject({}) do |pathsets, slug|
28
+ path = custom_view_path(slug)
29
+ pathsets[path] = ActionView::Base.process_view_paths(path).first
30
+ pathsets
31
+ end
32
+ end
33
+
34
+ @@split_test_map = begin
35
+ tm = {} # test map
36
+ SPLIT_TESTS.each { |k, v| tm[k] = v['size'].to_i }
37
+ tm.keys.zip(tm.values).collect { |v,d| (0...d).collect { v }}.flatten
38
+ end
39
+ else
40
+ @@split_test_map = []
41
+ @@preprocessed_pathsets = []
42
+ end
43
+ end
44
+
45
+ def self.split_test_map
46
+ @@split_test_map
47
+ end
48
+
49
+ def self.preprocessed_pathsets
50
+ @@preprocessed_pathsets
51
+ end
52
+
53
+ def self.custom_view_path(name)
54
+ name == "views" ? "app/views" : "test/split/#{name}/views"
55
+ end
56
+
57
+ def self.active_test?(key)
58
+ return false unless LOADED
59
+ SPLIT_TESTS.has_key?(key)
60
+ end
61
+
62
+ def self.view_path(key)
63
+ preprocessed_pathsets[custom_view_path(key)]
64
+ end
65
+
66
+ def self.random_test_key
67
+ split_test_map.sample
68
+ end
69
+ end
70
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Include hook code here
2
+ require 'split_tester/railtie'
3
+ SplitTester::Railtie.insert
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = 'split_tester'
4
+ s.version = '0.3'
5
+ s.summary = 'Provides A/B split testing functionality for Rails'
6
+ s.description = 'Split Tester provides support for A/B Split testing your pages with integration into Google Analytics.'
7
+
8
+ s.required_ruby_version = '>= 1.8.7'
9
+ s.required_rubygems_version = ">= 1.3.6"
10
+
11
+ s.author = 'Jeremy Hubert'
12
+ s.email = 'jhubert@gmail.com'
13
+ s.homepage = 'http://www.jeremyhubert.com'
14
+
15
+ s.add_dependency('rails', '>=3.0.0')
16
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class SplitTesterTest < ActiveSupport::TestCase
4
+ # Replace this with your real tests.
5
+ test "the truth" do
6
+ assert true
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'active_support'
data/uninstall.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Uninstall hook code here
2
+ puts "To clean up, just remove config/split_tests.yml and test/split/"
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: split_tester
3
+ version: !ruby/object:Gem::Version
4
+ hash: 13
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ version: "0.3"
10
+ platform: ruby
11
+ authors:
12
+ - Jeremy Hubert
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-25 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Split Tester provides support for A/B Split testing your pages with integration into Google Analytics.
22
+ email: jhubert@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README
29
+ files:
30
+ - init.rb
31
+ - install.rb
32
+ - MIT-LICENSE
33
+ - Rakefile
34
+ - README
35
+ - split_tester.gemspec
36
+ - uninstall.rb
37
+ - lib/generators/commands.rb
38
+ - lib/generators/split_test_generator.rb
39
+ - lib/generators/split_tester_install_generator.rb
40
+ - lib/generators/templates/split_tests.yml
41
+ - lib/generators/USAGE
42
+ - lib/split_tester/caching.rb
43
+ - lib/split_tester/controller.rb
44
+ - lib/split_tester/railtie.rb
45
+ - lib/split_tester/translation_helper.rb
46
+ - lib/split_tester.rb
47
+ - rails/init.rb
48
+ - files/split_tests.yml
49
+ - test/split_tester_test.rb
50
+ - test/test_helper.rb
51
+ has_rdoc: true
52
+ homepage: http://jeremyhubert.com/
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Provides A/B split testing functionality for Rails
85
+ test_files: []
86
+