Skip to content

Commit 193c82b

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

2 files changed

Lines changed: 137 additions & 2 deletions

File tree

lib/optimizely/decision_service.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,17 @@ 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+
is_cmab_experiment = experiment.key?('cmab')
103+
102104
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
103-
unless should_ignore_user_profile_service && user_profile_tracker
105+
# CMAB experiments are excluded from UPS because UPS maintains decisions
106+
# across the experiment lifetime without considering TTL or user attributes,
107+
# which contradicts CMAB's dynamic nature.
108+
if is_cmab_experiment && !should_ignore_user_profile_service && user_profile_tracker
109+
message = "Skipping user profile service for CMAB experiment '#{experiment_key}'."
110+
@logger.log(Logger::INFO, message)
111+
decide_reasons.push(message)
112+
elsif !should_ignore_user_profile_service && user_profile_tracker
104113
saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)
105114
decide_reasons.push(*reasons_received)
106115
if saved_variation_id
@@ -155,7 +164,8 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
155164
decide_reasons.push(message) if message
156165

157166
# Persist bucketing decision
158-
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
167+
# CMAB experiments are excluded from UPS to preserve dynamic decision-making.
168+
user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service || is_cmab_experiment
159169
VariationResult.new(cmab_uuid, false, decide_reasons, variation_id)
160170
end
161171

spec/decision_service_spec.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,5 +1166,130 @@
11661166
expect(spy_cmab_service).not_to have_received(:get_decision)
11671167
end
11681168
end
1169+
1170+
describe 'CMAB UPS exclusion' do
1171+
it 'should skip UPS lookup 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+
variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker)
1206+
1207+
# UPS lookup should NOT be called for CMAB experiments
1208+
expect(decision_service).not_to have_received(:get_saved_variation_id)
1209+
1210+
# Should still get a valid variation from CMAB service
1211+
expect(variation_result.variation_id).to eq('111151')
1212+
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid')
1213+
expect(variation_result.error).to eq(false)
1214+
1215+
# Should log that UPS was skipped for CMAB
1216+
expect(spy_logger).to have_received(:log).with(
1217+
Logger::INFO,
1218+
"Skipping user profile service for CMAB experiment 'cmab_experiment'."
1219+
)
1220+
expect(variation_result.reasons).to include(
1221+
"Skipping user profile service for CMAB experiment 'cmab_experiment'."
1222+
)
1223+
end
1224+
1225+
it 'should skip UPS save for CMAB experiments' do
1226+
cmab_experiment = {
1227+
'id' => '111150',
1228+
'key' => 'cmab_experiment',
1229+
'status' => 'Running',
1230+
'layerId' => '111150',
1231+
'audienceIds' => [],
1232+
'forcedVariations' => {},
1233+
'variations' => [
1234+
{'id' => '111151', 'key' => 'variation_1'},
1235+
{'id' => '111152', 'key' => 'variation_2'}
1236+
],
1237+
'trafficAllocation' => [
1238+
{'entityId' => '111151', 'endOfRange' => 5000},
1239+
{'entityId' => '111152', 'endOfRange' => 10_000}
1240+
],
1241+
'cmab' => {'trafficAllocation' => 5000}
1242+
}
1243+
user_context = project_instance.create_user_context('test_user', {})
1244+
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
1245+
1246+
allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment)
1247+
allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true)
1248+
allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []])
1249+
allow(decision_service.bucketer).to receive(:bucket_to_entity_id)
1250+
.with(config, cmab_experiment, 'test_user', 'test_user')
1251+
.and_return(['$', []])
1252+
allow(spy_cmab_service).to receive(:get_decision)
1253+
.with(config, user_context, '111150', [])
1254+
.and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid'))
1255+
allow(config).to receive(:get_variation_from_id_by_experiment_id)
1256+
.with('111150', '111151')
1257+
.and_return({'id' => '111151', 'key' => 'variation_1'})
1258+
1259+
variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker)
1260+
1261+
# UPS save should NOT be called for CMAB experiments
1262+
expect(user_profile_tracker).not_to have_received(:update_user_profile)
1263+
1264+
# Should still return a valid variation
1265+
expect(variation_result.variation_id).to eq('111151')
1266+
expect(variation_result.cmab_uuid).to eq('test-cmab-uuid')
1267+
end
1268+
1269+
it 'should still use UPS for non-CMAB experiments (regression test)' do
1270+
experiment = config.get_experiment_from_key('test_experiment')
1271+
experiment_id = experiment['id']
1272+
user_context = project_instance.create_user_context('test_user', {})
1273+
user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger)
1274+
user_profile_tracker.instance_variable_set(:@user_profile, {
1275+
user_id: 'test_user',
1276+
experiment_bucket_map: {
1277+
experiment_id => { variation_id: '111129' }
1278+
}
1279+
})
1280+
1281+
allow(config).to receive(:experiment_running?).with(experiment).and_return(true)
1282+
allow(config).to receive(:variation_id_exists?).with(experiment_id, '111129').and_return(true)
1283+
1284+
variation_result = decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1285+
1286+
# Should return the stored variation from UPS
1287+
expect(variation_result.variation_id).to eq('111129')
1288+
expect(variation_result.error).to eq(false)
1289+
expect(variation_result.reasons).to include(
1290+
a_string_matching(/Returning previously activated variation ID/)
1291+
)
1292+
end
1293+
end
11691294
end
11701295
end

0 commit comments

Comments
 (0)