From 76e1ee1a71b866dbbc54b6787e6b5a1cf153beb5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 6 Feb 2026 14:01:43 -0800 Subject: [PATCH] [FSSDK-12262] Exclude CMAB from UserProfileService - Skip UPS retrieval for CMAB experiments - Skip UPS saving for CMAB experiments - Add decision reason when UPS is excluded - Add test to verify CMAB excludes UPS Co-Authored-By: Claude Sonnet 4.5 --- lib/optimizely/decision_service.rb | 22 ++++++--- spec/decision_service_spec.rb | 78 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index 051a8b6..a92b2a4 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -100,14 +100,21 @@ 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 + # Exclude CMAB experiments from UPS - they handle dynamic decisions differently 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." - @logger.log(Logger::INFO, message) + unless experiment.key?('cmab') + 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 = "Skipping User Profile Service for CMAB experiment '#{experiment_key}'." + @logger.log(Logger::DEBUG, message) decide_reasons.push(message) - return VariationResult.new(nil, false, decide_reasons, saved_variation_id) end end @@ -155,7 +162,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 + # Exclude CMAB experiments from UPS - they handle dynamic decisions differently + user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker && experiment.key?('cmab') 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..bf6aa71 100644 --- a/spec/decision_service_spec.rb +++ b/spec/decision_service_spec.rb @@ -1166,5 +1166,83 @@ expect(spy_cmab_service).not_to have_received(:get_decision) end end + + describe 'when CMAB experiment excludes User Profile Service' do + it 'should skip UPS retrieval and saving for CMAB experiments' do + # Create a CMAB experiment configuration + cmab_experiment = { + 'id' => '111150', + 'key' => 'cmab_experiment', + 'status' => 'Running', + 'layerId' => '111150', + 'audienceIds' => [], + 'forcedVariations' => {}, + 'variations' => [ + {'id' => '111151', 'key' => 'variation_1'}, + {'id' => '111152', 'key' => 'variation_2'} + ], + 'trafficAllocation' => [ + {'entityId' => '111151', 'endOfRange' => 5000}, + {'entityId' => '111152', 'endOfRange' => 10_000} + ], + 'cmab' => {'trafficAllocation' => 5000} + } + + # Create a mock user profile service + mock_user_profile_service = double('user_profile_service') + decision_service_with_ups = Optimizely::DecisionService.new(spy_logger, spy_cmab_service, mock_user_profile_service) + + # Create user profile with saved variation (should be ignored for CMAB) + user_profile = { + user_id: 'test_user', + experiment_bucket_map: { + '111150' => {variation_id: '111152'} + } + } + + # Mock user profile service lookup + allow(mock_user_profile_service).to receive(:lookup).with('test_user').and_return(user_profile) + allow(mock_user_profile_service).to receive(:save) + + user_context = project_instance.create_user_context('test_user', {}) + + # Mock experiment lookup + 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) + + # Mock audience evaluation to pass + allow(Optimizely::Audience).to receive(:user_meets_audience_conditions?).and_return([true, []]) + + # Mock bucketer to return a valid entity ID + allow(decision_service_with_ups.bucketer).to receive(:bucket_to_entity_id) + .with(config, cmab_experiment, 'test_user', 'test_user') + .and_return(['$', []]) + + # Mock CMAB service to return a decision (variation_1, not the stored variation_2) + allow(spy_cmab_service).to receive(:get_decision) + .with(config, user_context, '111150', []) + .and_return(Optimizely::CmabDecision.new(variation_id: '111151', cmab_uuid: 'test-cmab-uuid-123')) + + # Mock variation lookup + allow(config).to receive(:get_variation_from_id_by_experiment_id) + .with('111150', '111151') + .and_return({'id' => '111151', 'key' => 'variation_1'}) + + variation_result = decision_service_with_ups.get_variation(config, '111150', user_context) + + # Verify the variation returned is from CMAB, not from stored profile + expect(variation_result.variation_id).to eq('111151') + expect(variation_result.cmab_uuid).to eq('test-cmab-uuid-123') + expect(variation_result.error).to eq(false) + + # Verify UPS exclusion reason is in decision reasons + expect(variation_result.reasons).to include( + "Skipping User Profile Service for CMAB experiment 'cmab_experiment'." + ) + + # Verify user profile service save was NOT called (CMAB shouldn't save to UPS) + expect(mock_user_profile_service).not_to have_received(:save) + end + end end end