diff --git a/lib/optimizely.rb b/lib/optimizely.rb index c64ea794..860401e4 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 - if !decide_options.include?(OptimizelyDecideOption::DISABLE_DECISION_EVENT) && (decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions) - send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes, decision&.cmab_uuid) + # 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, campaign_id) decision_event_dispatched = true end @@ -1271,16 +1279,25 @@ 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'] + bot_filtering = attributes&.dig('$opt_bot_filtering') + filtered_attributes = bot_filtering ? {'$opt_bot_filtering' => bot_filtering} : {} + 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? 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 diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb index f1bc92e2..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) @@ -313,12 +319,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 diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb index a5ee82a9..e63fde51 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 = 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 = '' + 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