ab_panel 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ab_panel.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Wouter de Vos
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # AbPanel
2
+
3
+ Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.
4
+
5
+ Create a config file with one or more experiments and conditions.
6
+
7
+ In `config/ab_panel.yml`
8
+
9
+ ```yaml
10
+ my_experiment:
11
+ - condition_b
12
+ - condition_c
13
+ ```
14
+
15
+ Note that this will create 3 conditions:
16
+
17
+ 1. Original condition (control condition)
18
+ 2. Condition B
19
+ 3. Condition C
20
+
21
+ You can add as many experiments and conditions as you want. Every visitor
22
+ will be assigned randomly to one condition for each scenario for as long as
23
+ their session remains active.
24
+
25
+ In your application controller:
26
+
27
+ ```ruby
28
+ class ApplicationController < ActionController::Base
29
+ initialize_ab_panel!
30
+ end
31
+ ```
32
+
33
+ Then track any event you want from your controller:
34
+
35
+ ```ruby
36
+ class CoursesController < ApplicationController
37
+ track_action '[visits] Booking form', { :only => :book_now, :course => :id }
38
+
39
+ # controller code here
40
+ end
41
+ ```
42
+
43
+ Use conditions based on experiments and conditions throughout your code, e.g. in your views:
44
+
45
+ ```erb
46
+ <% if AbPanel.my_experiment.condition_b? %>
47
+ <p>Hi there, you are in Condition B in my experiment.</p>
48
+ <% else %>
49
+ <p>Hi there, you are either in the Original condition or in Condition C in my experiment.</p>
50
+
51
+ <% if AbPanel.my_experiment.condition_c? %>
52
+ <p>Ah, you're in C.</p>
53
+ <% end %>
54
+ <% end %>
55
+ ```
56
+
57
+ Or in your controller:
58
+
59
+ ```ruby
60
+ case AbPanel.my_experiment.condition
61
+ when 'condition_b'
62
+ render 'my_experiment/condition_b'
63
+ when 'condition_c'
64
+ render 'my_experiment/condition_c'
65
+ else
66
+ render 'index'
67
+ end
68
+ ```
69
+
70
+ ## Installation
71
+
72
+ Add this line to your application's Gemfile:
73
+
74
+ gem 'ab_panel'
75
+
76
+ And then execute:
77
+
78
+ $ bundle
79
+
80
+ Or install it yourself as:
81
+
82
+ $ gem install ab_panel
83
+
84
+ ## Usage
85
+
86
+ TODO: Write usage instructions here
87
+
88
+ ## Contributing
89
+
90
+ 1. Fork it
91
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
92
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
93
+ 4. Push to the branch (`git push origin my-new-feature`)
94
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/ab_panel.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ab_panel/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ab_panel"
8
+ spec.version = AbPanel::VERSION
9
+ spec.authors = ["Wouter de Vos"]
10
+ spec.email = ["wouter@springest.com"]
11
+ spec.description = %q{Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.}
12
+ spec.summary = %q{Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.}
13
+ spec.homepage = "https://github.com/Springest/ab_panel"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rails", '~> 3.2'
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "fakeweb"
25
+ spec.add_development_dependency "rspec"
26
+
27
+ spec.add_runtime_dependency "mixpanel"
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'ostruct'
2
+
3
+ module AbPanel
4
+ class Config
5
+ def initialize
6
+ OpenStruct.new settings
7
+ end
8
+
9
+ def experiments
10
+ settings.keys.map(&:to_sym)
11
+ end
12
+
13
+ def scenarios(experiment)
14
+ raise ArgumentError.new( "Fatal: Experiment config not found for #{experiment}" ) unless experiments.include? experiment.to_sym
15
+ ( settings[experiment.to_sym].map(&:to_sym) + [:original] ).uniq
16
+ end
17
+
18
+
19
+ def settings
20
+ @settings ||= YAML.load(
21
+ ERB.new(File.read(File.join(Rails.root, 'config', 'ab_panel.yml'))).result)
22
+ .symbolize_keys
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,106 @@
1
+ module AbPanel
2
+ module ControllerAdditions
3
+ extend ActiveSupport::Concern
4
+
5
+ # This sets a unique id for this user.
6
+ #
7
+ # You could override this in your ApplicationController to use your
8
+ # own implementation, e.g.:
9
+ #
10
+ # `current_user.id` for logged in users.
11
+ def ab_panel_id
12
+ session[:ab_panel_id] ||=
13
+ (0..4).map { |i| i.even? ? ('A'..'Z').to_a[rand(26)] : rand(10) }.join
14
+ end
15
+
16
+ # Sets the environment hash for every request.
17
+ #
18
+ # Experiment conditions and unique user id are preserved
19
+ # in the user's session.
20
+ #
21
+ # You could override this to match your own env.
22
+ def ab_panel_env
23
+ {
24
+ 'REMOTE_ADDR' => request['REMOTE_ADDR'],
25
+ 'HTTP_X_FORWARDED_FOR' => request['HTTP_X_FORWARDED_FOR'],
26
+ 'rack.session' => request['rack.session'],
27
+ 'rails.env' => Rails.env
28
+ }
29
+ end
30
+
31
+ module ClassMethods
32
+ # Initializes AbPanel's environment.
33
+ #
34
+ # Typically, this would go in the ApplicationController.
35
+ #
36
+ # class ApplicationController < ActionController::Base
37
+ # initialize_ab_panel!
38
+ # end
39
+ #
40
+ # This makes sure an ab_panel session is re-initialized on every
41
+ # request. Experiment conditions and unique user id are preserved
42
+ # in the user's session.
43
+ def initialize_ab_panel!
44
+ self.before_filter(options.slice(:only, :except)) do |controller|
45
+ # Persist the conditions.
46
+ AbPanel.conditions = controller.session['ab_panel_conditions']
47
+
48
+ {
49
+ 'mixpanel_events' => controller.request['mixpanel_events'],
50
+ 'ab_panel_id' => controller.ab_panel_id
51
+ }.merge(controller.ab_panel_env).each do |key, val|
52
+ AbPanel.env_set key, val
53
+ end
54
+ end
55
+ end
56
+
57
+ # Track controller actions visits.
58
+ #
59
+ # name - The name of the event in Mixpanel.
60
+ # properties - The properties to be associated with the event.
61
+ #
62
+ # Example:
63
+ #
64
+ # track_action '[visits] Booking form', { :only => :book_now, :course => :id }
65
+ #
66
+ # This will track the event with the given name on CoursesController#book_now
67
+ # and assign an options hash:
68
+ #
69
+ # { 'course_id' => @course.id }
70
+ def track_visit(name, properties={})
71
+ self.after_filter(options.slice(:only, :except)) do |controller|
72
+ options = {
73
+ distinct_id: ab_panel_id,
74
+ time: Time.now,
75
+ time_utc: Time.now.utc
76
+ }
77
+
78
+ properties.each do |key, val|
79
+ if controller.respond_to?(key)
80
+ inst = controller.send(key)
81
+ elsif controller.instance_variable_defined?("@#{key}")
82
+ inst = controller.instance_variable_get("@#{key}")
83
+ else
84
+ options[key] = val
85
+ next
86
+ end
87
+
88
+ val = *val
89
+
90
+ val.each do |m|
91
+ options["#{key}_#{m}"] = inst.send(m)
92
+ end
93
+ end
94
+ end
95
+
96
+ AbPanel.track name, options
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ if defined? ActionController::Base
103
+ ActionController::Base.class_eval do
104
+ include AbPanel::ControllerAdditions
105
+ end
106
+ end
@@ -0,0 +1,49 @@
1
+ require 'mixpanel'
2
+
3
+ module AbPanel
4
+ module Mixpanel
5
+ class Tracker < ::Mixpanel::Tracker
6
+ def initialize(options={})
7
+ @tracker = ::Mixpanel::Tracker.new Config.token, ab_panel_options.merge(options)
8
+ end
9
+
10
+ def ab_panel_options
11
+ {
12
+ api_key: Config.api_key,
13
+ env: AbPanel.env
14
+ }
15
+ end
16
+
17
+ def track(event_name, properties, options={})
18
+ if defined?(Resque)
19
+ Resque.enqueue ResqueTracker, event_name, properties, options
20
+ else
21
+ @tracker.track event_name, properties, options
22
+ end
23
+ end
24
+ end
25
+
26
+ class ResqueTracker
27
+ @queue = :ab_panel
28
+
29
+ def self.perform(event_name, properties, options={})
30
+ Tracker.new.track(event_name, properties, options)
31
+ end
32
+ end
33
+
34
+ class Config
35
+ def self.api_key
36
+ config['api_key']
37
+ end
38
+
39
+ def self.token
40
+ config['token']
41
+ end
42
+
43
+ def self.config
44
+ @settings ||= YAML.load(
45
+ ERB.new(File.read(File.join(Rails.root, 'config', 'mixpanel.yml'))).result)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module AbPanel
2
+ VERSION = "0.0.1"
3
+ end
data/lib/ab_panel.rb ADDED
@@ -0,0 +1,77 @@
1
+ puts "Ab Panel."
2
+
3
+ Dir[File.expand_path(File.join(
4
+ File.dirname(__FILE__),'ab_panel','**','*.rb'))]
5
+ .each {|f| require f}
6
+
7
+ module AbPanel
8
+ class << self
9
+
10
+ # Track event in Mixpanel backend.
11
+ def track(event_name, properties, options={})
12
+ tracker.track event_name, properties, options
13
+ end
14
+
15
+ def conditions
16
+ @conditions ||= assign_conditions!
17
+ end
18
+
19
+ # Set the experiment's conditions.
20
+ #
21
+ # This is used to persist conditions from
22
+ # the session.
23
+ def conditions=(custom_conditions)
24
+ @conditions = custom_conditions || conditions
25
+ end
26
+
27
+ def experiments
28
+ config.experiments
29
+ end
30
+
31
+ def scenarios(experiment)
32
+ config.scenarios experiment
33
+ end
34
+
35
+ def env_set key, val
36
+ env[key] = val
37
+ end
38
+
39
+ def env
40
+ @env ||= {
41
+ 'conditions' => conditions
42
+ }
43
+ end
44
+
45
+ private # ----------------------------------------------------------------------------
46
+
47
+ def assign_conditions!
48
+ cs = {}
49
+
50
+ experiments.each do |experiment|
51
+ cs[experiment] ||= {}
52
+
53
+ scenarios(experiment).each do |scenario|
54
+ cs[experiment]["#{scenario}?"] = false
55
+ end
56
+
57
+ selected = scenarios(experiment)[rand(scenarios(experiment).size)]
58
+
59
+ cs[experiment]["#{selected}?"] = true
60
+
61
+ cs[experiment][:condition] = selected
62
+
63
+ cs[experiment] = OpenStruct.new cs[experiment]
64
+ end
65
+
66
+ OpenStruct.new cs
67
+ end
68
+
69
+ def tracker
70
+ @tracker ||= Mixpanel::Tracker.new
71
+ end
72
+
73
+ def config
74
+ @config ||= Config.new
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ class Controller
4
+ include AbPanel::ControllerAdditions
5
+
6
+ def session
7
+ @session ||= {}
8
+ end
9
+ end
10
+
11
+ describe AbPanel::ControllerAdditions do
12
+ let(:controller) { Controller.new }
13
+
14
+ describe "#ab_panel_id" do
15
+ subject { controller.ab_panel_id }
16
+
17
+ it { should match /^([A-Z]|[0-9])([A-Z]|[0-9])([A-Z]|[0-9])([A-Z]|[0-9])([A-Z]|[0-9])$/ }
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe AbPanel do
4
+ describe ".experiments" do
5
+ subject { AbPanel.experiments }
6
+
7
+ it { should =~ %w(experiment1 experiment2).map(&:to_sym) }
8
+ end
9
+
10
+ describe ".scenarios" do
11
+ subject { AbPanel.scenarios(experiment) }
12
+
13
+ let(:experiment) { AbPanel.experiments.first }
14
+
15
+ it { should =~ %w( scenario1 scenario2 scenario3 original ).map(&:to_sym) }
16
+
17
+ describe "With an unexisting experiment" do
18
+ let(:experiment) { :does_not_exist }
19
+
20
+ it 'should throw an ArgumentError' do
21
+ expect { subject }.to raise_exception ArgumentError
22
+ end
23
+ end
24
+ end
25
+
26
+ describe ".conditions" do
27
+ subject { AbPanel.conditions.experiment1 }
28
+
29
+ it { should respond_to :scenario1? }
30
+ it { should respond_to :original? }
31
+
32
+ describe 'uniqueness' do
33
+ let(:conditions) do
34
+ [
35
+ subject.scenario1?,
36
+ subject.scenario2?,
37
+ subject.scenario3?,
38
+ subject.original?
39
+ ]
40
+ end
41
+
42
+ it { conditions.any?.should be true }
43
+ it { conditions.all?.should be false }
44
+ it { conditions.select{|c| c}.size.should be 1 }
45
+ it { conditions.reject{|c| c}.size.should be 3 }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ puts "Spec Helper."
2
+ require 'rubygems'
3
+ require 'active_support/all'
4
+
5
+ require File.join(File.dirname(__FILE__), "../lib", "ab_panel")
6
+
7
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
8
+
@@ -0,0 +1,5 @@
1
+ # Fakeweb
2
+ require 'fakeweb'
3
+
4
+ FakeWeb.allow_net_connect = false
5
+ FakeWeb.register_uri(:any, /http:\/\/api\.mixpanel\.com.*/, :body => "1")
@@ -0,0 +1,9 @@
1
+ experiment1:
2
+ - scenario1
3
+ - scenario2
4
+ - scenario3
5
+
6
+ experiment2:
7
+ - scenario4
8
+ - scenario5
9
+
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+
3
+ RSpec.configure do |c|
4
+ c.before do
5
+ Rails.stub(:root) { File.expand_path( '../files', __FILE__ ) }
6
+ Rails.stub(:env) { 'test' }
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,171 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ab_panel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wouter de Vos
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rails
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.2'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.2'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: fakeweb
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: mixpanel
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.
111
+ email:
112
+ - wouter@springest.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - Gemfile
119
+ - LICENSE.txt
120
+ - README.md
121
+ - Rakefile
122
+ - ab_panel.gemspec
123
+ - lib/ab_panel.rb
124
+ - lib/ab_panel/config.rb
125
+ - lib/ab_panel/controller_additions.rb
126
+ - lib/ab_panel/mixpanel.rb
127
+ - lib/ab_panel/version.rb
128
+ - spec/ab_panel/controller_additions_spec.rb
129
+ - spec/ab_panel_spec.rb
130
+ - spec/spec_helper.rb
131
+ - spec/support/fakeweb.rb
132
+ - spec/support/files/config/ab_panel.yml
133
+ - spec/support/rails.rb
134
+ homepage: https://github.com/Springest/ab_panel
135
+ licenses:
136
+ - MIT
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ segments:
148
+ - 0
149
+ hash: -2064797611624218648
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ none: false
152
+ requirements:
153
+ - - ! '>='
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ segments:
157
+ - 0
158
+ hash: -2064797611624218648
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 1.8.23
162
+ signing_key:
163
+ specification_version: 3
164
+ summary: Run A/B test experiments on your Rails 3+ site using Mixpanel as a backend.
165
+ test_files:
166
+ - spec/ab_panel/controller_additions_spec.rb
167
+ - spec/ab_panel_spec.rb
168
+ - spec/spec_helper.rb
169
+ - spec/support/fakeweb.rb
170
+ - spec/support/files/config/ab_panel.yml
171
+ - spec/support/rails.rb