Class: Sinatra::AbingoObject
- Inherits:
-
Object
- Object
- Sinatra::AbingoObject
- Defined in:
- lib/sinmetrics/abingo.rb
Defined Under Namespace
Modules: ConversionRate, Statistics Classes: Alternative, Experiment
Instance Attribute Summary collapse
-
#app ⇒ Object
readonly
Returns the value of attribute app.
-
#cache ⇒ Object
readonly
Returns the value of attribute cache.
-
#enable_specification ⇒ Object
readonly
Returns the value of attribute enable_specification.
-
#identity ⇒ Object
Returns the value of attribute identity.
-
#salt ⇒ Object
readonly
Returns the value of attribute salt.
Instance Method Summary collapse
- #alternatives_for_test(test_name) ⇒ Object
-
#bingo!(name = nil, options = {}) ⇒ Object
Scores conversions for tests.
- #calculate_alternative_lookup(test_name, alternative_name) ⇒ Object
- #end_experiment(alternative_id) ⇒ Object
- #find_alternative_for_user(test_name, alternatives) ⇒ Object
-
#flip(test_name) ⇒ Object
A simple convenience method for doing an A/B test.
-
#initialize(app = {}) ⇒ AbingoObject
constructor
Defined options: :enable_specification => if true, allow params to override the calculated value for a test.
-
#parse_alternatives(alternatives) ⇒ Object
For programmer convenience, we allow you to specify what the alternatives for an experiment are in a few ways.
- #score_conversion!(test_name, options = {}) ⇒ Object
- #score_participation!(test_name, options = {}) ⇒ Object
-
#test(test_name, alternatives, options = {}) ⇒ Object
This is the meat of A/Bingo.
Constructor Details
#initialize(app = {}) ⇒ AbingoObject
Defined options:
:enable_specification => if true, allow params[test_name] to override the calculated value for a test.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/sinmetrics/abingo.rb', line 20 def initialize app={} if app.respond_to?(:options) @app = app [:identity, :enable_specification, :cache, :salt].each do |var| begin instance_variable_set("@#{var}", app..send("abingo_#{var}")) rescue NoMethodError end end else [:identity, :enable_specification, :cache, :salt].each do |var| instance_variable_set("@#{var}", app[var]) if app.has_key?(var) end end @identity ||= rand(10 ** 10).to_i.to_s @cache ||= ActiveSupport::Cache::MemoryStore.new @salt ||= '' end |
Instance Attribute Details
#app ⇒ Object (readonly)
Returns the value of attribute app.
39 40 41 |
# File 'lib/sinmetrics/abingo.rb', line 39 def app @app end |
#cache ⇒ Object (readonly)
Returns the value of attribute cache.
39 40 41 |
# File 'lib/sinmetrics/abingo.rb', line 39 def cache @cache end |
#enable_specification ⇒ Object (readonly)
Returns the value of attribute enable_specification.
39 40 41 |
# File 'lib/sinmetrics/abingo.rb', line 39 def enable_specification @enable_specification end |
#identity ⇒ Object
Returns the value of attribute identity.
40 41 42 |
# File 'lib/sinmetrics/abingo.rb', line 40 def identity @identity end |
#salt ⇒ Object (readonly)
Returns the value of attribute salt.
39 40 41 |
# File 'lib/sinmetrics/abingo.rb', line 39 def salt @salt end |
Instance Method Details
#alternatives_for_test(test_name) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/sinmetrics/abingo.rb', line 151 def alternatives_for_test(test_name) cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_") self.cache.fetch(cache_key) do Experiment.get(test_name).alternatives.map do |alt| if alt.weight > 1 [alt.content] * alt.weight else alt.content end end.flatten end end |
#bingo!(name = nil, options = {}) ⇒ Object
Scores conversions for tests. test_name_or_array supports three types of input:
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
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/sinmetrics/abingo.rb', line 84 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 = cache.read("Abingo::participating_tests::#{identity}") || [] participating_tests.each do |participating_test| self.bingo!(participating_test, ) end else self.score_conversion!(name.to_s, ) end end end |
#calculate_alternative_lookup(test_name, alternative_name) ⇒ Object
206 207 208 |
# File 'lib/sinmetrics/abingo.rb', line 206 def calculate_alternative_lookup(test_name, alternative_name) Digest::MD5.hexdigest(self.salt + test_name + alternative_name.to_s) end |
#end_experiment(alternative_id) ⇒ Object
198 199 200 201 202 203 204 |
# File 'lib/sinmetrics/abingo.rb', line 198 def end_experiment(alternative_id) alternative = Alternative.get(alternative_id) experiment = alternative.experiment if (experiment.status != "Completed") experiment.end_experiment!(self, alternative.content) end end |
#find_alternative_for_user(test_name, alternatives) ⇒ Object
138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/sinmetrics/abingo.rb', line 138 def find_alternative_for_user(test_name, alternatives) cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_") alternative_array = self.cache.fetch(cache_key) do self.parse_alternatives(alternatives) end #Quickly determines what alternative to show a given user. Given a test name #and their identity, we hash them together (which, for MD5, provably introduces #enough entropy that we don't care) otherwise choice = Digest::MD5.hexdigest(salt.to_s + test_name + self.identity.to_s).to_i(16) % alternative_array.size alternative_array[choice] 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.
44 45 46 47 48 49 50 51 |
# File 'lib/sinmetrics/abingo.rb', line 44 def flip(test_name) choice = test(test_name, [true, false]) if block_given? yield choice else choice end end |
#parse_alternatives(alternatives) ⇒ Object
For programmer convenience, we allow you to specify what the alternatives for an experiment are in a few ways. Thus, we need to actually be able to handle all of them. We fire this parser very infrequently (once per test, typically) so it can be as complicated as we want.
Integer => a number 1 through N
Range => a number within the range
Array => an element of the array.
Hash => assumes a hash of something to int. We pick one of the
somethings, weighted accorded to the ints provided. e.g.
{:a => 2, :b => 3} produces :a 40% of the time, :b 60%.
Alternatives are always represented internally as an array.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/sinmetrics/abingo.rb', line 116 def parse_alternatives(alternatives) if alternatives.kind_of? Array return alternatives elsif alternatives.kind_of? Integer return (1..alternatives).to_a elsif alternatives.kind_of? Range return alternatives.to_a elsif alternatives.kind_of? Hash alternatives_array = [] alternatives.each do |key, value| if value.kind_of? Integer alternatives_array += [key] * value else raise "You gave a hash with #{key} => #{value} as an element. The value must be an integral weight." end end return alternatives_array else raise "I don't know how to turn [#{alternatives}] into an array of alternatives." end end |
#score_conversion!(test_name, options = {}) ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/sinmetrics/abingo.rb', line 164 def score_conversion!(test_name, = {}) participating_tests = cache.read("Abingo::participating_tests::#{identity}") || [] if [:assume_participation] || participating_tests.include?(test_name) cache_key = "Abingo::conversions(#{identity},#{test_name}" if [:multiple_conversions] || !cache.read(cache_key) viewed_alternative = find_alternative_for_user(test_name, alternatives_for_test(test_name)) lookup = calculate_alternative_lookup(test_name, viewed_alternative) Alternative.all(:lookup => lookup).adjust!(:conversions => 1) if cache.exist?(cache_key) cache.increment(cache_key) else cache.write(cache_key, 1) end end end end |
#score_participation!(test_name, options = {}) ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/sinmetrics/abingo.rb', line 181 def score_participation!(test_name, = {}) participating_tests = cache.read("Abingo::participating_tests::#{identity}") || [] #Set this user to participate in this experiment, and increment participants count. if [:multiple_participation] || !(participating_tests.include?(test_name)) participating_tests = participating_tests.dup if participating_tests.frozen? unless participating_tests.include?(test_name) participating_tests << test_name cache.write("Abingo::participating_tests::#{identity}", participating_tests) end choice = find_alternative_for_user(test_name, alternatives_for_test(test_name)) lookup = calculate_alternative_lookup(test_name, choice) Alternative.all(:lookup => lookup).adjust!(:participants => 1) end end |
#test(test_name, alternatives, options = {}) ⇒ Object
This is the meat of A/Bingo. options accepts
:multiple_participation (true or false)
:no_particiation (true or false)
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/sinmetrics/abingo.rb', line 57 def test(test_name, alternatives, = {}) experiment = Experiment.get(test_name) experiment ||= Experiment.start_experiment!(self, test_name, parse_alternatives(alternatives)) # Test has been stopped, pick canonical alternative. unless experiment.short_circuit.nil? return experiment.short_circuit end choice = find_alternative_for_user(test_name, alternatives) score_participation!(test_name, ) unless [:no_participation] if block_given? yield(choice) else choice end end |