abingo_port 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README +160 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/abingo.gemspec +72 -0
- data/init.rb +5 -0
- data/install.rb +1 -0
- data/lib/abingo.rb +235 -0
- data/lib/abingo/alternative.rb +23 -0
- data/lib/abingo/controller/dashboard.rb +25 -0
- data/lib/abingo/conversion_rate.rb +9 -0
- data/lib/abingo/experiment.rb +99 -0
- data/lib/abingo/rails/controller/dashboard.rb +13 -0
- data/lib/abingo/railtie.rb +12 -0
- data/lib/abingo/statistics.rb +90 -0
- data/lib/abingo/views/dashboard/_experiment.erb +43 -0
- data/lib/abingo/views/dashboard/index.erb +20 -0
- data/lib/abingo_sugar.rb +28 -0
- data/lib/abingo_view_helper.rb +42 -0
- data/lib/generators/abingo_migration/USAGE +8 -0
- data/lib/generators/abingo_migration/abingo_migration_generator.rb +13 -0
- data/lib/generators/abingo_migration/templates/create_abingo_tables.rb +29 -0
- data/lib/generators/abingo_views/USAGE +8 -0
- data/lib/generators/abingo_views/abingo_views_generator.rb +9 -0
- data/lib/generators/abingo_views/templates/views/dashboard/_experiment.html.erb +43 -0
- data/lib/generators/abingo_views/templates/views/dashboard/index.html.erb +20 -0
- data/strip.rb +11 -0
- data/tasks/abingo_tasks.rake +4 -0
- data/test/abingo_test.rb +135 -0
- data/test/test_helper.rb +3 -0
- data/uninstall.rb +1 -0
- metadata +99 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.svn
|
data/MIT-LICENSE
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/abingo.gemspec
ADDED
@@ -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
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/abingo.rb
ADDED
@@ -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'
|