|
1166 | 1166 | expect(spy_cmab_service).not_to have_received(:get_decision) |
1167 | 1167 | end |
1168 | 1168 | end |
| 1169 | + |
| 1170 | + describe 'UPS exclusion for CMAB experiments' do |
| 1171 | + it 'should not save user profile for CMAB experiment decisions' 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(user_context.user_id, 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-123')) |
| 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 | + expect(variation_result.variation_id).to eq('111151') |
| 1208 | + expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-123') |
| 1209 | + expect(variation_result.error).to eq(false) |
| 1210 | + |
| 1211 | + # Verify user profile was NOT updated for CMAB experiment |
| 1212 | + expect(spy_user_profile_service).not_to have_received(:save) |
| 1213 | + |
| 1214 | + # Verify debug log was called to explain CMAB UPS exclusion |
| 1215 | + expect(spy_logger).to have_received(:log).with( |
| 1216 | + Logger::DEBUG, |
| 1217 | + "Skipping user profile service for CMAB experiment 'cmab_experiment'. CMAB decisions are dynamic and not stored for sticky bucketing." |
| 1218 | + ) |
| 1219 | + |
| 1220 | + # Verify the decision reason includes UPS exclusion message |
| 1221 | + expect(variation_result.reasons).to include( |
| 1222 | + "Skipping user profile service for CMAB experiment 'cmab_experiment'. CMAB decisions are dynamic and not stored for sticky bucketing." |
| 1223 | + ) |
| 1224 | + end |
| 1225 | + |
| 1226 | + it 'should not look up saved user profile decisions for CMAB experiments' do |
| 1227 | + saved_user_profile = { |
| 1228 | + user_id: 'test_user', |
| 1229 | + experiment_bucket_map: { |
| 1230 | + '111150' => { |
| 1231 | + variation_id: '111152' |
| 1232 | + } |
| 1233 | + } |
| 1234 | + } |
| 1235 | + allow(spy_user_profile_service).to receive(:lookup) |
| 1236 | + .with('test_user').and_return(saved_user_profile) |
| 1237 | + |
| 1238 | + cmab_experiment = { |
| 1239 | + 'id' => '111150', |
| 1240 | + 'key' => 'cmab_experiment', |
| 1241 | + 'status' => 'Running', |
| 1242 | + 'layerId' => '111150', |
| 1243 | + 'audienceIds' => [], |
| 1244 | + 'forcedVariations' => {}, |
| 1245 | + 'variations' => [ |
| 1246 | + {'id' => '111151', 'key' => 'variation_1'}, |
| 1247 | + {'id' => '111152', 'key' => 'variation_2'} |
| 1248 | + ], |
| 1249 | + 'trafficAllocation' => [ |
| 1250 | + {'entityId' => '111151', 'endOfRange' => 5000}, |
| 1251 | + {'entityId' => '111152', 'endOfRange' => 10_000} |
| 1252 | + ], |
| 1253 | + 'cmab' => {'trafficAllocation' => 5000} |
| 1254 | + } |
| 1255 | + user_context = project_instance.create_user_context('test_user', {}) |
| 1256 | + user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger) |
| 1257 | + user_profile_tracker.load_user_profile |
| 1258 | + |
| 1259 | + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) |
| 1260 | + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) |
| 1261 | + allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []]) |
| 1262 | + allow(decision_service.bucketer).to receive(:bucket_to_entity_id) |
| 1263 | + .with(config, cmab_experiment, 'test_user', 'test_user') |
| 1264 | + .and_return(['$', []]) |
| 1265 | + allow(spy_cmab_service).to receive(:get_decision) |
| 1266 | + .with(config, user_context, '111150', []) |
| 1267 | + .and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid-456')) |
| 1268 | + allow(config).to receive(:get_variation_from_id_by_experiment_id) |
| 1269 | + .with('111150', '111151') |
| 1270 | + .and_return({'id' => '111151', 'key' => 'variation_1'}) |
| 1271 | + |
| 1272 | + variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker) |
| 1273 | + |
| 1274 | + # Should return CMAB decision (variation_1), NOT the saved UPS decision (variation_2) |
| 1275 | + expect(variation_result.variation_id).to eq('111151') |
| 1276 | + expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-456') |
| 1277 | + expect(variation_result.error).to eq(false) |
| 1278 | + |
| 1279 | + # Verify CMAB service was called (not short-circuited by UPS lookup) |
| 1280 | + expect(spy_cmab_service).to have_received(:get_decision).once |
| 1281 | + |
| 1282 | + # Verify the reasons do NOT include a "Returning previously activated" message |
| 1283 | + expect(variation_result.reasons).not_to include( |
| 1284 | + a_string_matching(/Returning previously activated/) |
| 1285 | + ) |
| 1286 | + |
| 1287 | + # Verify the reasons DO include the CMAB UPS exclusion message |
| 1288 | + expect(variation_result.reasons).to include( |
| 1289 | + "Skipping user profile service for CMAB experiment 'cmab_experiment'. CMAB decisions are dynamic and not stored for sticky bucketing." |
| 1290 | + ) |
| 1291 | + end |
| 1292 | + |
| 1293 | + it 'should still save user profile for standard (non-CMAB) experiments' do |
| 1294 | + # Use a standard experiment (no cmab key) |
| 1295 | + user_context = project_instance.create_user_context('test_user') |
| 1296 | + user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger) |
| 1297 | + allow(spy_user_profile_service).to receive(:lookup).and_return(nil) |
| 1298 | + |
| 1299 | + allow(decision_service.bucketer).to receive(:bucket).and_call_original |
| 1300 | + |
| 1301 | + variation_result = decision_service.get_variation(config, '111127', user_context, user_profile_tracker) |
| 1302 | + expect(variation_result.variation_id).to eq('111128') |
| 1303 | + |
| 1304 | + # Verify user profile WAS updated for standard experiment |
| 1305 | + expect(user_profile_tracker.user_profile[:experiment_bucket_map]['111127']).to eq({variation_id: '111128'}) |
| 1306 | + |
| 1307 | + # Verify the reasons do NOT include the CMAB UPS exclusion message |
| 1308 | + expect(variation_result.reasons).not_to include( |
| 1309 | + a_string_matching(/Skipping user profile service for CMAB experiment/) |
| 1310 | + ) |
| 1311 | + end |
| 1312 | + end |
1169 | 1313 | end |
1170 | 1314 | end |
0 commit comments