From ccc93d160c13dd91be339a8e7c485c45bfdb9c8b Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 13:37:11 -0600 Subject: [PATCH 01/11] Test FSC --- lib/optimizely/decision_service.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index f1bc92e2..62f17888 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -313,12 +313,18 @@ 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) + holdouts = project_config.get_holdouts_for_flag(feature_flag['id']) + + if holdouts && !holdouts.empty? + decision_result = get_decision_for_flag(feature_flag, user_context, project_config, decide_options, user_profile_tracker) + else + 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) + end end decisions << decision_result end From bafacf6c7caaecca5ef10c292ab615c7f6f8c2eb Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 14:05:07 -0600 Subject: [PATCH 02/11] Fix FSC --- lib/optimizely/config/datafile_project_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb index d6f1d27b..f11cff20 100644 --- a/lib/optimizely/config/datafile_project_config.rb +++ b/lib/optimizely/config/datafile_project_config.rb @@ -205,7 +205,7 @@ def initialize(datafile, logger, error_handler) applicable_holdouts << holdout unless excluded_flag_ids.include?(flag_id) end - @flag_holdouts_map[key] = applicable_holdouts unless applicable_holdouts.empty? + @flag_holdouts_map[flag_id] = applicable_holdouts unless applicable_holdouts.empty? end # Adding Holdout variations in variation id and key maps From 4481b9a1d4021fb162a081d4b9cfb377a09b8fd6 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 15:05:17 -0600 Subject: [PATCH 03/11] Add holdout --- lib/optimizely.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 306d0a8cb5eb88758ff5ba879740442432b35642 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 15:34:08 -0600 Subject: [PATCH 04/11] Another try --- lib/optimizely.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 3d787c88..d75c9b9f 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1280,7 +1280,8 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl } metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? - user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes) + event_attributes = rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] ? nil : attributes + user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, event_attributes) @event_processor.process(user_event) return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive? From 5ddd742bed76d46a25f46f80c5ff52e2b99f1d2d Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 16:20:34 -0600 Subject: [PATCH 05/11] lets try --- lib/optimizely.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index d75c9b9f..abae995d 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1275,13 +1275,12 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl flag_key: flag_key, rule_key: rule_key, rule_type: rule_type, - variation_key: variation_key, - enabled: enabled + variation_key: variation_key } + metadata[:enabled] = enabled if rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? - event_attributes = rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] ? nil : attributes - user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, event_attributes) + user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, nil) @event_processor.process(user_event) return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive? From a55ebb04ebdffb3c5254b01971d2841245147f3d Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Wed, 3 Dec 2025 16:44:15 -0600 Subject: [PATCH 06/11] Revert the change --- lib/optimizely.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index abae995d..3a18bed2 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1275,9 +1275,9 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl flag_key: flag_key, rule_key: rule_key, rule_type: rule_type, - variation_key: variation_key + variation_key: variation_key, + enabled: enabled } - metadata[:enabled] = enabled if rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, nil) From a2f5e53b4c4f94f8040a776a981a451dae4478cf Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Thu, 4 Dec 2025 10:06:30 -0600 Subject: [PATCH 07/11] Put back the attributes on user event --- lib/optimizely.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 3a18bed2..3d787c88 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1280,7 +1280,7 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl } metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? - user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, nil) + user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes) @event_processor.process(user_event) return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive? From 2f3ec506e811343afa79f78e2910a6a3155447f0 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 5 Dec 2025 10:10:30 -0600 Subject: [PATCH 08/11] Update send impression --- lib/optimizely.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 3d787c88..6d277cbc 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1271,16 +1271,27 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl variation_id = variation ? variation['id'] : '' end + # For holdout decisions, filter attributes to only include $opt_bot_filtering + filtered_attributes = attributes + if rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] + filtered_attributes = {} + if attributes && attributes.is_a?(Hash) + filtered_attributes['$opt_bot_filtering'] = attributes['$opt_bot_filtering'] if attributes['$opt_bot_filtering'] + end + end + metadata = { flag_key: flag_key, rule_key: rule_key, rule_type: rule_type, - variation_key: variation_key, - enabled: enabled + variation_key: variation_key } + + # Only include enabled field for non-holdout rule types + metadata[:enabled] = enabled unless rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? - user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, attributes) + user_event = UserEventFactory.create_impression_event(config, experiment, variation_id, metadata, user_id, filtered_attributes) @event_processor.process(user_event) return unless @notification_center.notification_count(NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE]).positive? From 867342bc670b32ada23efe451c49e077bf02ffe8 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 5 Dec 2025 10:42:20 -0600 Subject: [PATCH 09/11] Fix impression event --- lib/optimizely.rb | 18 ++++++++++++------ lib/optimizely/decision_service.rb | 10 ++++++++-- lib/optimizely/event_builder.rb | 12 ++++++++++-- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index 6d277cbc..cc1ba662 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -220,8 +220,16 @@ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide decision_source = decision.source end + # For holdout decisions, ensure campaign_id is empty string, not nil + campaign_id = nil + if decision_source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] + campaign_id = '' + elsif experiment + campaign_id = experiment['campaignId'] || experiment['layerId'] + end + 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) + send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes, decision&.cmab_uuid, campaign_id) decision_event_dispatched = true end @@ -1274,10 +1282,8 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl # For holdout decisions, filter attributes to only include $opt_bot_filtering filtered_attributes = attributes if rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] - filtered_attributes = {} - if attributes && attributes.is_a?(Hash) - filtered_attributes['$opt_bot_filtering'] = attributes['$opt_bot_filtering'] if attributes['$opt_bot_filtering'] - end + bot_filtering = attributes&.dig('$opt_bot_filtering') + filtered_attributes = bot_filtering ? { '$opt_bot_filtering' => bot_filtering } : {} end metadata = { @@ -1286,7 +1292,7 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl rule_type: rule_type, variation_key: variation_key } - + # Only include enabled field for non-holdout rule types metadata[:enabled] = enabled unless rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] metadata[:cmab_uuid] = cmab_uuid unless cmab_uuid.nil? diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index 62f17888..d2e8911d 100644 --- a/lib/optimizely/decision_service.rb +++ b/lib/optimizely/decision_service.rb @@ -196,7 +196,13 @@ def get_decision_for_flag(feature_flag, user_context, project_config, decide_opt # Check holdouts holdouts = project_config.get_holdouts_for_flag(feature_flag['id']) - holdouts.each do |holdout| + # Sort holdouts: global holdouts (empty includedFlags) should be evaluated first + sorted_holdouts = holdouts.sort_by do |holdout| + included_flags = holdout['includedFlags'] || [] + included_flags.empty? ? 0 : 1 + end + + sorted_holdouts.each do |holdout| holdout_decision = get_variation_for_holdout(holdout, user_context, project_config) reasons.push(*holdout_decision.reasons) @@ -314,7 +320,7 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context, feature_flags.each do |feature_flag| # check if the feature is being experiment on and whether the user is bucketed into the experiment holdouts = project_config.get_holdouts_for_flag(feature_flag['id']) - + if holdouts && !holdouts.empty? decision_result = get_decision_for_flag(feature_flag, user_context, project_config, decide_options, user_profile_tracker) else diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb index a5ee82a9..575c065d 100644 --- a/lib/optimizely/event_builder.rb +++ b/lib/optimizely/event_builder.rb @@ -171,14 +171,22 @@ def get_impression_params(project_config, experiment, variation_id) experiment_key = experiment['key'] experiment_id = experiment['id'] + campaign_id = experiment&.dig('campaignId') || experiment&.dig('layerId') + if decision_source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] + campaign_id = '' + entity_id = '' + else + entity_id = campaign_id + end + { decisions: [{ - campaign_id: project_config.experiment_key_map[experiment_key]['layerId'], + campaign_id: campaign_id experiment_id: experiment_id, variation_id: variation_id }], events: [{ - entity_id: project_config.experiment_key_map[experiment_key]['layerId'], + entity_id: entity_id, timestamp: create_timestamp, key: ACTIVATE_EVENT_KEY, uuid: create_uuid From d073da3c8bdf0eb7345e7891f103e7b03e4147a3 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 5 Dec 2025 11:10:41 -0600 Subject: [PATCH 10/11] Fix lint issue --- lib/optimizely.rb | 2 +- lib/optimizely/event_builder.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/optimizely.rb b/lib/optimizely.rb index cc1ba662..860401e4 100644 --- a/lib/optimizely.rb +++ b/lib/optimizely.rb @@ -1283,7 +1283,7 @@ def send_impression(config, experiment, variation_key, flag_key, rule_key, enabl filtered_attributes = attributes if rule_type == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] bot_filtering = attributes&.dig('$opt_bot_filtering') - filtered_attributes = bot_filtering ? { '$opt_bot_filtering' => bot_filtering } : {} + filtered_attributes = bot_filtering ? {'$opt_bot_filtering' => bot_filtering} : {} end metadata = { diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb index 575c065d..88e759d5 100644 --- a/lib/optimizely/event_builder.rb +++ b/lib/optimizely/event_builder.rb @@ -181,7 +181,7 @@ def get_impression_params(project_config, experiment, variation_id) { decisions: [{ - campaign_id: campaign_id + campaign_id: campaign_id, experiment_id: experiment_id, variation_id: variation_id }], From 7d9e3d6f355c28d9280499d88a3366dd292f0490 Mon Sep 17 00:00:00 2001 From: esrakartalOpt Date: Fri, 5 Dec 2025 11:20:32 -0600 Subject: [PATCH 11/11] Add back to project config to fix lint --- lib/optimizely/event_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb index 88e759d5..e63fde51 100644 --- a/lib/optimizely/event_builder.rb +++ b/lib/optimizely/event_builder.rb @@ -171,7 +171,7 @@ def get_impression_params(project_config, experiment, variation_id) experiment_key = experiment['key'] experiment_id = experiment['id'] - campaign_id = experiment&.dig('campaignId') || experiment&.dig('layerId') + campaign_id = project_config.experiment_key_map[experiment_key]['layerId'] || project_config.experiment_key_map[experiment_key]['campaignId'] if decision_source == Optimizely::DecisionService::DECISION_SOURCES['HOLDOUT'] campaign_id = '' entity_id = ''