Class: Abingo
- Inherits:
-
Object
- Object
- Abingo
- Defined in:
- lib/abingo.rb,
lib/abingo/version.rb,
lib/abingo/controller/dashboard.rb,
lib/abingo/rails/controller/dashboard.rb
Overview
Usage of ABingo, including practical hints, is covered at www.bingocardcreator.com/abingo
Defined Under Namespace
Modules: Controller, ConversionRate, Rails, Statistics Classes: Alternative, Experiment
Constant Summary collapse
- VERSION =
"2.0.2"
- @@salt =
"Not really necessary."
Instance Attribute Summary collapse
-
#identity ⇒ Object
Returns the value of attribute identity.
Class Method Summary collapse
-
.cache ⇒ Object
ABingo stores whether a particular user has participated in a particular experiment yet, and if so whether they converted, in the cache.
- .cache=(cache) ⇒ Object
- .generate_identity ⇒ Object
-
.identify(identity = nil) ⇒ Object
This method identifies a user and ensures they consistently see the same alternative.
- .identity=(new_identity) ⇒ Object
Instance Method Summary collapse
-
#bingo!(name = nil, options = {}) ⇒ Object
Scores conversions for tests.
-
#flip(test_name) ⇒ Object
A simple convenience method for doing an A/B test.
-
#human! ⇒ Object
Marks that this user is human.
-
#initialize(identity) ⇒ Abingo
constructor
A new instance of Abingo.
- #is_human? ⇒ Boolean
- #participating_tests(only_current = true) ⇒ Object
-
#test(test_name, alternatives, options = {}) ⇒ Object
This is the meat of A/Bingo.
- #wait_for_lock_release(lock_key) ⇒ Object
Constructor Details
#initialize(identity) ⇒ Abingo
Returns a new instance of Abingo.
71 72 73 74 |
# File 'lib/abingo.rb', line 71 def initialize(identity) @identity = identity super() end |
Instance Attribute Details
#identity ⇒ Object
Returns the value of attribute identity.
23 24 25 |
# File 'lib/abingo.rb', line 23 def identity @identity end |
Class Method Details
.cache ⇒ Object
ABingo stores whether a particular user has participated in a particular experiment yet, and if so whether they converted, in the cache.
It is STRONGLY recommended that you use a MemcacheStore for this. If you’d like to persist this through a system restart or the like, you can look into memcachedb, which speaks the memcached protocol. From the perspective of Rails it is just another MemcachedStore.
You can overwrite Abingo’s cache instance, if you would like it to not share your generic Rails cache.
46 47 48 |
# File 'lib/abingo.rb', line 46 def self.cache @cache || ::Rails.cache end |
.cache=(cache) ⇒ Object
50 51 52 |
# File 'lib/abingo.rb', line 50 def self.cache=(cache) @cache = cache end |
.generate_identity ⇒ Object
58 59 60 |
# File 'lib/abingo.rb', line 58 def self.generate_identity rand(10 ** 10).to_i.to_s end |
.identify(identity = nil) ⇒ Object
This method identifies a user and ensures they consistently see the same alternative. This means that if you use Abingo.identify on someone at login, they will always see the same alternative for a particular test which is past the login screen. For details and usage notes, see the docs.
66 67 68 69 |
# File 'lib/abingo.rb', line 66 def self.identify(identity = nil) identity ||= generate_identity new(identity) end |
.identity=(new_identity) ⇒ Object
54 55 56 |
# File 'lib/abingo.rb', line 54 def self.identity=(new_identity) raise RuntimeError.new("Setting identity on the class level has been deprecated. Please create an instance via: @abingo = Abingo.identify('user-id')") end |
Instance Method Details
#bingo!(name = nil, options = {}) ⇒ Object
Scores conversions for tests. test_name_or_array supports three types of input:
A conversion name: scores a conversion for any test the user is participating in which
is listening to the specified conversion.
A test name: scores a conversion for the named test if the user is participating in it.
An array of either of the above: for each element of the array, process as above.
nil: score a conversion for every test the u
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/abingo.rb', line 159 def bingo!(name = nil, = {}) if name.kind_of? Array name.map do |single_test| self.bingo!(single_test, ) end else if name.nil? #Score all participating tests participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || [] participating_tests.each do |participating_test| self.bingo!(participating_test, ) end else #Could be a test name or conversion name. conversion_name = name.gsub(" ", "_") tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}") if tests_listening_to_conversion if tests_listening_to_conversion.size > 1 tests_listening_to_conversion.map do |individual_test| self.score_conversion!(individual_test.to_s) end elsif tests_listening_to_conversion.size == 1 test_name_str = tests_listening_to_conversion.first.to_s self.score_conversion!(test_name_str) end else #No tests listening for this conversion. Assume it is just a test name. test_name_str = name.to_s self.score_conversion!(test_name_str) end end end end |
#flip(test_name) ⇒ Object
A simple convenience method for doing an A/B test. Returns true or false. If you pass it a block, it will bind the choice to the variable given to the block.
78 79 80 81 82 83 84 |
# File 'lib/abingo.rb', line 78 def flip(test_name) if block_given? yield(self.test(test_name, [true, false])) else self.test(test_name, [true, false]) end end |
#human! ⇒ Object
Marks that this user is human.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/abingo.rb', line 209 def human! Abingo.cache.fetch("Abingo::is_human(#{self.identity})", {:expires_in => self.expires_in(true)}) do #Now that we know the user is human, score participation for all their tests. (Further participation will *not* be lazy evaluated.) #Score all tests which have been deferred. participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || [] #Refresh cache expiry for this user to match that of known humans. if (@@options[:expires_in_for_bots] && !participating_tests.blank?) Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests, {:expires_in => self.expires_in(true)}) end participating_tests.each do |test_name| viewed_alternative = find_alternative_for_user(test_name, Abingo::Experiment.alternatives_for_test(test_name)) Alternative.score_participation(test_name, viewed_alternative) if conversions = Abingo.cache.read("Abingo::conversions(#{self.identity},#{test_name}") conversions.times { Alternative.score_conversion(test_name, viewed_alternative) } end end true #Marks this user as human in the cache. end end |
#is_human? ⇒ Boolean
233 234 235 |
# File 'lib/abingo.rb', line 233 def is_human? !!Abingo.cache.read("Abingo::is_human(#{self.identity})") end |
#participating_tests(only_current = true) ⇒ Object
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/abingo.rb', line 192 def participating_tests(only_current = true) participating_tests = Abingo.cache.read("Abingo::participating_tests::#{identity}") || [] tests_and_alternatives = participating_tests.inject({}) do |acc, test_name| alternatives_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_") alternatives = Abingo.cache.read(alternatives_key) acc[test_name] = find_alternative_for_user(test_name, alternatives) acc end if (only_current) tests_and_alternatives.reject! do |key, value| Abingo.cache.read("Abingo::Experiment::short_circuit(#{key})") end end tests_and_alternatives end |
#test(test_name, alternatives, options = {}) ⇒ Object
This is the meat of A/Bingo. options accepts
:multiple_participation (true or false)
:conversion name of conversion to listen for (alias: conversion_name)
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/abingo.rb', line 90 def test(test_name, alternatives, = {}) short_circuit = Abingo.cache.read("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_")) unless short_circuit.nil? return short_circuit #Test has been stopped, pick canonical alternative. end unless Abingo::Experiment.exists?(test_name) lock_key = "Abingo::lock_for_creation(#{test_name.gsub(" ", "_")})" lock_id = SecureRandom.hex #this prevents (most) repeated creations of experiments in high concurrency environments. if Abingo.cache.exist?(lock_key) wait_for_lock_release(lock_key) else Abingo.cache.write(lock_key, lock_id, :expires_in => 5.seconds) sleep(0.1) if Abingo.cache.read(lock_key) == lock_id conversion_name = [:conversion] || [:conversion_name] Abingo::Experiment.start_experiment!(test_name, Abingo.parse_alternatives(alternatives), conversion_name) else wait_for_lock_release(lock_key) end end Abingo.cache.delete(lock_key) end choice = self.find_alternative_for_user(test_name, alternatives) participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || [] #Set this user to participate in this experiment, and increment participants count. if [:multiple_participation] || !(participating_tests.include?(test_name)) unless participating_tests.include?(test_name) participating_tests = participating_tests + [test_name] if self.expires_in Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests, {:expires_in => self.expires_in}) else Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests) end end #If we're only counting known humans, then postpone scoring participation until after we know the user is human. if (!@@options[:count_humans_only] || self.is_human?) Abingo::Alternative.score_participation(test_name, choice) end end if block_given? yield(choice) else choice end end |