Skip to content

Commit a9d4fe8

Browse files
committed
[AI-FSSDK] [FSSDK-12262] Exclude CMAB from UserProfileService
1 parent 4829b04 commit a9d4fe8

3 files changed

Lines changed: 114 additions & 3 deletions

File tree

lib/optimizely/decision_service.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,15 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
9999
return VariationResult.new(nil, false, decide_reasons, whitelisted_variation_id) if whitelisted_variation_id
100100

101101
should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
102-
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
103-
unless should_ignore_user_profile_service && user_profile_tracker
102+
# CMAB experiments are excluded from UPS because CMAB decisions are dynamic
103+
# and should not use sticky bucketing.
104+
is_cmab_experiment = experiment.key?('cmab')
105+
if is_cmab_experiment
106+
message = format(Optimizely::Helpers::Constants::CMAB_UPS_EXCLUDED, experiment_key)
107+
@logger.log(Logger::INFO, message)
108+
decide_reasons.push(message)
109+
elsif !(should_ignore_user_profile_service && user_profile_tracker)
110+
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
104111
saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)
105112
decide_reasons.push(*reasons_received)
106113
if saved_variation_id
@@ -155,7 +162,8 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
155162
decide_reasons.push(message) if message
156163

157164
# Persist bucketing decision
158-
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
165+
# CMAB experiments are excluded from UPS - do not save their decisions
166+
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker || is_cmab_experiment
159167
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
160168
end
161169

lib/optimizely/helpers/constants.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ module Constants
500500

501501
CMAB_FETCH_FAILED = 'CMAB decision fetch failed (%s).'
502502
INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'
503+
CMAB_UPS_EXCLUDED = "Skipping user profile service for CMAB experiment '%s'. CMAB decisions should not use UPS for sticky bucketing."
503504
end
504505
end
505506
end

spec/decision_service_spec.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,108 @@
11211121
end
11221122
end
11231123

1124+
describe 'CMAB UPS exclusion' do
1125+
it 'should skip UPS lookup for CMAB experiments' do
1126+
cmab_experiment = {
1127+
'id' => '111150',
1128+
'key' => 'cmab_experiment',
1129+
'status' => 'Running',
1130+
'layerId' => '111150',
1131+
'audienceIds' => [],
1132+
'forcedVariations' => {},
1133+
'variations' => [
1134+
{'id' => '111151', 'key' => 'variation_1'},
1135+
{'id' => '111152', 'key' => 'variation_2'}
1136+
],
1137+
'trafficAllocation' => [
1138+
{'entityId' => '111151', 'endOfRange' => 5000},
1139+
{'entityId' => '111152', 'endOfRange' => 10_000}
1140+
],
1141+
'cmab' => {'trafficAllocation' => 5000}
1142+
}
1143+
user_context = project_instance.create_user_context('test_user', {})
1144+
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
1145+
1146+
allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment)
1147+
allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true)
1148+
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])
1149+
allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
1150+
.with(config, cmab_experiment, 'test_user', 'test_user')
1151+
.and_return(['$', []])
1152+
allow(spy_cmab_service).to receive(:get_decision)
1153+
.with(config, user_context, '111150', [])
1154+
.and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid'))
1155+
allow(config).to receive(:get_variation_from_id_by_experiment_id)
1156+
.with('111150', '111151')
1157+
.and_return({'id' => '111151', 'key' => 'variation_1'})
1158+
1159+
# UPS lookup should not be called for CMAB experiments
1160+
expect(decision_service).not_to receive(:get_saved_variation_id)
1161+
1162+
variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker)
1163+
1164+
expect(variation_result.variation_id).to eq('111151')
1165+
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid')
1166+
expect(variation_result.reasons).to include(
1167+
"Skipping user profile service for CMAB experiment 'cmab_experiment'. CMAB decisions should not use UPS for sticky bucketing."
1168+
)
1169+
end
1170+
1171+
it 'should skip UPS save for CMAB experiments' do
1172+
cmab_experiment = {
1173+
'id' => '111150',
1174+
'key' => 'cmab_experiment',
1175+
'status' => 'Running',
1176+
'layerId' => '111150',
1177+
'audienceIds' => [],
1178+
'forcedVariations' => {},
1179+
'variations' => [
1180+
{'id' => '111151', 'key' => 'variation_1'},
1181+
{'id' => '111152', 'key' => 'variation_2'}
1182+
],
1183+
'trafficAllocation' => [
1184+
{'entityId' => '111151', 'endOfRange' => 5000},
1185+
{'entityId' => '111152', 'endOfRange' => 10_000}
1186+
],
1187+
'cmab' => {'trafficAllocation' => 5000}
1188+
}
1189+
user_context = project_instance.create_user_context('test_user', {})
1190+
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
1191+
1192+
allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment)
1193+
allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true)
1194+
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])
1195+
allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
1196+
.with(config, cmab_experiment, 'test_user', 'test_user')
1197+
.and_return(['$', []])
1198+
allow(spy_cmab_service).to receive(:get_decision)
1199+
.with(config, user_context, '111150', [])
1200+
.and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid'))
1201+
allow(config).to receive(:get_variation_from_id_by_experiment_id)
1202+
.with('111150', '111151')
1203+
.and_return({'id' => '111151', 'key' => 'variation_1'})
1204+
1205+
# UPS save should not be called for CMAB experiments
1206+
expect(user_profile_tracker).not_to receive(:update_user_profile)
1207+
1208+
decision_service.get_variation(config, '111150', user_context, user_profile_tracker)
1209+
end
1210+
1211+
it 'should still use UPS for non-CMAB experiments' do
1212+
experiment = config.get_experiment_from_key('test_experiment')
1213+
experiment_id = experiment['id']
1214+
user_context = project_instance.create_user_context('test_user', {})
1215+
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
1216+
1217+
allow(config).to receive(:experiment_running?).with(experiment).and_return(true)
1218+
1219+
# UPS lookup should be called for non-CMAB experiments
1220+
expect(decision_service).to receive(:get_saved_variation_id).and_call_original
1221+
1222+
decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1223+
end
1224+
end
1225+
11241226
describe 'when user has whitelisted variation' do
11251227
it 'should return whitelisted variation and skip CMAB service call' do
11261228
# Create a CMAB experiment with whitelisted users

0 commit comments

Comments
 (0)