abingo_port 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ .svn
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Patrick McKenzie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,160 @@
1
+ A/Bingo Version 1.0.0 - Rails 3 Version
2
+ =======================================
3
+
4
+ **This is a port of ABingo to work as a rails 3 plugin. It is not extensivly tested but appears to work. Future work will make it easier to use.**
5
+
6
+ **Known Issues with rails 3**
7
+
8
+ * **Named Conversions do not work (eg :conversion => "signup")**
9
+
10
+
11
+
12
+ Rails A/B testing. One minute to install. One line to set up a new A/B test.
13
+ One line to track conversion.
14
+
15
+ For usage notes, see: http://www.bingocardcreator.com/abingo
16
+
17
+ Installation instructions are below usage examples.
18
+
19
+ Key default features:
20
+
21
+ * Conversions only tracked once per individual.
22
+ * Conversions only tracked if individual saw test.
23
+ * Same individual ALWAYS sees same alternative for same test.
24
+ * Syntax sugar. Specify alternatives as a range, array, hash of alternative to weighting, or just let it default to true or false.
25
+ * A simple z-test of statistical significance, with output so clear anyone in your organization
26
+ can understand it.
27
+
28
+ Example: View
29
+ -------------
30
+
31
+ <% ab_test("login_button", ["/images/button1.jpg", "/images/button2.jpg"]) do |button_file| %>
32
+ <%= img_tag(button_file, :alt => "Login!") %>
33
+ <% end %>
34
+
35
+ Example: Controller
36
+ -------------------
37
+
38
+ def register_new_user
39
+ #See what level of free points maximizes users' decision to buy replacement points.
40
+ @starter_points = ab_test("new_user_free_points", [100, 200, 300])
41
+ end
42
+
43
+ Example: Controller
44
+ -------------------
45
+
46
+ def registration
47
+ if (ab_test("send_welcome_email"), :conversion => "purchase")
48
+ #send the email, track to see if it later increases conversion to full version
49
+ end
50
+ end
51
+
52
+ Example: Conversion tracking (in a controller!)
53
+ ------------------------------------------------
54
+
55
+ def buy_new_points
56
+ #some business logic
57
+ bingo!("buy_new_points") #Either a conversion named with :conversion or a test name.
58
+ end
59
+
60
+ Example: Conversion tracking (in a view)
61
+ -----------------------------------------
62
+
63
+ Thanks for signing up, dude! <% bingo!("signup_page_redesign") >
64
+
65
+ Example: Statistical Significance Testing
66
+ ------------------------------------------
67
+
68
+ Abingo::Experiment.last.describe_result_in_words
69
+ => "The best alternative you have is: [0], which had 130 conversions from 5000 participants (2.60%).
70
+ The other alternative was [1], which had 1800 conversions from 100000 participants (1.80%).
71
+ This difference is 99.9% likely to be statistically significant, which means you can be extremely
72
+ confident that it is the result of your alternatives actually mattering, rather than being due to
73
+ random chance. However, this doesn't say anything about how much the first alternative is really
74
+ likely to be better by."
75
+
76
+ Installation
77
+ ==================
78
+
79
+ Configure the Gem
80
+ ------------------
81
+
82
+ gem 'abingo', :git => "git://github.com/wildfalcon/abingo.git", :branch => "rails3"
83
+
84
+ bundle install
85
+
86
+ Generate the database tables
87
+ -----------------------------
88
+ Creates tables "experiments" and "alternatives". If you use these names already you will need to do some hacking)
89
+
90
+
91
+ rails g abingo_migration
92
+ rake db:migrate
93
+
94
+ Configure a Cache
95
+ -----------------
96
+ A/Bingo makes HEAVY use of the cache to reduce load on thedatabase and share potentially long-lived "temporary" data, such as what alternative a given visitor should be shown for a particular test.
97
+
98
+ A/Bingo defaults to using the same cache store as Rails. These instructions
99
+ are on how to use the memcache-addon in heroku.
100
+
101
+ heroku addons:add memcache:5mb
102
+
103
+ #Gemile
104
+ group :production do
105
+ gem "memcache-client"
106
+ gem 'memcached-northscale', :require => 'memcached'
107
+ end
108
+
109
+
110
+ #config/environments/production.rb
111
+ # Use a different cache store in production
112
+ # config.cache_store = :mem_cache_store
113
+ config.cache_store = :mem_cache_store, Memcached::Rails.new
114
+
115
+
116
+ Tell A/Bingo a user's identity
117
+ -------------------------------
118
+
119
+ So abingo knows who a users is if they come back to a test. (The same identity will always see the same alternative for the same test.) How you do this is up to you -- I suggest integrating with your login/account infrastructure. The simplest thing that can possibly work
120
+
121
+ #Somewhere in application.rb
122
+ before_filter :set_abingo_identity
123
+
124
+ def set_abingo_identity
125
+ #treat all bots as one user to prevent skewing results
126
+ if request.user_agent =~ /\b(Baidu|Gigabot|Googlebot|libwww-perl|lwp-trivial|msnbot|SiteUptime|Slurp|WordPress|ZIBB|ZyBorg)\b/i
127
+ Abingo.identity = "robot"
128
+ elsif current_user
129
+ Abingo.identity = current_user.id
130
+ else
131
+ session[:abingo_identity] ||= rand(10 ** 10)
132
+ Abingo.identity = session[:abingo_identity]
133
+ end
134
+ end
135
+
136
+ Create the Dashboard
137
+ --------------------
138
+
139
+ You need to create a controller which includes the methods from the Abingo module, as well as generate the views. You can customise the view if you wish.
140
+ Don't forget to authenticate access to the controller
141
+
142
+ rails g controller admin/abingo_dashboard
143
+
144
+ #app/controllers/admin/abingo_dashboard_controller.rb
145
+ class Admin::AbingoDashboardController < ApplicationController
146
+ include Abingo::Controller::Dashboard
147
+ end
148
+
149
+ #routes.rb
150
+ namespace :admin do
151
+ get "ab_dashboard" => "abingo_dashboard#index"
152
+ post "ab_end_experiment/:id" => "abingo_dashboard#end_experiment"
153
+ end
154
+
155
+ rails g abingo_views
156
+
157
+ Run your first test
158
+ ====================
159
+
160
+ Copyright (c) 2009-2010 Patrick McKenzie, released under the MIT license
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "abingo_port"
8
+ gem.summary = %Q{A/B Testing for Rails}
9
+ gem.description = %Q{Incorperate AB Testing into your rails apps}
10
+ gem.email = "laurie@wildfalcon.com"
11
+ gem.homepage = "http://github.com/wildfalcon/abingo"
12
+ gem.authors = ["Wildfalcon"]
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/test_*.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+ task :test => :check_dependencies
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "baby_railtie #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{abingo}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Wildfalcon"]
12
+ s.date = %q{2010-08-07}
13
+ s.description = %q{Incorperate AB Testing into your rails apps}
14
+ s.email = %q{laurie@wildfalcon.com}
15
+ s.extra_rdoc_files = [
16
+ "README"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "abingo.gemspec",
25
+ "init.rb",
26
+ "install.rb",
27
+ "lib/abingo.rb",
28
+ "lib/abingo/alternative.rb",
29
+ "lib/abingo/controller/dashboard.rb",
30
+ "lib/abingo/conversion_rate.rb",
31
+ "lib/abingo/experiment.rb",
32
+ "lib/abingo/rails/controller/dashboard.rb",
33
+ "lib/abingo/railtie.rb",
34
+ "lib/abingo/statistics.rb",
35
+ "lib/abingo/views/dashboard/_experiment.erb",
36
+ "lib/abingo/views/dashboard/index.erb",
37
+ "lib/abingo_sugar.rb",
38
+ "lib/abingo_view_helper.rb",
39
+ "lib/generators/abingo_migration/USAGE",
40
+ "lib/generators/abingo_migration/abingo_migration_generator.rb",
41
+ "lib/generators/abingo_migration/templates/create_abingo_tables.rb",
42
+ "lib/generators/abingo_views/USAGE",
43
+ "lib/generators/abingo_views/abingo_views_generator.rb",
44
+ "lib/generators/abingo_views/templates/views/dashboard/_experiment.html.erb",
45
+ "lib/generators/abingo_views/templates/views/dashboard/index.html.erb",
46
+ "strip.rb",
47
+ "tasks/abingo_tasks.rake",
48
+ "test/abingo_test.rb",
49
+ "test/test_helper.rb",
50
+ "uninstall.rb"
51
+ ]
52
+ s.homepage = %q{http://github.com/wildfalcon/abingo}
53
+ s.rdoc_options = ["--charset=UTF-8"]
54
+ s.require_paths = ["lib"]
55
+ s.rubygems_version = %q{1.3.7}
56
+ s.summary = %q{A/B Testing for Rails}
57
+ s.test_files = [
58
+ "test/abingo_test.rb",
59
+ "test/test_helper.rb"
60
+ ]
61
+
62
+ if s.respond_to? :specification_version then
63
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
64
+ s.specification_version = 3
65
+
66
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
67
+ else
68
+ end
69
+ else
70
+ end
71
+ end
72
+
data/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/lib/abingo'
2
+
3
+ ActionController::Base.send :include, AbingoSugar
4
+
5
+ ActionView::Base.send :include, AbingoViewHelper
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,235 @@
1
+ #This class is outside code's main interface into the ABingo A/B testing framework.
2
+ #Unless you're fiddling with implementation details, it is the only one you need worry about.
3
+
4
+ #Usage of ABingo, including practical hints, is covered at http://www.bingocardcreator.com/abingo
5
+ require 'abingo/railtie'
6
+ require 'abingo_sugar'
7
+ require 'abingo_view_helper'
8
+
9
+ class Abingo
10
+
11
+ @@VERSION = "1.0.0"
12
+ @@MAJOR_VERSION = "1.0"
13
+ cattr_reader :VERSION
14
+ cattr_reader :MAJOR_VERSION
15
+
16
+ #Not strictly necessary, but eh, as long as I'm here.
17
+ cattr_accessor :salt
18
+ @@salt = "Not really necessary."
19
+
20
+ @@options ||= {}
21
+ cattr_accessor :options
22
+
23
+ #Defined options:
24
+ # :enable_specification => if true, allow params[test_name] to override the calculated value for a test.
25
+
26
+ #ABingo stores whether a particular user has participated in a particular
27
+ #experiment yet, and if so whether they converted, in the cache.
28
+ #
29
+ #It is STRONGLY recommended that you use a MemcacheStore for this.
30
+ #If you'd like to persist this through a system restart or the like, you can
31
+ #look into memcachedb, which speaks the memcached protocol. From the perspective
32
+ #of Rails it is just another MemcachedStore.
33
+ #
34
+ #You can overwrite Abingo's cache instance, if you would like it to not share
35
+ #your generic Rails cache.
36
+ cattr_writer :cache
37
+
38
+ def self.cache
39
+ @@cache || Rails.cache
40
+ end
41
+
42
+ #This method gives a unique identity to a user. It can be absolutely anything
43
+ #you want, as long as it is consistent.
44
+ #
45
+ #We use the identity to determine, deterministically, which alternative a user sees.
46
+ #This means that if you use Abingo.identify_user on someone at login, they will
47
+ #always see the same alternative for a particular test which is past the login
48
+ #screen. For details and usage notes, see the docs.
49
+ def self.identity=(new_identity)
50
+ @@identity = new_identity.to_s
51
+ end
52
+
53
+ def self.identity
54
+ @@identity ||= rand(10 ** 10).to_i.to_s
55
+ end
56
+
57
+ #A simple convenience method for doing an A/B test. Returns true or false.
58
+ #If you pass it a block, it will bind the choice to the variable given to the block.
59
+ def self.flip(test_name)
60
+ if block_given?
61
+ yield(self.test(test_name, [true, false]))
62
+ else
63
+ self.test(test_name, [true, false])
64
+ end
65
+ end
66
+
67
+ #This is the meat of A/Bingo.
68
+ #options accepts
69
+ # :multiple_participation (true or false)
70
+ # :conversion name of conversion to listen for (alias: conversion_name)
71
+ def self.test(test_name, alternatives, options = {})
72
+
73
+ short_circuit = Abingo.cache.read("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_"))
74
+ unless short_circuit.nil?
75
+ return short_circuit #Test has been stopped, pick canonical alternative.
76
+ end
77
+
78
+ unless Abingo::Experiment.exists?(test_name)
79
+ lock_key = test_name.gsub(" ", "_")
80
+ if Abingo.cache.exist?(lock_key)
81
+ while Abingo.cache.exist?(lock_key)
82
+ sleep(0.1)
83
+ end
84
+ break
85
+ end
86
+ Abingo.cache.write(lock_key, 1, :expires_in => 5.seconds)
87
+ conversion_name = options[:conversion] || options[:conversion_name]
88
+ Abingo::Experiment.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
89
+ Abingo.cache.delete(lock_key)
90
+ end
91
+
92
+ choice = self.find_alternative_for_user(test_name, alternatives)
93
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
94
+
95
+ #Set this user to participate in this experiment, and increment participants count.
96
+ if options[:multiple_participation] || !(participating_tests.include?(test_name))
97
+ unless participating_tests.include?(test_name)
98
+ participating_tests = participating_tests + [test_name]
99
+ Abingo.cache.write("Abingo::participating_tests::#{Abingo.identity}", participating_tests)
100
+ end
101
+ Abingo::Alternative.score_participation(test_name)
102
+ end
103
+
104
+ if block_given?
105
+ yield(choice)
106
+ else
107
+ choice
108
+ end
109
+ end
110
+
111
+
112
+ #Scores conversions for tests.
113
+ #test_name_or_array supports three types of input:
114
+ #
115
+ #A conversion name: scores a conversion for any test the user is participating in which
116
+ # is listening to the specified conversion.
117
+ #
118
+ #A test name: scores a conversion for the named test if the user is participating in it.
119
+ #
120
+ #An array of either of the above: for each element of the array, process as above.
121
+ #
122
+ #nil: score a conversion for every test the u
123
+ def Abingo.bingo!(name = nil, options = {})
124
+ if name.kind_of? Array
125
+ name.map do |single_test|
126
+ self.bingo!(single_test, options)
127
+ end
128
+ else
129
+ if name.nil?
130
+ #Score all participating tests
131
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
132
+ participating_tests.each do |participating_test|
133
+ self.bingo!(participating_test, options)
134
+ end
135
+ else #Could be a test name or conversion name.
136
+ conversion_name = name.gsub(" ", "_")
137
+ tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}")
138
+ if tests_listening_to_conversion
139
+ if tests_listening_to_conversion.size > 1
140
+ tests_listening_to_conversion.map do |individual_test|
141
+ self.score_conversion!(individual_test.to_s)
142
+ end
143
+ elsif tests_listening_to_conversion.size == 1
144
+ test_name_str = tests_listening_to_conversion.first.to_s
145
+ self.score_conversion!(test_name_str)
146
+ end
147
+ else
148
+ #No tests listening for this conversion. Assume it is just a test name.
149
+ test_name_str = name.to_s
150
+ self.score_conversion!(test_name_str)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ protected
157
+
158
+ #For programmer convenience, we allow you to specify what the alternatives for
159
+ #an experiment are in a few ways. Thus, we need to actually be able to handle
160
+ #all of them. We fire this parser very infrequently (once per test, typically)
161
+ #so it can be as complicated as we want.
162
+ # Integer => a number 1 through N
163
+ # Range => a number within the range
164
+ # Array => an element of the array.
165
+ # Hash => assumes a hash of something to int. We pick one of the
166
+ # somethings, weighted accorded to the ints provided. e.g.
167
+ # {:a => 2, :b => 3} produces :a 40% of the time, :b 60%.
168
+ #
169
+ #Alternatives are always represented internally as an array.
170
+ def self.parse_alternatives(alternatives)
171
+ if alternatives.kind_of? Array
172
+ return alternatives
173
+ elsif alternatives.kind_of? Integer
174
+ return (1..alternatives).to_a
175
+ elsif alternatives.kind_of? Range
176
+ return alternatives.to_a
177
+ elsif alternatives.kind_of? Hash
178
+ alternatives_array = []
179
+ alternatives.each do |key, value|
180
+ if value.kind_of? Integer
181
+ alternatives_array += [key] * value
182
+ else
183
+ raise "You gave a hash with #{key} => #{value} as an element. The value must be an integral weight."
184
+ end
185
+ end
186
+ return alternatives_array
187
+ else
188
+ raise "I don't know how to turn [#{alternatives}] into an array of alternatives."
189
+ end
190
+ end
191
+
192
+ def self.retrieve_alternatives(test_name, alternatives)
193
+ cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
194
+ alternative_array = self.cache.fetch(cache_key) do
195
+ self.parse_alternatives(alternatives)
196
+ end
197
+ alternative_array
198
+ end
199
+
200
+ def self.find_alternative_for_user(test_name, alternatives)
201
+ alternatives_array = retrieve_alternatives(test_name, alternatives)
202
+ alternatives_array[self.modulo_choice(test_name, alternatives_array.size)]
203
+ end
204
+
205
+ #Quickly determines what alternative to show a given user. Given a test name
206
+ #and their identity, we hash them together (which, for MD5, provably introduces
207
+ #enough entropy that we don't care) otherwise
208
+ def self.modulo_choice(test_name, choices_count)
209
+ Digest::MD5.hexdigest(Abingo.salt.to_s + test_name + self.identity.to_s).to_i(16) % choices_count
210
+ end
211
+
212
+ def self.score_conversion!(test_name)
213
+ test_name.gsub!(" ", "_")
214
+ participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
215
+ if options[:assume_participation] || participating_tests.include?(test_name)
216
+ cache_key = "Abingo::conversions(#{Abingo.identity},#{test_name}"
217
+ if options[:multiple_conversions] || !Abingo.cache.read(cache_key)
218
+ Abingo::Alternative.score_conversion(test_name)
219
+ if Abingo.cache.exist?(cache_key)
220
+ Abingo.cache.increment(cache_key)
221
+ else
222
+ Abingo.cache.write(cache_key, 1)
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ end
229
+
230
+ require 'abingo/statistics'
231
+ require 'abingo/conversion_rate'
232
+ require 'abingo/alternative'
233
+ require 'abingo/experiment'
234
+ require 'abingo/controller/dashboard'
235
+ # require 'abingo/experiment'