diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index 051a8b6..32ac106 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -100,14 +100,22 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE # Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService + # CMAB experiments are excluded from UserProfileService to allow dynamic decisions + is_cmab_experiment = experiment.key?('cmab') unless should_ignore_user_profile_service && user_profile_tracker - saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile) - decide_reasons.push(*reasons_received) - if saved_variation_id - message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile." + unless is_cmab_experiment + saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile) + decide_reasons.push(*reasons_received) + if saved_variation_id + message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile." + @logger.log(Logger::INFO, message) + decide_reasons.push(message) + return VariationResult.new(nil, false, decide_reasons, saved_variation_id) + end + else + message = 'User profile service excluded for CMAB experiment to allow dynamic decisions.' @logger.log(Logger::INFO, message) decide_reasons.push(message) - return VariationResult.new(nil, false, decide_reasons, saved_variation_id) end end @@ -155,7 +163,8 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac decide_reasons.push(message) if message # Persist bucketing decision - user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker + # CMAB experiments are excluded from UserProfileService + user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker || is_cmab_experiment VariationResult.new(cmab_uuid, false, decide_reasons, variation_id) end diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb index 30ad7d2..6080ef4 100644 --- a/spec/decision_service_spec.rb +++ b/spec/decision_service_spec.rb @@ -1166,5 +1166,68 @@ expect(spy_cmab_service).not_to have_received(:get_decision) end end + + describe 'UserProfileService exclusion for CMAB' do + it 'should exclude CMAB experiments from UserProfileService and include decision reason' do + # Create a CMAB experiment + cmab_experiment = { + 'id' => '111150', + 'key' => 'cmab_experiment', + 'status' => 'Running', + 'audienceIds' => [], + 'audienceConditions' => [], + 'forcedVariations' => {}, + 'layerId' => '1', + 'trafficAllocation' => [ + {'entityId' => '111151', 'endOfRange' => 10_000} + ], + 'variations' => [ + {'id' => '111151', 'key' => 'variation_1', 'variables' => []} + ], + 'cmab' => true + } + + # Setup mocks for CMAB decision + cmab_decision = Optimizely::CmabService::CmabDecision.new('111151', 'test-uuid') + allow(spy_cmab_service).to receive(:get_decision) + .and_return([cmab_decision, ['CMAB decision made']]) + + # Setup user profile service with a stored variation + stored_profile = { + 'user_id' => 'test_user', + 'experiment_bucket_map' => { + '111150' => {'variation_id' => '111152'} # Different variation + } + } + allow(spy_user_profile_service).to receive(:lookup).and_return(stored_profile) + + # Setup config mocks + allow(config).to receive(:get_experiment_from_id).with('111150').and_return(cmab_experiment) + allow(config).to receive(:experiment_running?).with(cmab_experiment).and_return(true) + allow(config).to receive(:get_variation_from_id_by_experiment_id) + .with('111150', '111151') + .and_return({'id' => '111151', 'key' => 'variation_1'}) + + # Create user profile tracker + user_profile_tracker = Optimizely::UserProfileTracker.new('test_user', spy_user_profile_service, spy_logger) + + # Call get_variation + user_context = project_instance.create_user_context('test_user') + variation_result = decision_service.get_variation(config, '111150', user_context, user_profile_tracker) + + # Verify the CMAB decision was used (variation_1), not the stored profile (111152) + expect(variation_result.variation_id).to eq('111151') + expect(variation_result.cmab_uuid).to eq('test-uuid') + + # Verify the UPS exclusion reason is in the decision reasons + expect(variation_result.reasons).to include('User profile service excluded for CMAB experiment to allow dynamic decisions.') + + # Verify UPS lookup was NOT called (bypassed) + expect(spy_user_profile_service).not_to have_received(:lookup) + + # Verify UPS save was NOT called (bypassed) + expect(spy_user_profile_service).not_to have_received(:save) + end + end end end