Skip to content

Commit ef897e2

Browse files
oakbanimikeproeng37
authored andcommitted
Fix Rollout Bucketer.bucket call & Decision Service logic (#71)
1 parent 986565a commit ef897e2

File tree

5 files changed

+185
-186
lines changed

5 files changed

+185
-186
lines changed

lib/optimizely/audience.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ def user_in_experiment?(config, experiment, attributes)
4141
# Return true if any one of the audience conditions are met
4242
@condition_evaluator = ConditionEvaluator.new(attributes)
4343
audience_ids.each do |audience_id|
44-
audience_conditions = config.get_audience_conditions_from_id(audience_id)
44+
audience = config.get_audience_from_id(audience_id)
45+
audience_conditions = audience['conditions']
4546
audience_conditions = JSON.load(audience_conditions)
4647
return true if @condition_evaluator.evaluate(audience_conditions)
4748
end

lib/optimizely/decision_service.rb

Lines changed: 95 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,7 @@ def get_variation(experiment_key, user_id, attributes = nil)
5151
# Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
5252

5353
# By default, the bucketing ID should be the user ID
54-
bucketing_id = user_id;
55-
56-
# If the bucketing ID key is defined in attributes, then use that in place of the userID
57-
if attributes and attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].is_a? String
58-
unless attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].empty?
59-
bucketing_id = attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]
60-
@config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
61-
end
62-
end
63-
54+
bucketing_id = get_bucketing_id(user_id, attributes)
6455
# Check to make sure experiment is active
6556
experiment = @config.get_experiment_from_key(experiment_key)
6657
if experiment.nil?
@@ -121,16 +112,14 @@ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
121112

122113
# check if the feature is being experiment on and whether the user is bucketed into the experiment
123114
decision = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
124-
unless decision.nil?
125-
return decision
126-
end
115+
return decision unless decision.nil?
127116

128117
feature_flag_key = feature_flag['key']
129118
variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
130119
if variation
131120
@config.logger.log(
132121
Logger::INFO,
133-
"User '#{user_id}' is in the rollout for feature flag '#{feature_flag_key}'."
122+
"User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
134123
)
135124
# return decision with nil experiment so we don't track impressions for it
136125
return {
@@ -140,11 +129,11 @@ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
140129
else
141130
@config.logger.log(
142131
Logger::INFO,
143-
"User '#{user_id}' is not in the rollout for feature flag '#{feature_flag_key}'."
132+
"User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
144133
)
145134
end
146135

147-
return nil
136+
nil
148137
end
149138

150139
def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
@@ -156,65 +145,43 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
156145
#
157146
# Returns a hash with the experiment and variation where visitor will be bucketed
158147
# or nil if the user is not bucketed into any of the experiments on the feature
159-
160148
feature_flag_key = feature_flag['key']
161-
unless feature_flag['experimentIds'].empty?
162-
# check if experiment is part of mutex group
163-
experiment_id = feature_flag['experimentIds'][0]
164-
experiment = @config.experiment_id_map[experiment_id]
165-
unless experiment
166-
@config.logger.log(
167-
Logger::DEBUG,
168-
"Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
169-
)
170-
return nil
171-
end
172-
173-
group_id = experiment['groupId']
174-
# if experiment is part of mutex group we first determine which experiment (if any) in the group the user is part of
175-
if group_id and @config.group_key_map.has_key?(group_id)
176-
group = @config.group_key_map[group_id]
177-
bucketed_experiment_id = @bucketer.find_bucket(user_id, group_id, group['trafficAllocation'])
178-
if bucketed_experiment_id.nil?
149+
if feature_flag['experimentIds'].empty?
150+
@config.logger.log(
151+
Logger::DEBUG,
152+
"The feature flag '#{feature_flag_key}' is not used in any experiments."
153+
)
154+
return nil
155+
else
156+
# Evaluate each experiment and return the first bucketed experiment variation
157+
feature_flag['experimentIds'].each do |experiment_id|
158+
experiment = @config.experiment_id_map[experiment_id]
159+
unless experiment
179160
@config.logger.log(
180-
Logger::INFO,
181-
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
161+
Logger::DEBUG,
162+
"Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
182163
)
183164
return nil
184165
end
185-
else
186-
bucketed_experiment_id = experiment_id
187-
end
188-
189-
if feature_flag['experimentIds'].include?(bucketed_experiment_id)
190-
experiment = @config.experiment_id_map[bucketed_experiment_id]
191166
experiment_key = experiment['key']
192167
variation_id = get_variation(experiment_key, user_id, attributes)
193-
unless variation_id.nil?
194-
variation = @config.variation_id_map[experiment_key][variation_id]
195-
@config.logger.log(
196-
Logger::INFO,
197-
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
198-
)
199-
return {
200-
'variation' => variation,
201-
'experiment' => experiment
202-
}
203-
else
204-
@config.logger.log(
205-
Logger::INFO,
206-
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
207-
)
208-
end
168+
next unless variation_id
169+
variation = @config.variation_id_map[experiment_key][variation_id]
170+
@config.logger.log(Logger::INFO,
171+
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature "\
172+
"'#{feature_flag_key}'.")
173+
return {
174+
'experiment' => experiment,
175+
'variation' => variation
176+
}
209177
end
210-
else
211178
@config.logger.log(
212-
Logger::DEBUG,
213-
"The feature flag '#{feature_flag_key}' is not used in any experiments."
179+
Logger::INFO,
180+
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
214181
)
215182
end
216183

217-
return nil
184+
nil
218185
end
219186

220187
def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
@@ -227,72 +194,75 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
227194
#
228195
# Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
229196

197+
bucketing_id = get_bucketing_id(user_id, attributes)
198+
230199
rollout_id = feature_flag['rolloutId']
231-
if rollout_id.nil? or rollout_id.empty?
200+
if rollout_id.nil? || rollout_id.empty?
232201
feature_flag_key = feature_flag['key']
233202
@config.logger.log(
234203
Logger::DEBUG,
235-
"Feature flag '#{feature_flag_key}' is not part of a rollout."
204+
"Feature flag '#{feature_flag_key}' is not used in a rollout."
236205
)
237206
return nil
238207
end
239208

240209
rollout = @config.get_rollout_from_id(rollout_id)
241-
unless rollout.nil? or rollout['experiments'].empty?
242-
rollout_experiments = rollout['experiments']
243-
number_of_rules = rollout_experiments.length - 1
210+
if rollout.nil?
211+
@config.logger.log(
212+
Logger::DEBUG,
213+
"Rollout with ID '#{rollout_id}' is not in the datafile '#{feature_flag['key']}'"
214+
)
215+
return nil
216+
end
244217

245-
# Go through each experiment in order and try to get the variation for the user
246-
for index in (0...number_of_rules)
247-
experiment = rollout_experiments[index]
248-
experiment_key = experiment['key']
218+
return nil if rollout['experiments'].empty?
249219

250-
# Check that user meets audience conditions for targeting rule
251-
unless Audience.user_in_experiment?(@config, experiment, attributes)
252-
@config.logger.log(
253-
Logger::DEBUG,
254-
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
255-
)
256-
# move onto the next targeting rule
257-
next
258-
end
220+
rollout_rules = rollout['experiments']
221+
number_of_rules = rollout_rules.length - 1
259222

260-
@config.logger.log(
261-
Logger::DEBUG,
262-
"User '#{user_id}' meets conditions for targeting rule '#{index + 1}'."
263-
)
264-
variation = @bucketer.bucket(experiment, user_id)
265-
unless variation.nil?
266-
variation_key = variation['key']
267-
return variation
268-
end
223+
# Go through each experiment in order and try to get the variation for the user
224+
number_of_rules.times do |index|
225+
rollout_rule = rollout_rules[index]
226+
audience_id = rollout_rule['audienceIds'][0]
227+
audience = @config.get_audience_from_id(audience_id)
228+
audience_name = audience['name']
269229

270-
# User failed traffic allocation, jump to Everyone Else rule
230+
# Check that user meets audience conditions for targeting rule
231+
unless Audience.user_in_experiment?(@config, rollout_rule, attributes)
271232
@config.logger.log(
272233
Logger::DEBUG,
273-
"User '#{user_id}' is not in the traffic group for the targeting rule. Checking 'Eveyrone Else' rule now."
234+
"User '#{user_id}' does not meet the conditions to be in rollout rule for audience '#{audience_name}'."
274235
)
275-
break
236+
# move onto the next targeting rule
237+
next
276238
end
277239

278-
# Evalute the "Everyone Else" rule, which is the last rule.
279-
everyone_else_experiment = rollout_experiments[number_of_rules]
280-
variation = @bucketer.bucket(everyone_else_experiment, user_id)
281-
unless variation.nil?
282-
@config.logger.log(
283-
Logger::DEBUG,
284-
"User '#{user_id}' meets conditions for targeting rule 'Everyone Else'."
285-
)
286-
return variation
287-
end
240+
@config.logger.log(
241+
Logger::DEBUG,
242+
"Attempting to bucket user '#{user_id}' into rollout rule for audience '#{audience_name}'."
243+
)
244+
# Evaluate if user satisfies the traffic allocation for this rollout rule
245+
variation = @bucketer.bucket(rollout_rule, bucketing_id, user_id)
246+
return variation unless variation.nil?
288247

248+
# User failed traffic allocation, jump to Everyone Else rule
289249
@config.logger.log(
290250
Logger::DEBUG,
291-
"User '#{user_id}' does not meet conditions for targeting rule 'Everyone Else'."
251+
"User '#{user_id}' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."
292252
)
253+
break
293254
end
294255

295-
return nil
256+
# get last rule which is the everyone else rule
257+
everyone_else_experiment = rollout_rules[number_of_rules]
258+
variation = @bucketer.bucket(everyone_else_experiment, bucketing_id, user_id)
259+
return variation unless variation.nil?
260+
261+
@config.logger.log(
262+
Logger::DEBUG,
263+
"User '#{user_id}' was excluded from the 'Everyone Else' rule for feature flag"
264+
)
265+
nil
296266
end
297267

298268
private
@@ -374,7 +344,6 @@ def get_user_profile(user_id)
374344
user_profile
375345
end
376346

377-
378347
def save_user_profile(user_profile, experiment_id, variation_id)
379348
# Save a given bucketing decision to a given user profile
380349
#
@@ -387,13 +356,32 @@ def save_user_profile(user_profile, experiment_id, variation_id)
387356
user_id = user_profile[:user_id]
388357
begin
389358
user_profile[:experiment_bucket_map][experiment_id] = {
390-
:variation_id => variation_id
359+
variation_id: variation_id
391360
}
392361
@user_profile_service.save(user_profile)
393362
@config.logger.log(Logger::INFO, "Saved variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
394363
rescue => e
395364
@config.logger.log(Logger::ERROR, "Error while saving user profile for user ID '#{user_id}': #{e}.")
396365
end
397366
end
367+
368+
def get_bucketing_id(user_id, attributes)
369+
# Gets the Bucketing Id for Bucketing
370+
#
371+
# user_id - String user ID
372+
# attributes - Hash user attributes
373+
374+
# By default, the bucketing ID should be the user ID
375+
bucketing_id = user_id
376+
377+
# If the bucketing ID key is defined in attributes, then use that in place of the userID
378+
if attributes && attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].is_a?(String)
379+
unless attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID].empty?
380+
bucketing_id = attributes[RESERVED_ATTRIBUTE_KEY_BUCKETING_ID]
381+
@config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
382+
end
383+
end
384+
bucketing_id
385+
end
398386
end
399387
end

lib/optimizely/project_config.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ def get_experiment_ids_for_event(event_key)
198198
[]
199199
end
200200

201-
def get_audience_conditions_from_id(audience_id)
202-
# Get audience conditions for the provided audience ID
201+
def get_audience_from_id(audience_id)
202+
# Get audience for the provided audience ID
203203
#
204204
# audience_id - ID of the audience
205205
#
206-
# Returns conditions for the audience
206+
# Returns the audience
207207

208208
audience = @audience_id_map[audience_id]
209-
return audience['conditions'] if audience
209+
return audience if audience
210210
@logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
211211
@error_handler.handle_error InvalidAudienceError
212212
nil

0 commit comments

Comments
 (0)