class Optimizely::Bucketer
Constants
- BUCKETING_ID_TEMPLATE
Optimizely
bucketing algorithm that evenly distributes visitors.- HASH_SEED
- MAX_HASH_VALUE
- MAX_TRAFFIC_VALUE
- UNSIGNED_MAX_32_BIT_VALUE
Public Class Methods
new(logger)
click to toggle source
# File lib/optimizely/bucketer.rb, line 31 def initialize(logger) # Bucketer init method to set bucketing seed and logger. # logger - Optional component which provides a log method to log messages. @logger = logger @bucket_seed = HASH_SEED end
Public Instance Methods
bucket(project_config, experiment, bucketing_id, user_id)
click to toggle source
# File lib/optimizely/bucketer.rb, line 38 def bucket(project_config, experiment, bucketing_id, user_id) # Determines ID of variation to be shown for a given experiment key and user ID. # # project_config - Instance of ProjectConfig # experiment - Experiment or Rollout rule for which visitor is to be bucketed. # bucketing_id - String A customer-assigned value used to generate the bucketing key # user_id - String ID for user. # # Returns variation in which visitor with ID user_id has been placed. Nil if no variation. return nil, [] if experiment.nil? decide_reasons = [] # check if experiment is in a group; if so, check if user is bucketed into specified experiment # this will not affect evaluation of rollout rules. experiment_id = experiment['id'] experiment_key = experiment['key'] group_id = experiment['groupId'] if group_id group = project_config.group_id_map.fetch(group_id) if Helpers::Group.random_policy?(group) traffic_allocations = group.fetch('trafficAllocation') bucketed_experiment_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, group_id, traffic_allocations) decide_reasons.push(*find_bucket_reasons) # return if the user is not bucketed into any experiment unless bucketed_experiment_id message = "User '#{user_id}' is in no experiment." @logger.log(Logger::INFO, message) decide_reasons.push(message) return nil, decide_reasons end # return if the user is bucketed into a different experiment than the one specified if bucketed_experiment_id != experiment_id message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}." @logger.log(Logger::INFO, message) decide_reasons.push(message) return nil, decide_reasons end # continue bucketing if the user is bucketed into the experiment specified message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}." @logger.log(Logger::INFO, message) decide_reasons.push(message) end end traffic_allocations = experiment['trafficAllocation'] variation_id, find_bucket_reasons = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations) decide_reasons.push(*find_bucket_reasons) if variation_id && variation_id != '' variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id) return variation, decide_reasons end # Handle the case when the traffic range is empty due to sticky bucketing if variation_id == '' message = 'Bucketed into an empty traffic range. Returning nil.' @logger.log(Logger::DEBUG, message) decide_reasons.push(message) end [nil, decide_reasons] end
find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
click to toggle source
# File lib/optimizely/bucketer.rb, line 105 def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations) # Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations. # # bucketing_id - String A customer-assigned value user to generate bucketing key # user_id - String ID for user # parent_id - String entity ID to use for bucketing ID # traffic_allocations - Array of traffic allocations # # Returns and array of two values where first value is the entity ID corresponding to the provided bucket value # or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken decide_reasons = [] bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id) bucket_value = generate_bucket_value(bucketing_key) message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'." @logger.log(Logger::DEBUG, message) traffic_allocations.each do |traffic_allocation| current_end_of_range = traffic_allocation['endOfRange'] if bucket_value < current_end_of_range entity_id = traffic_allocation['entityId'] return entity_id, decide_reasons end end [nil, decide_reasons] end
Private Instance Methods
generate_bucket_value(bucketing_key)
click to toggle source
# File lib/optimizely/bucketer.rb, line 135 def generate_bucket_value(bucketing_key) # Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE). # # bucketing_key - String - Value used to generate bucket value # # Returns bucket value corresponding to the provided bucketing key. ratio = generate_unsigned_hash_code_32_bit(bucketing_key).to_f / MAX_HASH_VALUE (ratio * MAX_TRAFFIC_VALUE).to_i end
generate_unsigned_hash_code_32_bit(bucketing_key)
click to toggle source
# File lib/optimizely/bucketer.rb, line 146 def generate_unsigned_hash_code_32_bit(bucketing_key) # Helper function to retreive hash code # # bucketing_key - String - Value used for the key of the murmur hash # # Returns hash code which is a 32 bit unsigned integer. MurmurHash3::V32.str_hash(bucketing_key, @bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE end