Skip to content

Commit f8fac58

Browse files
authored
Rollout bucketing (#57)
1 parent cc25602 commit f8fac58

File tree

5 files changed

+430
-19
lines changed

5 files changed

+430
-19
lines changed

lib/optimizely/decision_service.rb

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,34 @@ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
101101

102102
# check if the feature is being experiment on and whether the user is bucketed into the experiment
103103
decision = get_variation_for_feature_experiment(feature_flag, user_id, attributes)
104-
return decision
104+
unless decision.nil?
105+
return decision
106+
end
105107

106-
# @TODO(mng) next check if the user feature being rolled out and whether the user is part of the rollout
107-
end
108+
feature_flag_key = feature_flag['key']
109+
variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
110+
if variation
111+
@config.logger.log(
112+
Logger::INFO,
113+
"User '#{user_id}' is in the rollout for feature flag '#{feature_flag_key}'."
114+
)
115+
# return decision with nil experiment so we don't track impressions for it
116+
return {
117+
'experiment' => nil,
118+
'variation' => variation
119+
}
120+
else
121+
@config.logger.log(
122+
Logger::INFO,
123+
"User '#{user_id}' is not in the rollout for feature flag '#{feature_flag_key}'."
124+
)
125+
end
108126

109-
private
127+
return nil
128+
end
110129

111130
def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil)
112-
# Gets the variation the user is bucketed into for the feature flag's experiment
131+
# Gets the variation the user is bucketed into for the feature flag's experiment.
113132
#
114133
# feature_flag - The feature flag the user wants to access
115134
# user_id - String ID for the user
@@ -178,6 +197,90 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
178197
return nil
179198
end
180199

200+
def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
201+
# Determine which variation the user is in for a given rollout.
202+
# Returns the variation of the first experiment the user qualifies for.
203+
#
204+
# feature_flag - The feature flag the user wants to access
205+
# user_id - String ID for the user
206+
# attributes - Hash representing user attributes
207+
#
208+
# Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
209+
210+
rollout_id = feature_flag['rolloutId']
211+
if rollout_id.nil? or rollout_id.empty?
212+
feature_flag_key = feature_flag['key']
213+
@config.logger.log(
214+
Logger::DEBUG,
215+
"Feature flag '#{feature_flag_key}' is not part of a rollout."
216+
)
217+
return nil
218+
end
219+
220+
rollout = @config.get_rollout_from_id(rollout_id)
221+
unless rollout.nil? or rollout['experiments'].empty?
222+
rollout_experiments = rollout['experiments']
223+
number_of_rules = rollout_experiments.length - 1
224+
225+
# Go through each experiment in order and try to get the variation for the user
226+
for index in (0...number_of_rules)
227+
experiment = rollout_experiments[index]
228+
experiment_key = experiment['key']
229+
230+
# Check that user meets audience conditions for targeting rule
231+
unless Audience.user_in_experiment?(@config, experiment, attributes)
232+
@config.logger.log(
233+
Logger::DEBUG,
234+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
235+
)
236+
# move onto the next targeting rule
237+
next
238+
end
239+
240+
@config.logger.log(
241+
Logger::DEBUG,
242+
"User '#{user_id}' meets conditions for targeting rule '#{index + 1}'."
243+
)
244+
variation = @bucketer.bucket(experiment, user_id)
245+
unless variation.nil?
246+
variation_key = variation['key']
247+
@config.logger.log(
248+
Logger::DEBUG,
249+
"User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_key}'."
250+
)
251+
return variation
252+
end
253+
254+
# User failed traffic allocation, jump to Everyone Else rule
255+
@config.logger.log(
256+
Logger::DEBUG,
257+
"User '#{user_id}' is not in the traffic group for the targeting rule. Checking 'Eveyrone Else' rule now."
258+
)
259+
break
260+
end
261+
262+
# Evalute the "Everyone Else" rule, which is the last rule.
263+
everyone_else_experiment = rollout_experiments[number_of_rules]
264+
variation = @bucketer.bucket(everyone_else_experiment, user_id)
265+
unless variation.nil?
266+
@config.logger.log(
267+
Logger::DEBUG,
268+
"User '#{user_id}' meets conditions for targeting rule 'Everyone Else'."
269+
)
270+
return variation
271+
end
272+
273+
@config.logger.log(
274+
Logger::DEBUG,
275+
"User '#{user_id}' does not meet conditions for targeting rule 'Everyone Else'."
276+
)
277+
end
278+
279+
return nil
280+
end
281+
282+
private
283+
181284
def get_forced_variation_id(experiment_key, user_id)
182285
# Determine if a user is forced into a variation for the given experiment and return the ID of that variation
183286
#

lib/optimizely/project_config.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,18 @@ def get_feature_flag_from_key(feature_flag_key)
303303
nil
304304
end
305305

306+
def get_rollout_from_id(rollout_id)
307+
# Retrieves the rollout with the given ID
308+
#
309+
# rollout_id - String rollout ID
310+
#
311+
# Returns the rollout if found, otherwise nil
312+
rollout = @rollout_id_map[rollout_id]
313+
return rollout if rollout
314+
@logger.log Logger::ERROR, "Rollout with ID '#{rollout_id}' is not in the datafile."
315+
nil
316+
end
317+
306318
private
307319

308320
def generate_key_map(array, key)

0 commit comments

Comments
 (0)