abtest 0.0.5 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -2
- data/abtest.gemspec +4 -4
- data/lib/abtest.rb +64 -0
- data/lib/abtest/asset.rb +44 -0
- data/lib/abtest/asset_task.rb +34 -0
- data/lib/abtest/filters.rb +1 -1
- data/lib/abtest/processor.rb +31 -3
- data/lib/abtest/railtie.rb +19 -3
- data/lib/abtest/registry.rb +3 -6
- data/lib/abtest/tasks/experiments.rake +22 -24
- data/lib/abtest/tasks/templates/abtest.erb +2 -0
- data/lib/abtest/tasks/templates/initializer.erb +4 -3
- data/lib/abtest/version.rb +1 -1
- metadata +10 -7
- data/lib/abtest/tasks/templates/application.scss.erb +0 -3
- data/lib/abtest/tasks/templates/precompile_config.erb +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0852f46342eab358cfacc8f07a5d1db0a7ed3b99
|
4
|
+
data.tar.gz: 4d431be07850f6a8ab6f034f66959083aa435b40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 364fc08b9a2f310a91962c183500501e61e50996ae566fbfc0854b2215ff43cbcb8675cf106a57a6663ba37a36dac665b51e57bb382ffef29ef5f0f35dc6acfa
|
7
|
+
data.tar.gz: 1b66303675bf12aabc23e13adc702bf9e61e217f14b474e7aea5adcaca480e5cea26115e1d5c591305c7b1a6d368b3011a53c282b9e4454e5da1ed3f73618165
|
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# Abtest
|
2
2
|
|
3
|
-
|
3
|
+
A-B testing framework and manager for Rails. This gem allows the addition of experiments that change view code and assets
|
4
|
+
based on the results of a simple test proc.
|
5
|
+
|
6
|
+
This gem modifies ActionView::Base as well as the global assets environment to enable overrides in the experiments directory to
|
7
|
+
take effect.
|
4
8
|
|
5
9
|
## Installation
|
6
10
|
|
@@ -18,7 +22,13 @@ Or install it yourself as:
|
|
18
22
|
|
19
23
|
## Usage
|
20
24
|
|
21
|
-
|
25
|
+
Once the gem is installed in your Rails application, rou can run the following command to set up an experiment:
|
26
|
+
|
27
|
+
$ bundle exec rake abtest:add_experiment[experiment_name]
|
28
|
+
|
29
|
+
To remove all experiments, run the following command:
|
30
|
+
|
31
|
+
$ bundle exec rake abtest:delete_experiments
|
22
32
|
|
23
33
|
## Contributing
|
24
34
|
|
data/abtest.gemspec
CHANGED
@@ -6,10 +6,10 @@ require 'abtest/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "abtest"
|
8
8
|
spec.version = Abtest::VERSION
|
9
|
-
spec.authors = ["Steve Saarinen"]
|
10
|
-
spec.email = ["ssaarinen@whitepages.com"]
|
11
|
-
spec.description = %q{
|
12
|
-
spec.summary = %q{Manages
|
9
|
+
spec.authors = ["Steve Saarinen", "Keatton Lee"]
|
10
|
+
spec.email = ["ssaarinen@whitepages.com", "klee@whitepages.com"]
|
11
|
+
spec.description = %q{Rails based AB test framework}
|
12
|
+
spec.summary = %q{Manages AB experiments and allows for view and asset context switching for experiments.}
|
13
13
|
spec.homepage = "http://www.whitepages.com"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
data/lib/abtest.rb
CHANGED
@@ -3,6 +3,70 @@ require "abtest/railtie"
|
|
3
3
|
require "abtest/processor"
|
4
4
|
require "abtest/registry"
|
5
5
|
require "abtest/filters"
|
6
|
+
require "abtest/asset"
|
7
|
+
require 'rails'
|
6
8
|
|
7
9
|
module Abtest
|
10
|
+
class ManifestManager
|
11
|
+
include Singleton
|
12
|
+
attr_accessor :manifests
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@manifests = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def retrieve_manifest name
|
19
|
+
manifests[name] ||= create_manifest(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_manifest name
|
23
|
+
app = Rails.application
|
24
|
+
experiment_path = File.join(app.root, 'abtest', 'experiments', name)
|
25
|
+
application_css_path = File.join(experiment_path, app.config.assets.prefix, 'stylesheets')
|
26
|
+
images_path = File.join(experiment_path, app.config.assets.prefix, 'images')
|
27
|
+
javascript_path = File.join(experiment_path, app.config.assets.prefix, 'javascript')
|
28
|
+
|
29
|
+
# Create a custom sprockets environment
|
30
|
+
experiment_environment = Sprockets::Environment.new(Rails.root.to_s) do |env|
|
31
|
+
env.context_class.class_eval do
|
32
|
+
include ::Sprockets::Rails::Helper
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Monkey patch class in-place with sass_config accessor
|
37
|
+
experiment_environment.context_class.extend(Sass::Rails::Railtie::SassContext)
|
38
|
+
|
39
|
+
# Always calculate digests and compile files
|
40
|
+
app.config.assets.digest = true
|
41
|
+
app.config.assets.compile = true
|
42
|
+
experiment_environment.cache = :null_store # Disables the Asset cache
|
43
|
+
|
44
|
+
experiment_environment.prepend_path("#{application_css_path}")
|
45
|
+
experiment_environment.prepend_path("#{images_path}")
|
46
|
+
experiment_environment.prepend_path("#{javascript_path}")
|
47
|
+
|
48
|
+
# Copy config.assets.paths to Sprockets
|
49
|
+
app.config.assets.paths.each do |path|
|
50
|
+
experiment_environment.append_path path
|
51
|
+
end
|
52
|
+
|
53
|
+
experiment_environment.js_compressor = app.config.assets.js_compressor
|
54
|
+
experiment_environment.css_compressor = app.config.assets.css_compressor
|
55
|
+
|
56
|
+
if app.config.logger
|
57
|
+
experiment_environment.logger = app.config.logger
|
58
|
+
else
|
59
|
+
experiment_environment.logger = Logger.new($stdout)
|
60
|
+
experiment_environment.logger.level = Logger::INFO
|
61
|
+
end
|
62
|
+
|
63
|
+
output_file = File.join(app.root, 'public', app.config.assets.prefix, 'experiments', name)
|
64
|
+
experiment_environment.context_class.assets_prefix = "#{app.config.assets.prefix}/experiments/#{name}"
|
65
|
+
experiment_environment.context_class.digest_assets = app.config.assets.digest
|
66
|
+
experiment_environment.context_class.config = app.config.action_controller
|
67
|
+
experiment_environment.context_class.sass_config = app.config.sass
|
68
|
+
|
69
|
+
manifests[name] = Sprockets::Manifest.new(experiment_environment, output_file)
|
70
|
+
end
|
71
|
+
end
|
8
72
|
end
|
data/lib/abtest/asset.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Sprockets
|
2
|
+
class Base
|
3
|
+
def find_asset(path, options = {})
|
4
|
+
logical_path = path
|
5
|
+
pathname = Pathname.new(path)
|
6
|
+
|
7
|
+
if pathname.absolute?
|
8
|
+
return unless stat(pathname)
|
9
|
+
logical_path = attributes_for(pathname).logical_path
|
10
|
+
else
|
11
|
+
begin
|
12
|
+
pathname = resolve(logical_path)
|
13
|
+
|
14
|
+
# If logical path is missing a mime type extension, append
|
15
|
+
# the absolute path extname so it has one.
|
16
|
+
#
|
17
|
+
# Ensures some consistency between finding "foo/bar" vs
|
18
|
+
# "foo/bar.js".
|
19
|
+
if File.extname(logical_path) == ""
|
20
|
+
expanded_logical_path = attributes_for(pathname).logical_path
|
21
|
+
logical_path += File.extname(expanded_logical_path)
|
22
|
+
end
|
23
|
+
rescue FileNotFound
|
24
|
+
# Check to see if we are in an experiment
|
25
|
+
if (path.starts_with?("experiments"))
|
26
|
+
Abtest.abtest_config.registered_tests.each do |test_hash|
|
27
|
+
if (path.starts_with?("experiments/#{test_hash[:name]}"))
|
28
|
+
# Strip experiment path
|
29
|
+
experiment_path = path.sub("experiments/#{test_hash[:name]}/", '')
|
30
|
+
|
31
|
+
# Grab experiment manifest
|
32
|
+
manifest = Abtest::ManifestManager.instance.retrieve_manifest(test_hash[:name])
|
33
|
+
return manifest.environment.index.find_asset(experiment_path, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
build_asset(logical_path, pathname, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'sprockets/rails'
|
2
|
+
|
3
|
+
module Abtest
|
4
|
+
class AssetTask < Sprockets::Rails::Task
|
5
|
+
attr_accessor :app
|
6
|
+
|
7
|
+
def initialize(app = nil)
|
8
|
+
self.app = app
|
9
|
+
super(app)
|
10
|
+
end
|
11
|
+
|
12
|
+
def define
|
13
|
+
namespace :abtest do
|
14
|
+
desc "Compile all the assets named in config.assets.precompile"
|
15
|
+
task :precompile => :environment do
|
16
|
+
configured_experiments = app.config.abtest.registered_tests
|
17
|
+
|
18
|
+
# Precompile assets for each experiment
|
19
|
+
configured_experiments.each do |experiment|
|
20
|
+
name = experiment[:name]
|
21
|
+
manifest = Abtest::ManifestManager.instance.retrieve_manifest(name)
|
22
|
+
|
23
|
+
# Add our experiments asset path
|
24
|
+
assets << lambda {|filename, path| path =~ /#{name}\/assets/ && !%w(.js .css).include?(File.extname(filename))}
|
25
|
+
|
26
|
+
with_logger do
|
27
|
+
manifest.compile(assets)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/abtest/filters.rb
CHANGED
data/lib/abtest/processor.rb
CHANGED
@@ -3,12 +3,40 @@ require 'rails'
|
|
3
3
|
module Abtest
|
4
4
|
class Processor
|
5
5
|
def self.process_tests controller
|
6
|
+
|
7
|
+
experiment_activated = false
|
8
|
+
|
6
9
|
Abtest.abtest_config.registered_tests.each do |test_hash|
|
7
|
-
|
8
|
-
|
10
|
+
app_config = Rails.application.config
|
11
|
+
environment = Rails.application.assets
|
12
|
+
experiment_name = test_hash[:name]
|
13
|
+
experiment_path = Rails.root.join('abtest', 'experiments', experiment_name)
|
14
|
+
|
15
|
+
if (test_hash[:check].call(controller.request) && !experiment_activated)
|
16
|
+
# ensure experimental translations are loaded
|
17
|
+
unless (I18n.load_path || []).last.include?(experiment_name)
|
18
|
+
I18n.load_path = app_config.i18n.load_path + Dir[Rails.root.join('abtest', 'experiments', experiment_name, 'config', 'locales', '*.{rb,yml}').to_s]
|
19
|
+
I18n.reload!
|
20
|
+
end
|
21
|
+
|
22
|
+
manifest = Abtest::ManifestManager.instance.retrieve_manifest(experiment_name)
|
23
|
+
|
24
|
+
# Set view context for asset path
|
25
|
+
controller.view_context_class.assets_prefix = File.join(app_config.assets.prefix, 'experiments', experiment_name)
|
26
|
+
controller.view_context_class.assets_environment = manifest.environment
|
27
|
+
controller.view_context_class.assets_manifest = manifest
|
28
|
+
|
29
|
+
# Prepend the lookup paths for our views
|
30
|
+
controller.prepend_view_path(File.join(experiment_path, 'views'))
|
31
|
+
|
9
32
|
test_hash[:process].call(controller) unless test_hash[:process].nil?
|
33
|
+
|
34
|
+
experiment_activated = true
|
35
|
+
elsif (!experiment_activated)
|
36
|
+
# ensure experimental translations are removed
|
37
|
+
I18n.reload! if I18n.load_path.reject! { |path| path.include?(experiment_name) }
|
10
38
|
end
|
11
39
|
end
|
12
40
|
end
|
13
41
|
end
|
14
|
-
end
|
42
|
+
end
|
data/lib/abtest/railtie.rb
CHANGED
@@ -2,19 +2,35 @@ require 'rails'
|
|
2
2
|
|
3
3
|
module Abtest
|
4
4
|
class Railtie < ::Rails::Railtie
|
5
|
+
rake_tasks do |app|
|
6
|
+
require 'abtest/asset_task'
|
7
|
+
Abtest::AssetTask.new(app)
|
8
|
+
end
|
9
|
+
|
5
10
|
rake_tasks do
|
6
11
|
Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
|
7
12
|
end
|
8
|
-
|
13
|
+
|
9
14
|
initializer "abtest.set_config", :after => 'bootstrap_hook' do
|
10
|
-
config.abtest
|
11
|
-
config.abtest.registered_tests
|
15
|
+
config.abtest = ActiveSupport::OrderedOptions.new
|
16
|
+
config.abtest.registered_tests = Set.new
|
17
|
+
config.abtest.precompile_assets = Array.new
|
12
18
|
end
|
13
19
|
|
14
20
|
initializer "abtest.set_filter" do
|
15
21
|
ActiveSupport.on_load(:action_controller) do
|
16
22
|
ActionController::Base.send(:include, Abtest::Filters)
|
17
23
|
end
|
24
|
+
|
25
|
+
module ActionView
|
26
|
+
module Rendering
|
27
|
+
module ClassMethods
|
28
|
+
def view_context
|
29
|
+
view_context_class.new(view_renderer, view_assigns, self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
18
34
|
end
|
19
35
|
end
|
20
36
|
end
|
data/lib/abtest/registry.rb
CHANGED
@@ -2,15 +2,12 @@ require "rails"
|
|
2
2
|
|
3
3
|
module Abtest
|
4
4
|
# Register a test. This method takes the following parameters:
|
5
|
-
#
|
5
|
+
# name: Name of the experiment.
|
6
6
|
# test: A lambda used to determine whether or not to activate this test. The provided
|
7
7
|
# lambda must return a truthy value for the test to be activated and accept a request object.
|
8
8
|
# process (optional): Lambda to run in the case of a test being activated. Will be passed the controller object.
|
9
|
-
def self.register_test(
|
10
|
-
|
11
|
-
# This is used mainly for engine based tests.
|
12
|
-
ActionController::Base.view_paths = ActionController::Base.view_paths.reject {|path| path.to_path == view_path }
|
13
|
-
abtest_config.registered_tests.add({prefix: view_path, check: test, process: process})
|
9
|
+
def self.register_test(name, test, process = nil)
|
10
|
+
abtest_config.registered_tests.add({name: name, check: test, process: process})
|
14
11
|
end
|
15
12
|
|
16
13
|
def self.abtest_config
|
@@ -2,39 +2,37 @@ namespace :abtest do
|
|
2
2
|
desc "Create a new experiment scaffold. Experiment name is a required arg. (rake abtest:add_experiment[name])"
|
3
3
|
task :add_experiment, [:name] => :environment do |t, args|
|
4
4
|
name = args[:name]
|
5
|
-
puts "Experiment name is required" and return if name.nil?
|
6
5
|
|
7
|
-
|
8
|
-
|
6
|
+
if (name.nil? || name.blank?)
|
7
|
+
puts "Experiment name is required. Usage: rake abtest:add_experiment[name]"
|
8
|
+
next
|
9
|
+
end
|
9
10
|
|
10
11
|
# Add directories for views and assets
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
app_config = Rails.application.config
|
13
|
+
test_root = File.join(Rails.root, 'abtest')
|
14
|
+
experiment_path = File.join(test_root, 'experiments', name)
|
15
|
+
application_css_path = File.join(experiment_path, app_config.assets.prefix, 'stylesheets')
|
16
|
+
images_path = File.join(experiment_path, app_config.assets.prefix, 'images')
|
17
|
+
javascript_path = File.join(experiment_path, app_config.assets.prefix, 'javascript')
|
18
|
+
view_path = File.join(experiment_path, 'views')
|
19
|
+
|
15
20
|
FileUtils.mkdir_p(view_path)
|
16
21
|
FileUtils.mkdir_p(application_css_path)
|
17
|
-
FileUtils.mkdir_p(
|
18
|
-
|
19
|
-
# Add template stylesheet
|
20
|
-
css_template = File.read("#{File.dirname(__FILE__)}/templates/application.scss.erb")
|
21
|
-
renderer = ERB.new(css_template)
|
22
|
-
css_result = renderer.result(binding)
|
23
|
-
|
24
|
-
File.open("#{application_css_path}/application.scss", 'w') {|f| f.write(css_result) }
|
22
|
+
FileUtils.mkdir_p(images_path)
|
23
|
+
FileUtils.mkdir_p(javascript_path)
|
25
24
|
|
26
25
|
# Create a new initializer file if it doesn't exist already
|
27
|
-
initializer_path =
|
26
|
+
initializer_path = File.join(Rails.root, 'config', 'initializers', 'abtest.rb')
|
28
27
|
unless File.exists?(initializer_path)
|
29
|
-
|
30
|
-
renderer
|
31
|
-
result
|
32
|
-
|
33
|
-
File.open(initializer_path, 'a') { |f| f.write(result) }
|
28
|
+
ab_template = File.read(File.join(File.dirname(__FILE__), 'templates', 'abtest.erb'))
|
29
|
+
renderer = ERB.new(ab_template)
|
30
|
+
result = renderer.result(binding)
|
31
|
+
File.open(initializer_path, 'w') {|f| f.write(result) }
|
34
32
|
end
|
35
33
|
|
36
34
|
# Add template initializer
|
37
|
-
template = File.read(
|
35
|
+
template = File.read(File.join(File.dirname(__FILE__), 'templates', 'initializer.erb'))
|
38
36
|
renderer = ERB.new(template)
|
39
37
|
result = renderer.result(binding)
|
40
38
|
|
@@ -47,10 +45,10 @@ namespace :abtest do
|
|
47
45
|
desc "Delete all experiments"
|
48
46
|
task :delete_experiments => :environment do
|
49
47
|
# Remove experiments directory
|
50
|
-
FileUtils.rm_rf(
|
48
|
+
FileUtils.rm_rf(File.join(Rails.root, 'abtest'))
|
51
49
|
|
52
50
|
# Remove initializer
|
53
|
-
FileUtils.rm_f(
|
51
|
+
FileUtils.rm_f(File.join(Rails.root, 'config', 'initializers', 'abtest.rb'))
|
54
52
|
|
55
53
|
puts "All tests removed"
|
56
54
|
end
|
@@ -13,9 +13,10 @@
|
|
13
13
|
|
14
14
|
}
|
15
15
|
|
16
|
-
Abtest.register_test("<%=
|
17
|
-
|
18
|
-
|
16
|
+
Abtest.register_test("<%= name %>", <%= name %>_test, <%= name %>_process)
|
17
|
+
|
18
|
+
# Add additional files to precompile here
|
19
|
+
Rails.application.config.abtest.precompile_assets = []
|
19
20
|
|
20
21
|
################################################
|
21
22
|
#
|
data/lib/abtest/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abtest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steve Saarinen
|
8
|
+
- Keatton Lee
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2014-
|
12
|
+
date: 2014-05-28 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rails
|
@@ -52,9 +53,10 @@ dependencies:
|
|
52
53
|
- - '>='
|
53
54
|
- !ruby/object:Gem::Version
|
54
55
|
version: '0'
|
55
|
-
description:
|
56
|
+
description: Rails based AB test framework
|
56
57
|
email:
|
57
58
|
- ssaarinen@whitepages.com
|
59
|
+
- klee@whitepages.com
|
58
60
|
executables: []
|
59
61
|
extensions: []
|
60
62
|
extra_rdoc_files: []
|
@@ -67,14 +69,15 @@ files:
|
|
67
69
|
- Rakefile
|
68
70
|
- abtest.gemspec
|
69
71
|
- lib/abtest.rb
|
72
|
+
- lib/abtest/asset.rb
|
73
|
+
- lib/abtest/asset_task.rb
|
70
74
|
- lib/abtest/filters.rb
|
71
75
|
- lib/abtest/processor.rb
|
72
76
|
- lib/abtest/railtie.rb
|
73
77
|
- lib/abtest/registry.rb
|
74
78
|
- lib/abtest/tasks/experiments.rake
|
75
|
-
- lib/abtest/tasks/templates/
|
79
|
+
- lib/abtest/tasks/templates/abtest.erb
|
76
80
|
- lib/abtest/tasks/templates/initializer.erb
|
77
|
-
- lib/abtest/tasks/templates/precompile_config.erb
|
78
81
|
- lib/abtest/version.rb
|
79
82
|
homepage: http://www.whitepages.com
|
80
83
|
licenses:
|
@@ -99,6 +102,6 @@ rubyforge_project:
|
|
99
102
|
rubygems_version: 2.1.11
|
100
103
|
signing_key:
|
101
104
|
specification_version: 4
|
102
|
-
summary: Manages
|
103
|
-
|
105
|
+
summary: Manages AB experiments and allows for view and asset context switching for
|
106
|
+
experiments.
|
104
107
|
test_files: []
|
@@ -1,13 +0,0 @@
|
|
1
|
-
Rails.application.config.assets.precompile << Proc.new do |path|
|
2
|
-
unless path =~ /\.(css|js)\z/
|
3
|
-
full_path = Rails.application.assets.resolve(path).to_path
|
4
|
-
app_assets_path = "<%= experiment_path %>/assets/"
|
5
|
-
if full_path.starts_with? app_assets_path
|
6
|
-
true
|
7
|
-
else
|
8
|
-
false
|
9
|
-
end
|
10
|
-
else
|
11
|
-
false
|
12
|
-
end
|
13
|
-
end
|