class Abongo

This module exists entirely to save finger strain for programmers. It is designed to be included in your ApplicationController.

See abongo.rb for descriptions of what these do.

Gives you easy syntax to use ABongo in your views.

Constants

MAJOR_VERSION
VERSION

Public Class Methods

bongo!(name = nil, options = {}) click to toggle source
# File lib/abongo.rb, line 84
def self.bongo!(name = nil, options = {})
  if name.kind_of? Array
    name.map do |single_test|
      self.bongo!(single_test, options)
    end
  else
    if name.nil?
      # Score all participating tests
      participant = Abongo.find_participant(Abongo.identity)
      participating_tests = participant['tests']
      participating_tests.each do |participating_test|
        self.bongo!(participating_test, options)
      end
    else # Could be a test name or conversion name
      tests_listening_to_conversion = Abongo.tests_listening_to_conversion(name)
      if tests_listening_to_conversion
        tests_listening_to_conversion.each do |test|
          self.score_conversion!(test)
        end
      else # No tests listening for this conversion. Assume it is just a test name
        if name.kind_of? BSON::ObjectId
          self.score_conversion!(name)
        else
          self.score_conversion!(name.to_s)
        end
      end
    end
  end
end
db() click to toggle source
# File lib/abongo.rb, line 21
def self.db; @@db; end
db=(db) click to toggle source
# File lib/abongo.rb, line 22
def self.db=(db)
  @@db = db
  @@experiments = db['abongo_experiments']
  @@conversions = db['abongo_conversions']
  @@participants = db['abongo_participants']
  @@alternatives = db['abongo_alternatives']
end
end_experiment!(test_name, final_alternative, conversion_name = nil) click to toggle source
# File lib/abongo.rb, line 202
def self.end_experiment!(test_name, final_alternative, conversion_name = nil)
  warn("conversion_name is deprecated") if conversion_name
  test = get_test(test_name)
  Abongo.experiments.update({:name => test_name}, {'$set' => { :final => final_alternative}}, :upsert => true, :safe => true)
  Abongo.conversions.update({'tests' => test['_id']}, {'$pull' => {'tests' => test['_id']}}, :multi => true)
end
expires_in(known_human = false) click to toggle source
# File lib/abongo.rb, line 134
def self.expires_in(known_human = false)
  expires_in = nil
  if (@@options[:expires_in])
    expires_in = @@options[:expires_in]
  end
  if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !known_human)
    expires_in = @@options[:expires_in_for_bots]
  end
  expires_in
end
flip(test_name, options = {}) { |test(test_name, [true, false], options)| ... } click to toggle source
# File lib/abongo.rb, line 38
def self.flip(test_name, options = {})
  if block_given?
    yield(self.test(test_name, [true, false], options))
  else
    self.test(test_name, [true, false], options)
  end
end
human!(identity = nil) click to toggle source
# File lib/abongo.rb, line 168
def self.human!(identity = nil)
  identity ||= Abongo.identity
  begin
    previous = Abongo.participants.find_and_modify({'query' => {:identity => identity}, 'update' => {'$set' => {:human => true}}, 'upsert' => true})
  rescue Mongo::OperationFailure
    Abongo.participants.update({:identity => identity}, {'$set' => {:human => true}}, :upsert => true)
    previous = Abongo.participants.find_one(:identity => identity)
  end
  
  if !previous['human'] and options[:count_humans_only]
    if options[:expires_in_for_bots] and previous['tests']
      Abongo.set_expiration(Abongo.identity, expires_in(true))
    end
    
    if previous['tests']
      previous['tests'].each do |test_id|
        test = Abongo.experiments.find_one(test_id)
        choice = Abongo.find_alternative_for_user(identity, test)
        Abongo.alternatives.update({:content => choice, :test => test_id}, {:$inc => {:participants => 1}})
        Abongo.experiments.update({:_id => test_id}, {'$inc' => {:participants => 1}})
      end
    end

    if previous['conversions']
      previous['conversions'].each do |test_id|
        test = Abongo.experiments.find_one(:_id => test_id)
        viewed_alternative = Abongo.find_alternative_for_user(identity, test)
        Abongo.alternatives.update({:content => viewed_alternative, :test => test_id}, {'$inc' => {:conversions => 1}})
        Abongo.experiments.update({:_id => test_id}, {'$inc' => {:conversions => 1}})
      end
    end
  end
end
identity() click to toggle source
# File lib/abongo.rb, line 34
def self.identity
  @@identity ||= rand(10 ** 10)
end
identity=(new_identity) click to toggle source
# File lib/abongo.rb, line 30
def self.identity=(new_identity)
  @@identity = new_identity.to_s
end
is_human?(identity = nil) click to toggle source
# File lib/abongo.rb, line 162
def self.is_human?(identity = nil)
  identity ||= Abongo.identity
  participant = Abongo.participants.find_one(:identity => identity)
  return !!(participant && participant["human"])
end
options() click to toggle source
# File lib/abongo.rb, line 14
def self.options; @@options; end
options=(options) click to toggle source
# File lib/abongo.rb, line 15
def self.options=(options); @@options = options; end
participating_tests(only_current = true, identity = nil) click to toggle source
# File lib/abongo.rb, line 146
def self.participating_tests(only_current = true, identity = nil)
  identity ||= Abongo.identity
  participating_tests = (Abongo.participants.find_one({:identity => identity}) || {} )['tests']
  return {} if participating_tests.nil?
  tests_and_alternatives = participating_tests.inject({}) do |acc, test_id|
    test = Abongo.experiments.find_one(test_id)
    if !only_current or (test['final'].nil? or !test['final'])
      alternative = Abongo.find_alternative_for_user(identity, test)
      acc[test['name']] = alternative
    end
    acc
  end

  tests_and_alternatives
end
salt() click to toggle source
# File lib/abongo.rb, line 18
def self.salt; @@salt; end
salt=(salt) click to toggle source
# File lib/abongo.rb, line 19
def self.salt=(salt); @@salt = salt; end
score_conversion!(test_name) click to toggle source
# File lib/abongo.rb, line 114
def self.score_conversion!(test_name)
  if test_name.kind_of? BSON::ObjectId
    participant = Abongo.find_participant(Abongo.identity)
    expired = participant['expires'] ? (participant['expires'] < Time.now) : false
    if options[:assume_participation] || participant['tests'].include?(test_name)
      if options[:multiple_conversions] || !participant['conversions'].include?(test_name) || expired
        Abongo.add_conversion(Abongo.identity, test_name)
        if !options[:count_humans_only] || participant['human']
          test = Abongo.experiments.find_one(:_id => test_name)
          viewed_alternative = Abongo.find_alternative_for_user(Abongo.identity, test)
          Abongo.alternatives.update({:content => viewed_alternative, :test => test['_id']}, {'$inc' => {:conversions => 1}})
          Abongo.experiments.update({:_id => test_name}, {'$inc' => {:conversions => 1}})
        end
      end
    end 
  else
    Abongo.score_conversion!(Abongo.get_test(test_name)['_id'])
  end
end
test(test_name, alternatives, options = {}) { |choice| ... } click to toggle source
# File lib/abongo.rb, line 46
def self.test(test_name, alternatives, options = {})
  # Test for short-circuit (the test has been ended)
  test = Abongo.get_test(test_name)
  return test['final'] unless test.nil? or test['final'].nil?

  # Create the test (if necessary)
  unless test
    conversion_name = options[:conversion] || options[:conversion_name]
    test = Abongo.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
  end

  # Should expired be part of the find_participant?
  participant = Abongo.find_participant(Abongo.identity)
  expired = participant['expires'] ? (participant['expires'] < Time.now) : false

  choice = self.find_alternative_for_user(Abongo.identity, test)
  participating_tests = participant['tests']

  # TODO: Pull participation add out
  if options[:multiple_participation] || !participating_tests.include?(test['_id']) || expired
    unless participating_tests.include?(test['_id'])
      Abongo.add_participation(identity, test['_id'], self.expires_in(participant['human']))
    end
    
    # Small timing issue in here
    if (!@@options[:count_humans_only] || participant['human'])
      Abongo.alternatives.update({:content => choice, :test => test['_id']}, {:$inc => {:participants => 1}})
      Abongo.experiments.update({:_id => test['_id']}, {'$inc' => {:participants => 1}})
    end
  end

  if block_given?
    yield(choice)
  else
    choice
  end
end

Protected Class Methods

add_conversion(identity, test_id) click to toggle source
# File lib/abongo.rb, line 291
def self.add_conversion(identity, test_id)
  Abongo.participants.update({:identity => identity}, {'$addToSet' => {:conversions => test_id}}, :upsert => true, :safe => true)
end
add_participation(identity, test_id, expires_in = nil) click to toggle source
# File lib/abongo.rb, line 295
def self.add_participation(identity, test_id, expires_in = nil)
  if expires_in.nil?
    Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}}, :upsert => true)
  else
    Abongo.participants.update({:identity => identity}, {'$addToSet' => {:tests => test_id}, '$set' => {:expires => Time.now + expires_in}}, :upsert => true)
  end
end
all_tests() click to toggle source
# File lib/abongo.rb, line 245
def self.all_tests
  Abongo.experiments.find.sort(:final).to_a
end
alternatives() click to toggle source
# File lib/abongo.rb, line 213
def self.alternatives; @@alternatives; end
conversions() click to toggle source
# File lib/abongo.rb, line 211
def self.conversions; @@conversions; end
experiments() click to toggle source
# File lib/abongo.rb, line 210
def self.experiments; @@experiments; end
find_alternative_for_user(identity, test) click to toggle source
# File lib/abongo.rb, line 215
def self.find_alternative_for_user(identity, test)
  test['alternatives'][self.modulo_choice(test['name'], test['alternatives'].size)]
end
find_participant(identity) click to toggle source
# File lib/abongo.rb, line 287
def self.find_participant(identity)
  {'identity' => identity, 'tests' => [], 'conversions' => [], 'human' => false}.merge(Abongo.participants.find_one({'identity' => identity})||{})
end
get_alternative(alternative_id) click to toggle source
# File lib/abongo.rb, line 257
def self.get_alternative(alternative_id)
  Abongo.alternatives.find_one({:_id => BSON::ObjectId(alternative_id)})
end
get_alternatives(test_id) click to toggle source
# File lib/abongo.rb, line 253
def self.get_alternatives(test_id)
  Abongo.alternatives.find({:test => test_id})
end
get_test(test) click to toggle source
# File lib/abongo.rb, line 249
def self.get_test(test)
  Abongo.experiments.find_one({:name => test}) || Abongo.experiments.find_one({:_id => test}) || nil
end
modulo_choice(test_name, choices_count) click to toggle source
# File lib/abongo.rb, line 219
def self.modulo_choice(test_name, choices_count)
  Digest::MD5.hexdigest(Abongo.salt.to_s + test_name + Abongo.identity.to_s).to_i(16) % choices_count
end
parse_alternatives(alternatives) click to toggle source
# File lib/abongo.rb, line 223
def self.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
participants() click to toggle source
# File lib/abongo.rb, line 212
def self.participants; @@participants; end
set_expiration(identity, expires_in) click to toggle source
# File lib/abongo.rb, line 303
def self.set_expiration(identity, expires_in)
  Abongo.participants.update({:identity => identity}, {'$set' => {:expires => Time.now + expires_in}}, :upsert => true)
end
start_experiment!(test_name, alternatives_array, conversion_name = nil) click to toggle source
# File lib/abongo.rb, line 267
def self.start_experiment!(test_name, alternatives_array, conversion_name = nil)
  conversion_name ||= test_name

  Abongo.experiments.update({:name => test_name}, {:$set => {:alternatives => alternatives_array}, :$inc => {:participants => 0, :conversions => 0}}, :upsert => true, :safe => true)
  test = Abongo.experiments.find_one({:name => test_name})

  # This could be a lot more elegant
  cloned_alternatives_array = alternatives_array.clone
  while (cloned_alternatives_array.size > 0)
    alt = cloned_alternatives_array[0]
    weight = cloned_alternatives_array.size - (cloned_alternatives_array - [alt]).size
    Abongo.alternatives.update({:test => test['_id'], :content => alt}, {'$set' => {:weight => weight}, '$inc' => {:participants => 0, :conversions => 0}}, :upsert => true, :safe => true)
    cloned_alternatives_array -= [alt]
  end

  Abongo.conversions.update({'name' => conversion_name}, {'$addToSet' => { 'tests' => test['_id'] }}, :upsert => true, :safe => true)

  test
end
tests_listening_to_conversion(conversion) click to toggle source
# File lib/abongo.rb, line 261
def self.tests_listening_to_conversion(conversion)
  conversions = Abongo.conversions.find_one({:name => conversion})
  return nil unless conversions
  conversions['tests']
end