Skip to content

Commit 75ee816

Browse files
update: Integrate CMAB decision logic into DecisionService and update related tests
1 parent d6dd3aa commit 75ee816

File tree

2 files changed

+157
-137
lines changed

2 files changed

+157
-137
lines changed

lib/optimizely/decision_service.rb

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class DecisionService
2929
# 3. Check whitelisting
3030
# 4. Check user profile service for past bucketing decisions (sticky bucketing)
3131
# 5. Check audience targeting
32-
# 6. Use Murmurhash3 to bucket the user
32+
# 6. Check cmab service
33+
# 7. Use Murmurhash3 to bucket the user
3334

3435
attr_reader :bucketer
3536

@@ -39,7 +40,7 @@ class DecisionService
3940

4041
Decision = Struct.new(:experiment, :variation, :source, :cmab_uuid)
4142
CmabDecisionResult = Struct.new(:error, :result, :reasons)
42-
VariationResult = Struct.new(:cmab_uuid, :error, :reasons, :variation)
43+
VariationResult = Struct.new(:cmab_uuid, :error, :reasons, :variation_id)
4344
DecisionResult = Struct.new(:decision, :error, :reasons)
4445

4546
DECISION_SOURCES = {
@@ -77,25 +78,25 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
7778
decide_reasons.push(*bucketing_id_reasons)
7879
# Check to make sure experiment is active
7980
experiment = project_config.get_experiment_from_id(experiment_id)
80-
return nil, decide_reasons if experiment.nil?
81+
return VariationResult.new(nil, false, decide_reasons, nil) if experiment.nil?
8182

8283
experiment_key = experiment['key']
8384
unless project_config.experiment_running?(experiment)
8485
message = "Experiment '#{experiment_key}' is not running."
8586
@logger.log(Logger::INFO, message)
8687
decide_reasons.push(message)
87-
return nil, decide_reasons
88+
return VariationResult.new(nil, false, decide_reasons, nil)
8889
end
8990

9091
# Check if a forced variation is set for the user
9192
forced_variation, reasons_received = get_forced_variation(project_config, experiment['key'], user_id)
9293
decide_reasons.push(*reasons_received)
93-
return forced_variation['id'], decide_reasons if forced_variation
94+
return VariationResult.new(nil, false, decide_reasons, forced_variation['id']) if forced_variation
9495

9596
# Check if user is in a white-listed variation
9697
whitelisted_variation_id, reasons_received = get_whitelisted_variation_id(project_config, experiment_id, user_id)
9798
decide_reasons.push(*reasons_received)
98-
return whitelisted_variation_id, decide_reasons if whitelisted_variation_id
99+
return VariationResult.new(nil, false, decide_reasons, whitelisted_variation_id) if whitelisted_variation_id
99100

100101
should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
101102
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
@@ -106,7 +107,7 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
106107
message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
107108
@logger.log(Logger::INFO, message)
108109
decide_reasons.push(message)
109-
return saved_variation_id, decide_reasons
110+
return VariationResult.new(nil, false, decide_reasons, saved_variation_id)
110111
end
111112
end
112113

@@ -117,27 +118,43 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
117118
message = "User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'."
118119
@logger.log(Logger::INFO, message)
119120
decide_reasons.push(message)
120-
return nil, decide_reasons
121+
return VariationResult.new(nil, false, decide_reasons, nil)
121122
end
122123

123-
# Bucket normally
124-
variation, bucket_reasons = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
125-
decide_reasons.push(*bucket_reasons)
126-
variation_id = variation ? variation['id'] : nil
124+
# Check if this is a CMAB experiment
125+
# If so, handle CMAB-specific traffic allocation and decision logic.
126+
# Otherwise, proceed with standard bucketing logic for non-CMAB experiments.
127+
if experiment.key?('cmab')
128+
cmab_decision_result = get_decision_for_cmab_experiment(project_config, experiment, user_context, bucketing_id, decide_options)
129+
decide_reasons.push(*cmab_decision_result.reasons)
130+
if cmab_decision_result.error
131+
# CMAB decision failed, return error
132+
return VariationResult.new(nil, true, decide_reasons, nil)
133+
end
127134

128-
message = ''
129-
if variation_id
130-
variation_key = variation['key']
131-
message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_id}'."
135+
cmab_decision = cmab_decision_result.result
136+
variation_id = cmab_decision&.variation_id
137+
cmab_uuid = cmab_decision&.cmab_uuid
132138
else
133-
message = "User '#{user_id}' is in no variation."
139+
# Bucket normally
140+
variation, bucket_reasons = @bucketer.bucket(project_config, experiment, bucketing_id, user_id)
141+
decide_reasons.push(*bucket_reasons)
142+
variation_id = variation ? variation['id'] : nil
143+
cmab_uuid = nil
144+
message = ''
145+
if variation_id
146+
variation_key = variation['key']
147+
message = "User '#{user_id}' is in variation '#{variation_key}' of experiment '#{experiment_id}'."
148+
else
149+
message = "User '#{user_id}' is in no variation."
150+
end
151+
@logger.log(Logger::INFO, message)
152+
decide_reasons.push(message)
134153
end
135-
@logger.log(Logger::INFO, message)
136-
decide_reasons.push(message)
137154

138155
# Persist bucketing decision
139156
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
140-
[variation_id, decide_reasons]
157+
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
141158
end
142159

143160
def get_variation_for_feature(project_config, feature_flag, user_context, decide_options = [])
@@ -203,7 +220,7 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_cont
203220
message = "The feature flag '#{feature_flag_key}' is not used in any experiments."
204221
@logger.log(Logger::DEBUG, message)
205222
decide_reasons.push(message)
206-
return nil, decide_reasons
223+
return DecisionResult.new(nil, false, decide_reasons)
207224
end
208225

209226
# Evaluate each experiment and return the first bucketed experiment variation
@@ -213,26 +230,31 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_cont
213230
message = "Feature flag experiment with ID '#{experiment_id}' is not in the datafile."
214231
@logger.log(Logger::DEBUG, message)
215232
decide_reasons.push(message)
216-
return nil, decide_reasons
233+
return DecisionResult.new(nil, false, decide_reasons)
217234
end
218235

219236
experiment_id = experiment['id']
220-
variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, user_profile_tracker, decide_options)
237+
variation_result = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, user_profile_tracker, decide_options)
238+
error = variation_result.error
239+
reasons_received = variation_result.reasons
240+
variation_id = variation_result.variation_id
241+
cmab_uuid = variation_result.cmab_uuid
221242
decide_reasons.push(*reasons_received)
222243

223244
next unless variation_id
224245

225246
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
226247
variation = project_config.get_variation_from_flag(feature_flag['key'], variation_id, 'id') if variation.nil?
227248

228-
return Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST']), decide_reasons
249+
decision = Decision.new(experiment, variation, DECISION_SOURCES['FEATURE_TEST'], cmab_uuid)
250+
return DecisionResult.new(decision, error, decide_reasons)
229251
end
230252

231253
message = "The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
232254
@logger.log(Logger::INFO, message)
233255
decide_reasons.push(message)
234256

235-
[nil, decide_reasons]
257+
DecisionResult.new(nil, false, decide_reasons)
236258
end
237259

238260
def get_variation_for_feature_rollout(project_config, feature_flag, user_context)
@@ -298,12 +320,9 @@ def get_variation_from_experiment_rule(project_config, flag_key, rule, user, use
298320
variation, forced_reasons = validated_forced_decision(project_config, context, user)
299321
reasons.push(*forced_reasons)
300322

301-
return [variation['id'], reasons] if variation
302-
303-
variation_id, response_reasons = get_variation(project_config, rule['id'], user, user_profile_tracker, options)
304-
reasons.push(*response_reasons)
323+
return VariationResult.new(nil, false, reasons, variation['id']) if variation
305324

306-
[variation_id, reasons]
325+
get_variation(project_config, rule['id'], user, user_profile_tracker, options)
307326
end
308327

309328
def get_variation_from_delivery_rule(project_config, flag_key, rules, rule_index, user_context)
@@ -493,7 +512,7 @@ def get_decision_for_cmab_experiment(project_config, experiment, user_context, b
493512
bucketed_entity_id, bucket_reasons = @bucketer.bucket_to_entity_id(
494513
project_config, experiment, user_id, bucketing_id
495514
)
496-
decide_reasons.extend(bucket_reasons)
515+
decide_reasons.push(*bucket_reasons)
497516
unless bucketed_entity_id
498517
message = "User \"#{user_context.user_id}\" not in CMAB experiment \"#{experiment['key']}\" due to traffic allocation."
499518
@logger.log(Logger::INFO, message)

0 commit comments

Comments
 (0)