Class: Abingo

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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

#identityObject

Returns the value of attribute identity


23
24
25
# File 'lib/abingo.rb', line 23

def identity
  @identity
end

Class Method Details

.cacheObject

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_identityObject


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

Raises:

  • (RuntimeError)

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, options = {})
  if name.kind_of? Array
    name.map do |single_test|
      self.bingo!(single_test, options)
    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, options)
      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

Returns:

  • (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, options = {})

  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 = options[:conversion] || options[: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 options[: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

#wait_for_lock_release(lock_key) ⇒ Object


142
143
144
145
146
# File 'lib/abingo.rb', line 142

def wait_for_lock_release(lock_key)
  while Abingo.cache.exist?(lock_key)
    sleep(0.1)
  end
end