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