Class: Optimizely::Bucketer

Inherits:
Object
  • Object
show all
Defined in:
lib/optimizely/bucketer.rb

Constant Summary collapse

BUCKETING_ID_TEMPLATE =

Optimizely bucketing algorithm that evenly distributes visitors.

'%<bucketing_id>s%<entity_id>s'
HASH_SEED =
1
MAX_HASH_VALUE =
2**32
MAX_TRAFFIC_VALUE =
10_000
UNSIGNED_MAX_32_BIT_VALUE =
0xFFFFFFFF

Instance Method Summary collapse

Constructor Details

#initialize(logger) ⇒ Bucketer

Returns a new instance of Bucketer.



31
32
33
34
35
36
# 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

Instance Method Details

#bucket(project_config, experiment, bucketing_id, user_id) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# 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) ⇒ Object



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
# 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 an 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 decision 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