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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cec62b482674e24404cf39a8d5f0e7603a28fc16
4
- data.tar.gz: 48b6d4fbe6e12b4f871e476d8a08535718868a8d
3
+ metadata.gz: 0852f46342eab358cfacc8f07a5d1db0a7ed3b99
4
+ data.tar.gz: 4d431be07850f6a8ab6f034f66959083aa435b40
5
5
  SHA512:
6
- metadata.gz: 1788c07944936edf716f55f91a35d766a96f2bfd1113ea53d5ee1d04a92bde119d2db14254f9b3f46c4788d4a6877fb025497683e77ff6d08f2455cfd857b967
7
- data.tar.gz: 5944e39d2d7ddb12d8a13b41e87bb7b34e10391855853742fd46b998d75d236d15bfb9af151e22305c73fbee3a62a23fb850627d352fff49a8ce94b27c45b97d
6
+ metadata.gz: 364fc08b9a2f310a91962c183500501e61e50996ae566fbfc0854b2215ff43cbcb8675cf106a57a6663ba37a36dac665b51e57bb382ffef29ef5f0f35dc6acfa
7
+ data.tar.gz: 1b66303675bf12aabc23e13adc702bf9e61e217f14b474e7aea5adcaca480e5cea26115e1d5c591305c7b1a6d368b3011a53c282b9e4454e5da1ed3f73618165
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Abtest
2
2
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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
 
@@ -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{Railtie base to enable engine baset AB test modules}
12
- spec.summary = %q{Manages registered AB test engines and provides before_filter to dynamically add load paths for enabled tests}
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
 
@@ -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
@@ -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
@@ -10,4 +10,4 @@ module Abtest
10
10
  Abtest::Processor.process_tests(self)
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -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
- if (test_hash[:check].call(controller.request))
8
- controller.prepend_view_path(test_hash[:prefix])
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
@@ -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 = ActiveSupport::OrderedOptions.new
11
- config.abtest.registered_tests = Set.new
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
@@ -2,15 +2,12 @@ require "rails"
2
2
 
3
3
  module Abtest
4
4
  # Register a test. This method takes the following parameters:
5
- # view_path: View prefix of the directory where your view overrides are located
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(view_path, test, process = nil)
10
- # If this path is already in the configured paths, remove it.
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
- # Check to see if we have a experiments directory
8
- FileUtils.mkdir_p("#{Rails.root}/experiments")
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
- experiment_path = "#{Rails.root}/experiments/#{args[:name]}"
12
- application_css_path = "#{experiment_path}/assets/#{name}/stylesheets"
13
- image_path = "#{experiment_path}/assets/#{name}/images"
14
- view_path = "#{experiment_path}/views"
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(image_path)
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 = "#{Rails.root}/config/initializers/abtest.rb"
26
+ initializer_path = File.join(Rails.root, 'config', 'initializers', 'abtest.rb')
28
27
  unless File.exists?(initializer_path)
29
- initializer_template = File.read("#{File.dirname(__FILE__)}/templates/precompile_config.erb")
30
- renderer = ERB.new(initializer_template)
31
- result = renderer.result(binding)
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("#{File.dirname(__FILE__)}/templates/initializer.erb")
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("#{Rails.root}/experiments")
48
+ FileUtils.rm_rf(File.join(Rails.root, 'abtest'))
51
49
 
52
50
  # Remove initializer
53
- FileUtils.rm_f("#{Rails.root}/config/initializers/abtest.rb")
51
+ FileUtils.rm_f(File.join(Rails.root, 'config', 'initializers', 'abtest.rb'))
54
52
 
55
53
  puts "All tests removed"
56
54
  end
@@ -0,0 +1,2 @@
1
+ # Add Root for locating experimental assets
2
+ Rails.application.config.assets.paths << "<%= test_root %>"
@@ -13,9 +13,10 @@
13
13
 
14
14
  }
15
15
 
16
- Abtest.register_test("<%= experiment_path %>", <%= name %>_test, <%= name %>_process)
17
- Rails.application.config.assets.paths << "<%= experiment_path %>/assets/"
18
- Rails.application.config.assets.precompile += ['<%= experiment_path %>/assets/<%= name %>/stylesheets/application.scss']
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
  #
@@ -1,3 +1,3 @@
1
1
  module Abtest
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
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.5
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-01-09 00:00:00.000000000 Z
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: Railtie base to enable engine baset AB test modules
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/application.scss.erb
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 registered AB test engines and provides before_filter to dynamically
103
- add load paths for enabled tests
105
+ summary: Manages AB experiments and allows for view and asset context switching for
106
+ experiments.
104
107
  test_files: []
@@ -1,3 +0,0 @@
1
- @import "<%= "#{Rails.root}/app/assets/stylesheets/application" %>";
2
-
3
- /* IMPORT YOUR OVERRIDES HERE */
@@ -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