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 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