@@ -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