@@ -29,7 +29,8 @@ class DecisionService
29
29
# 3. Check whitelisting
30
30
# 4. Check user profile service for past bucketing decisions (sticky bucketing)
31
31
# 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
33
34
34
35
attr_reader :bucketer
35
36
@@ -39,7 +40,7 @@ class DecisionService
39
40
40
41
Decision = Struct . new ( :experiment , :variation , :source , :cmab_uuid )
41
42
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 )
43
44
DecisionResult = Struct . new ( :decision , :error , :reasons )
44
45
45
46
DECISION_SOURCES = {
@@ -77,25 +78,25 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
77
78
decide_reasons . push ( *bucketing_id_reasons )
78
79
# Check to make sure experiment is active
79
80
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?
81
82
82
83
experiment_key = experiment [ 'key' ]
83
84
unless project_config . experiment_running? ( experiment )
84
85
message = "Experiment '#{ experiment_key } ' is not running."
85
86
@logger . log ( Logger ::INFO , message )
86
87
decide_reasons . push ( message )
87
- return nil , decide_reasons
88
+ return VariationResult . new ( nil , false , decide_reasons , nil )
88
89
end
89
90
90
91
# Check if a forced variation is set for the user
91
92
forced_variation , reasons_received = get_forced_variation ( project_config , experiment [ 'key' ] , user_id )
92
93
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
94
95
95
96
# Check if user is in a white-listed variation
96
97
whitelisted_variation_id , reasons_received = get_whitelisted_variation_id ( project_config , experiment_id , user_id )
97
98
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
99
100
100
101
should_ignore_user_profile_service = decide_options . include? Optimizely ::Decide ::OptimizelyDecideOption ::IGNORE_USER_PROFILE_SERVICE
101
102
# 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
106
107
message = "Returning previously activated variation ID #{ saved_variation_id } of experiment '#{ experiment_key } ' for user '#{ user_id } ' from user profile."
107
108
@logger . log ( Logger ::INFO , message )
108
109
decide_reasons . push ( message )
109
- return saved_variation_id , decide_reasons
110
+ return VariationResult . new ( nil , false , decide_reasons , saved_variation_id )
110
111
end
111
112
end
112
113
@@ -117,27 +118,43 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
117
118
message = "User '#{ user_id } ' does not meet the conditions to be in experiment '#{ experiment_key } '."
118
119
@logger . log ( Logger ::INFO , message )
119
120
decide_reasons . push ( message )
120
- return nil , decide_reasons
121
+ return VariationResult . new ( nil , false , decide_reasons , nil )
121
122
end
122
123
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
127
134
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
132
138
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 )
134
153
end
135
- @logger . log ( Logger ::INFO , message )
136
- decide_reasons . push ( message )
137
154
138
155
# Persist bucketing decision
139
156
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 )
141
158
end
142
159
143
160
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
203
220
message = "The feature flag '#{ feature_flag_key } ' is not used in any experiments."
204
221
@logger . log ( Logger ::DEBUG , message )
205
222
decide_reasons . push ( message )
206
- return nil , decide_reasons
223
+ return DecisionResult . new ( nil , false , decide_reasons )
207
224
end
208
225
209
226
# 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
213
230
message = "Feature flag experiment with ID '#{ experiment_id } ' is not in the datafile."
214
231
@logger . log ( Logger ::DEBUG , message )
215
232
decide_reasons . push ( message )
216
- return nil , decide_reasons
233
+ return DecisionResult . new ( nil , false , decide_reasons )
217
234
end
218
235
219
236
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
221
242
decide_reasons . push ( *reasons_received )
222
243
223
244
next unless variation_id
224
245
225
246
variation = project_config . get_variation_from_id_by_experiment_id ( experiment_id , variation_id )
226
247
variation = project_config . get_variation_from_flag ( feature_flag [ 'key' ] , variation_id , 'id' ) if variation . nil?
227
248
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 )
229
251
end
230
252
231
253
message = "The user '#{ user_id } ' is not bucketed into any of the experiments on the feature '#{ feature_flag_key } '."
232
254
@logger . log ( Logger ::INFO , message )
233
255
decide_reasons . push ( message )
234
256
235
- [ nil , decide_reasons ]
257
+ DecisionResult . new ( nil , false , decide_reasons )
236
258
end
237
259
238
260
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
298
320
variation , forced_reasons = validated_forced_decision ( project_config , context , user )
299
321
reasons . push ( *forced_reasons )
300
322
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
305
324
306
- [ variation_id , reasons ]
325
+ get_variation ( project_config , rule [ 'id' ] , user , user_profile_tracker , options )
307
326
end
308
327
309
328
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
493
512
bucketed_entity_id , bucket_reasons = @bucketer . bucket_to_entity_id (
494
513
project_config , experiment , user_id , bucketing_id
495
514
)
496
- decide_reasons . extend ( bucket_reasons )
515
+ decide_reasons . push ( * bucket_reasons )
497
516
unless bucketed_entity_id
498
517
message = "User \" #{ user_context . user_id } \" not in CMAB experiment \" #{ experiment [ 'key' ] } \" due to traffic allocation."
499
518
@logger . log ( Logger ::INFO , message )
0 commit comments