class EasyAb::Experiment
Attributes
name[R]
rules[R]
scope[R]
variants[R]
weights[R]
winner[R]
Public Class Methods
find_by_name!(experiment_name)
click to toggle source
# File lib/easy_ab/experiment.rb, line 21 def self.find_by_name!(experiment_name) experiment_name = experiment_name.to_s exp = EasyAb.experiments.all.find { |exp| exp.name == experiment_name } raise ExperimentNotFound if exp.nil? exp end
new(name, options = {})
click to toggle source
# File lib/easy_ab/experiment.rb, line 5 def initialize(name, options = {}) @name = name.to_s @variants = options[:variants].map(&:to_s) @weights = options[:weights] @rules = options[:rules] @scope = options[:scope] @winner = options[:winner].nil? ? nil : options[:winner].to_s raise ArgumentError, 'Please define variants' if @variants.blank? raise ArgumentError, 'Number of variants and weights should be identical' if @weights.present? && @weights.size != @variants.size raise ArgumentError, 'Number of variants and rules should be identical' if @rules.present? && @rules.size != @variants.size raise ArgumentError, 'All rules should be a Proc' if @rules.present? && @rules.any? { |rule| !rule.is_a?(Proc) } raise ArgumentError, 'Scope should be a Proc' if @scope.present? && !@scope.is_a?(Proc) raise ArgumentError, 'winner should be one of variants' if @winner && !@variants.include?(@winner) end
Public Instance Methods
assign_variant(user_recognition, options = {})
click to toggle source
Priority:
-
winner
-
url parameter or assign variant (ex: ab_test(:experiment, variant: 'variant A'))
-
scope
-
rules/weights
# File lib/easy_ab/experiment.rb, line 33 def assign_variant(user_recognition, options = {}) # 1. winner return winner if winner grouping = find_grouping_by_user_recognition(user_recognition) || ::EasyAb::Grouping.new(experiment: name, user_id: user_recognition[:user_id], cookie: user_recognition[:cookie]) # 2. url parameter or assign variant if options[:variant] && variants.include?(options[:variant].to_s) grouping.variant = options[:variant].to_s else # 3. scope return nil if options[:scope] && !options[:scope].call # 4. rules/weights grouping.variant ||= flexible_variant(options[:contexted_rules]) return nil if grouping.variant.nil? end if grouping.changed? && !options[:skip_save] begin grouping.save! rescue ActiveRecord::RecordNotUnique grouping = find_grouping_by_user_recognition(user_recognition) rescue ActiveRecord::RecordInvalid => e if grouping.errors[:user_id].present? || grouping.errors[:cookie].present? grouping = find_grouping_by_user_recognition(user_recognition) else raise e end end end grouping.variant end
equal_weighted_variant()
click to toggle source
# File lib/easy_ab/experiment.rb, line 132 def equal_weighted_variant variants.sample end
find_grouping_by_user_recognition(user_recognition)
click to toggle source
TODO: add spec
# File lib/easy_ab/experiment.rb, line 68 def find_grouping_by_user_recognition(user_recognition) user_id = user_recognition[:user_id].presence cookie = user_recognition[:cookie].presence raise 'User not found: both user_id and cookie are empty' if user_id.nil? && cookie.nil? # Cases should take into consideration # Case I: user participated experiment with login and return again # Case II: user participated experiment with login and return by another device with login # Case III: user participated experiment with login and return by the same device, but cookie was cleared between last and this participation # => Both II and III already exist a record with the same user_id but different cookie # In the above two cases, we update the cookie of the exising record # # Case IV: User participated experiment without login and return with login # => Assign user_id to the existing record # # Case V: User participated experiment without login and return without login, too grouping = nil if user_id # User is signed in # Case I, II, III return grouping if (grouping = groupings.where(user_id: user_id).first) && ((cookie && grouping.cookie = cookie) || true) # Case IV return grouping if (cookie && grouping = groupings.where(user_id: nil, cookie: cookie).first) && ((grouping.user_id = user_id) || true) # Assign user_id elsif cookie # User is not signed in # Case V return grouping if grouping = groupings.where(cookie: cookie).first end grouping end
flexible_variant(contexted_rules = nil)
click to toggle source
# File lib/easy_ab/experiment.rb, line 102 def flexible_variant(contexted_rules = nil) if contexted_rules variant_by_rule(contexted_rules) elsif weights weighted_variant else equal_weighted_variant end end
groupings()
click to toggle source
# File lib/easy_ab/experiment.rb, line 98 def groupings ::EasyAb::Grouping.where(experiment: name) end
variant_by_rule(contexted_rules)
click to toggle source
# File lib/easy_ab/experiment.rb, line 112 def variant_by_rule(contexted_rules) contexted_rules.each_with_index do |rule, i| return variants[i] if rule.call end # If all rules not matched, return nil nil end
weighted_variant()
click to toggle source
# File lib/easy_ab/experiment.rb, line 121 def weighted_variant total = weights.sum roll = rand sum = 0 weights.each_with_index do |weight, i| sum += weight.to_d / total return variants[i] if sum >= roll end variants.last end