abingo_port 0.1.0

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.
@@ -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'