From f1b8cdd7542fb799c79b5c01f095aec6983f551f Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 8 Dec 2025 08:43:49 -0600 Subject: [PATCH 01/11] Fix FSC tests --- .../config/datafile_project_config.rb | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index d6f1d27b..9073a17c 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -645,6 +645,27 @@ def get_holdouts_for_flag(flag_id) return [] if @holdouts.nil? || @holdouts.empty? + # Check cache and return persistent holdouts + return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id) + + # Prioritize global holdouts first + active_holdouts = [] + + excluded = @excluded_holdouts[flag_id] || [] + + if excluded.any? + active_holdouts = @global_holdouts.values.reject { |holdout| excluded.include?(holdout) } + else + active_holdouts = @global_holdouts.values.dup + end + + # Append included holdouts + included = @included_holdouts[flag_id] || [] + active_holdouts.concat(included) + + # Cache the result + @flag_holdouts_map[flag_id] = active_holdouts + @flag_holdouts_map[flag_id] || [] end From d2a7aa77110b550eb4f9b98e833fe81a3630c080 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 8 Dec 2025 10:20:51 -0600 Subject: [PATCH 02/11] Update holdout login on datafile_project_config --- .../config/datafile_project_config.rb | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 9073a17c..d4463c57 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -115,7 +115,7 @@ def initialize(datafile, logger, error_handler) @variation_id_to_experiment_map = {} @flag_variation_map = {} @holdout_id_map = {} - @global_holdouts = {} + @global_holdouts = [] @included_holdouts = {} @excluded_holdouts = {} @flag_holdouts_map = {} @@ -125,21 +125,29 @@ def initialize(datafile, logger, error_handler) @holdout_id_map[holdout['id']] = holdout - if holdout['includedFlags'].nil? || holdout['includedFlags'].empty? - @global_holdouts[holdout['id']] = holdout + included_flags = holdout['includedFlags'] || [] + excluded_flags = holdout['excludedFlags'] || [] - excluded_flags = holdout['excludedFlags'] - if excluded_flags && !excluded_flags.empty? - excluded_flags.each do |flag_id| - @excluded_holdouts[flag_id] ||= [] - @excluded_holdouts[flag_id] << holdout - end - end - else - holdout['includedFlags'].each do |flag_id| + case [included_flags.empty?, excluded_flags.empty?] + when [true, true] + # No included or excluded flags - this is a global holdout + @global_holdouts << holdout + + when [false, true], [false, false] + # Has included flags - add to included_holdouts map + included_flags.each do |flag_id| @included_holdouts[flag_id] ||= [] @included_holdouts[flag_id] << holdout end + + when [true, false] + # No included flags but has excluded flags - global with exclusions + @global_holdouts << holdout + + excluded_flags.each do |flag_id| + @excluded_holdouts[flag_id] ||= [] + @excluded_holdouts[flag_id] << holdout + end end end @@ -194,18 +202,6 @@ def initialize(datafile, logger, error_handler) feature_flag['experimentIds'].each do |experiment_id| @experiment_feature_map[experiment_id] = [feature_flag['id']] end - - flag_id = feature_flag['id'] - applicable_holdouts = [] - - applicable_holdouts.concat(@included_holdouts[flag_id]) if @included_holdouts[flag_id] - - @global_holdouts.each_value do |holdout| - excluded_flag_ids = holdout['excludedFlags'] || [] - applicable_holdouts << holdout unless excluded_flag_ids.include?(flag_id) - end - - @flag_holdouts_map[key] = applicable_holdouts unless applicable_holdouts.empty? end # Adding Holdout variations in variation id and key maps @@ -653,11 +649,11 @@ def get_holdouts_for_flag(flag_id) excluded = @excluded_holdouts[flag_id] || [] - if excluded.any? - active_holdouts = @global_holdouts.values.reject { |holdout| excluded.include?(holdout) } - else - active_holdouts = @global_holdouts.values.dup - end + active_holdouts = if excluded.any? + @global_holdouts.values.reject { |holdout| excluded.include?(holdout) } + else + @global_holdouts.values.dup + end # Append included holdouts included = @included_holdouts[flag_id] || [] From 98735fc18dfa76d88453666a332e58fed5cff5ed Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 8 Dec 2025 13:35:21 -0600 Subject: [PATCH 03/11] Fix lint --- lib/optimizely/config/datafile_project_config.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index d4463c57..7d7c232d 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -645,14 +645,12 @@ def get_holdouts_for_flag(flag_id) return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id) # Prioritize global holdouts first - active_holdouts = [] - excluded = @excluded_holdouts[flag_id] || [] active_holdouts = if excluded.any? - @global_holdouts.values.reject { |holdout| excluded.include?(holdout) } + @global_holdouts.reject { |holdout| excluded.include?(holdout) } else - @global_holdouts.values.dup + @global_holdouts.dup end # Append included holdouts From 188204ae6f659efcc676f09eacfb569da81bdb9f Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 8 Dec 2025 13:57:44 -0600 Subject: [PATCH 04/11] Update decision service --- lib/optimizely/decision_service.rb | 45 +++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index f1bc92e2..451999e9 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -312,14 +312,45 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context, decisions = [] feature_flags.each do |feature_flag| - # check if the feature is being experiment on and whether the user is bucketed into the experiment - decision_result = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options) - # Only process rollout if no experiment decision was found and no error - if decision_result.decision.nil? && !decision_result.error - decision_result_rollout = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision - decision_result.decision = decision_result_rollout.decision - decision_result.reasons.push(*decision_result_rollout.reasons) + # Check holdouts first + holdouts = project_config.get_holdouts_for_flag(feature_flag['id']) + decision_result = nil + + if holdouts && !holdouts.empty? + # Check each holdout + holdouts.each do |holdout| + holdout_decision = get_variation_for_holdout(holdout, user_context, project_config) + + if holdout_decision.decision + # User is bucketed into a holdout + decision_result = holdout_decision + break + else + # User didn't bucket into this holdout, try next or continue to experiments + decision_result = holdout_decision unless decision_result + end + end end + + # If no holdout decision, check if the feature is being experiment on and whether the user is bucketed into the experiment + if decision_result.nil? || decision_result.decision.nil? + decision_result_exp = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options) + if decision_result + decision_result.reasons.push(*decision_result_exp.reasons) + decision_result.decision = decision_result_exp.decision if decision_result_exp.decision + decision_result.error = decision_result_exp.error if decision_result_exp.error + else + decision_result = decision_result_exp + end + + # Only process rollout if no experiment decision was found and no error + if decision_result.decision.nil? && !decision_result.error + decision_result_rollout = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision + decision_result.decision = decision_result_rollout.decision + decision_result.reasons.push(*decision_result_rollout.reasons) + end + end + decisions << decision_result end user_profile_tracker&.save_user_profile From 9b513f2a0b9a22dcf277c6168363399919fcad92 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Mon, 8 Dec 2025 15:24:14 -0600 Subject: [PATCH 05/11] Update optimizely --- lib/optimizely.rb | 2 +- lib/optimizely/decision_service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index c64ea794..3d787c88 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -220,7 +220,7 @@ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide decision_source = decision.source end - if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions) + if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || decision_source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] || config.send_flag_decisions) send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes, decision&.cmab_uuid) decision_event_dispatched = true end diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index 451999e9..deaeec2a 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -327,7 +327,7 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context, break else # User didn't bucket into this holdout, try next or continue to experiments - decision_result = holdout_decision unless decision_result + decision_result ||= holdout_decision end end end From 8108c7313482f3aaf1252c5081efbbad719537c5 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 9 Dec 2025 16:23:21 -0600 Subject: [PATCH 06/11] Fix unit tests --- .../config/datafile_project_config.rb | 3 +++ spec/config/datafile_project_config_spec.rb | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 7d7c232d..b7af1c78 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -123,6 +123,9 @@ def initialize(datafile, logger, error_handler) @holdouts.each do |holdout| next unless holdout['status'] == 'Running' + # Ensure holdout has layerId field (holdouts don't have campaigns) + holdout['layerId'] ||= '' + @holdout_id_map[holdout['id']] = holdout included_flags = holdout['includedFlags'] || [] diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index 84dd4509..775028a9 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1250,7 +1250,8 @@ end it 'should return global holdouts that do not exclude the flag' do - holdouts = config_with_holdouts.get_holdouts_for_flag('multi_variate_feature') + multi_variate_feature_id = '155559' + holdouts = config_with_holdouts.get_holdouts_for_flag(multi_variate_feature_id) expect(holdouts.length).to eq(3) global_holdout = holdouts.find { |h| h['key'] == 'global_holdout' } @@ -1263,7 +1264,8 @@ end it 'should not return global holdouts that exclude the flag' do - holdouts = config_with_holdouts.get_holdouts_for_flag('boolean_single_variable_feature') + boolean_feature_id = '155554' + holdouts = config_with_holdouts.get_holdouts_for_flag(boolean_feature_id) expect(holdouts.length).to eq(1) global_holdout = holdouts.find { |h| h['key'] == 'global_holdout' } @@ -1271,14 +1273,16 @@ end it 'should cache results for subsequent calls' do - holdouts1 = config_with_holdouts.get_holdouts_for_flag('multi_variate_feature') - holdouts2 = config_with_holdouts.get_holdouts_for_flag('multi_variate_feature') + multi_variate_feature_id = '155559' + holdouts1 = config_with_holdouts.get_holdouts_for_flag(multi_variate_feature_id) + holdouts2 = config_with_holdouts.get_holdouts_for_flag(multi_variate_feature_id) expect(holdouts1).to equal(holdouts2) expect(holdouts1.length).to eq(3) end it 'should return only global holdouts for flags not specifically targeted' do - holdouts = config_with_holdouts.get_holdouts_for_flag('string_single_variable_feature') + string_feature_id = '594060' + holdouts = config_with_holdouts.get_holdouts_for_flag(string_feature_id) # Should only include global holdout (not excluded and no specific targeting) expect(holdouts.length).to eq(2) @@ -1394,7 +1398,7 @@ it 'should properly categorize holdouts during initialization' do expect(config_with_complex_holdouts.holdout_id_map.keys).to contain_exactly('global_holdout', 'specific_holdout') - expect(config_with_complex_holdouts.global_holdouts.keys).to contain_exactly('global_holdout') + expect(config_with_complex_holdouts.global_holdouts.map { |h| h['id'] }).to contain_exactly('global_holdout') # Use the correct feature flag IDs boolean_feature_id = '155554' @@ -1416,7 +1420,7 @@ it 'should only process running holdouts during initialization' do expect(config_with_complex_holdouts.holdout_id_map['inactive_holdout']).to be_nil - expect(config_with_complex_holdouts.global_holdouts['inactive_holdout']).to be_nil + expect(config_with_complex_holdouts.global_holdouts.find { |h| h['id'] == 'inactive_holdout' }).to be_nil boolean_feature_id = '155554' included_for_boolean = config_with_complex_holdouts.included_holdouts[boolean_feature_id] @@ -1594,7 +1598,7 @@ it 'should handle mixed holdout configurations' do # Verify the config has properly categorized holdouts - expect(config_with_holdouts.global_holdouts).to be_a(Hash) + expect(config_with_holdouts.global_holdouts).to be_a(Array) expect(config_with_holdouts.included_holdouts).to be_a(Hash) expect(config_with_holdouts.excluded_holdouts).to be_a(Hash) end @@ -1774,7 +1778,7 @@ config = Optimizely::DatafileProjectConfig.new(config_json, logger, error_handler) # Should treat as global holdout - expect(config.global_holdouts['holdout_nil']).not_to be_nil + expect(config.global_holdouts.find { |h| h['id'] == 'holdout_nil' }).not_to be_nil end it 'should only include running holdouts in maps' do From 7584ee714ce67d50bc96cf8c19c91b10a6cd7c05 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 9 Dec 2025 17:28:17 -0600 Subject: [PATCH 07/11] Fix unit tests --- lib/optimizely/config/datafile_project_config.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index b7af1c78..9aa3512d 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -644,6 +644,10 @@ def get_holdouts_for_flag(flag_id) return [] if @holdouts.nil? || @holdouts.empty? + # Validate that the flag exists in the datafile + flag_exists = @feature_flags.any? { |flag| flag['id'] == flag_id } + return [] unless flag_exists + # Check cache and return persistent holdouts return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id) From 646032bb931190a43728676177807af48cb97c65 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Tue, 9 Dec 2025 18:49:22 -0600 Subject: [PATCH 08/11] Fix unit test failures --- .../config/datafile_project_config.rb | 12 ++++++--- spec/config/datafile_project_config_spec.rb | 26 ++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index 9aa3512d..cfb67f24 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -644,12 +644,16 @@ def get_holdouts_for_flag(flag_id) return [] if @holdouts.nil? || @holdouts.empty? + # Check cache first (before validation, so we cache the validation result too) + return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id) + # Validate that the flag exists in the datafile flag_exists = @feature_flags.any? { |flag| flag['id'] == flag_id } - return [] unless flag_exists - - # Check cache and return persistent holdouts - return @flag_holdouts_map[flag_id] if @flag_holdouts_map.key?(flag_id) + unless flag_exists + # Cache the empty result for non-existent flags + @flag_holdouts_map[flag_id] = [] + return [] + end # Prioritize global holdouts first excluded = @excluded_holdouts[flag_id] || [] diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb index 775028a9..ea47fe6f 100644 --- a/spec/config/datafile_project_config_spec.rb +++ b/spec/config/datafile_project_config_spec.rb @@ -1281,7 +1281,7 @@ end it 'should return only global holdouts for flags not specifically targeted' do - string_feature_id = '594060' + string_feature_id = '155557' holdouts = config_with_holdouts.get_holdouts_for_flag(string_feature_id) # Should only include global holdout (not excluded and no specific targeting) @@ -1366,8 +1366,8 @@ # Use the correct feature flag IDs from the debug output boolean_feature_id = '155554' multi_variate_feature_id = '155559' - empty_feature_id = '594032' - string_feature_id = '594060' + empty_feature_id = '155564' + string_feature_id = '155557' config_body_with_holdouts['holdouts'] = [ { @@ -1403,8 +1403,8 @@ # Use the correct feature flag IDs boolean_feature_id = '155554' multi_variate_feature_id = '155559' - empty_feature_id = '594032' - string_feature_id = '594060' + empty_feature_id = '155564' + string_feature_id = '155557' expect(config_with_complex_holdouts.included_holdouts[multi_variate_feature_id]).not_to be_nil expect(config_with_complex_holdouts.included_holdouts[multi_variate_feature_id]).not_to be_empty @@ -1474,7 +1474,7 @@ feature_flag = config_with_holdouts.feature_flag_key_map['boolean_feature'] expect(feature_flag).not_to be_nil - holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag('boolean_feature') + holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) expect(holdouts_for_flag).to be_an(Array) end end @@ -1485,7 +1485,7 @@ feature_flag = config_with_holdouts.feature_flag_key_map['boolean_feature'] if feature_flag - holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag('boolean_feature') + holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) # Should not include holdouts that exclude this flag excluded_holdout = holdouts_for_flag.find { |h| h['key'] == 'excluded_holdout' } @@ -1497,7 +1497,7 @@ feature_flag = config_with_holdouts.feature_flag_key_map['boolean_feature'] expect(feature_flag).not_to be_nil - holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag('boolean_feature') + holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) expect(holdouts_for_flag).to be_an(Array) end end @@ -1523,8 +1523,9 @@ end it 'should properly cache holdout lookups' do - holdouts_1 = config_with_holdouts.get_holdouts_for_flag('boolean_feature') - holdouts_2 = config_with_holdouts.get_holdouts_for_flag('boolean_feature') + feature_flag = config_with_holdouts.feature_flag_key_map['boolean_feature'] + holdouts_1 = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) + holdouts_2 = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) expect(holdouts_1).to equal(holdouts_2) end @@ -1703,7 +1704,7 @@ feature_flag = config_with_holdouts.feature_flag_key_map['boolean_feature'] expect(feature_flag).not_to be_nil - holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag('boolean_feature') + holdouts_for_flag = config_with_holdouts.get_holdouts_for_flag(feature_flag['id']) holdouts_for_flag.each do |holdout| # Each holdout should have necessary info for decision reasoning @@ -1758,7 +1759,8 @@ error_handler ) - holdouts_for_flag = config_without_holdouts.get_holdouts_for_flag('boolean_feature') + feature_flag = config_without_holdouts.feature_flag_key_map['boolean_feature'] + holdouts_for_flag = config_without_holdouts.get_holdouts_for_flag(feature_flag['id']) expect(holdouts_for_flag).to eq([]) end From 2e21a7e2134ddac647e96ce5734f8ffbdf3e3e7d Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 10 Dec 2025 10:25:26 -0600 Subject: [PATCH 09/11] Clear the duplicate code --- lib/optimizely/decision_service.rb | 42 ++---------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index deaeec2a..6d7b597e 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -312,46 +312,8 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context, decisions = [] feature_flags.each do |feature_flag| - # Check holdouts first - holdouts = project_config.get_holdouts_for_flag(feature_flag['id']) - decision_result = nil - - if holdouts && !holdouts.empty? - # Check each holdout - holdouts.each do |holdout| - holdout_decision = get_variation_for_holdout(holdout, user_context, project_config) - - if holdout_decision.decision - # User is bucketed into a holdout - decision_result = holdout_decision - break - else - # User didn't bucket into this holdout, try next or continue to experiments - decision_result ||= holdout_decision - end - end - end - - # If no holdout decision, check if the feature is being experiment on and whether the user is bucketed into the experiment - if decision_result.nil? || decision_result.decision.nil? - decision_result_exp = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options) - if decision_result - decision_result.reasons.push(*decision_result_exp.reasons) - decision_result.decision = decision_result_exp.decision if decision_result_exp.decision - decision_result.error = decision_result_exp.error if decision_result_exp.error - else - decision_result = decision_result_exp - end - - # Only process rollout if no experiment decision was found and no error - if decision_result.decision.nil? && !decision_result.error - decision_result_rollout = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision - decision_result.decision = decision_result_rollout.decision - decision_result.reasons.push(*decision_result_rollout.reasons) - end - end - - decisions << decision_result + decision = get_decision_for_flag(feature_flag, user_context, project_config, decide_options, user_profile_tracker) + decisions << decision end user_profile_tracker&.save_user_profile decisions From d41693e1967a45ec8cf60ea154c9594b37d7b005 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 10 Dec 2025 10:47:00 -0600 Subject: [PATCH 10/11] Fix unit and FSC errors --- lib/optimizely/decision_service.rb | 3 +++ spec/decision_service_spec.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index 6d7b597e..051a8b66 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -214,6 +214,9 @@ def get_decision_for_flag(feature_flag, user_context, project_config, decide_opt return DecisionResult.new(experiment_decision.decision, experiment_decision.error, reasons) if experiment_decision.decision + # If there's an error (e.g., CMAB error), return immediately without falling back to rollout + return DecisionResult.new(nil, experiment_decision.error, reasons) if experiment_decision.error + # Check if the feature flag has a rollout and the user is bucketed into that rollout rollout_decision = get_variation_for_feature_rollout(project_config, feature_flag, user_context) reasons.push(*rollout_decision.reasons) diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb index fe2cc881..a9ece718 100644 --- a/spec/decision_service_spec.rb +++ b/spec/decision_service_spec.rb @@ -775,7 +775,7 @@ decision_result = decision_service.get_variation_for_feature(config, feature_flag, user_context) expect(decision_result.decision).to eq(expected_decision) - expect(decision_result.reasons).to eq([]) + expect(decision_result.reasons).to eq(["The user 'test_user' is bucketed into a rollout for feature flag 'string_single_variable_feature'."]) end end From 25168623e75609853db0eda53469d263104db4f7 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 10 Dec 2025 11:06:19 -0600 Subject: [PATCH 11/11] Fix test --- spec/decision_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb index a9ece718..30ad7d2e 100644 --- a/spec/decision_service_spec.rb +++ b/spec/decision_service_spec.rb @@ -775,7 +775,7 @@ decision_result = decision_service.get_variation_for_feature(config, feature_flag, user_context) expect(decision_result.decision).to eq(expected_decision) - expect(decision_result.reasons).to eq(["The user 'test_user' is bucketed into a rollout for feature flag 'string_single_variable_feature'."]) + expect(decision_result.reasons).to eq(["The user 'user_1' is bucketed into a rollout for feature flag 'string_single_variable_feature'."]) end end