ab_panel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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