abtest 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|