11/*
2- * Copyright 2017-2022, 2024 Optimizely
2+ * Copyright 2017-2022, 2024, 2026 Optimizely
33*
44* Licensed under the Apache License, Version 2.0 (the "License");
55* you may not use this file except in compliance with the License.
@@ -686,7 +686,6 @@ ProjectConfig config
686686 reasons ) ;
687687 }
688688
689- // Check local holdouts targeting this specific delivery rule (FSSDK-12369)
690689 var localHoldoutResult = EvaluateLocalHoldouts ( rule . Id , user , config ) ;
691690 reasons += localHoldoutResult . DecisionReasons ;
692691 if ( localHoldoutResult . ResultObject != null )
@@ -814,7 +813,6 @@ public virtual Result<FeatureDecision> GetVariationForFeatureExperiment(
814813 }
815814 else
816815 {
817- // Check local holdouts targeting this specific experiment rule (FSSDK-12369)
818816 var localHoldoutResult = EvaluateLocalHoldouts ( experiment . Id , user , config ) ;
819817 reasons += localHoldoutResult . DecisionReasons ;
820818 if ( localHoldoutResult . ResultObject != null )
@@ -916,7 +914,8 @@ public virtual Result<FeatureDecision> GetDecisionForFlag(
916914
917915 var userId = user . GetUserId ( ) ;
918916
919- // Check global holdouts first (highest priority — evaluated at flag level, before any rules)
917+ FeatureDecision globalHoldoutDecision = null ;
918+ Holdout matchedHoldout = null ;
920919 var globalHoldouts = projectConfig . GetGlobalHoldouts ( ) ;
921920 foreach ( var holdout in globalHoldouts )
922921 {
@@ -925,24 +924,50 @@ public virtual Result<FeatureDecision> GetDecisionForFlag(
925924
926925 if ( holdoutDecision . ResultObject != null )
927926 {
928- Logger . Log ( LogLevel . INFO ,
929- reasons . AddInfo (
930- $ "The user \" { userId } \" is bucketed into holdout \" { holdout . Key } \" for feature flag \" { featureFlag . Key } \" .") ) ;
931- return Result < FeatureDecision > . NewResult ( holdoutDecision . ResultObject , reasons ) ;
927+ if ( holdout . ExcludeTargetedDeliveries )
928+ {
929+ Logger . Log ( LogLevel . INFO ,
930+ reasons . AddInfo (
931+ $ "User \" { userId } \" is in holdout \" { holdout . Key } \" which excludes targeted deliveries. Targeted delivery rules will be evaluated normally.") ) ;
932+ globalHoldoutDecision = holdoutDecision . ResultObject ;
933+ matchedHoldout = holdout ;
934+ }
935+ else
936+ {
937+ Logger . Log ( LogLevel . INFO ,
938+ reasons . AddInfo (
939+ $ "The user \" { userId } \" is bucketed into holdout \" { holdout . Key } \" for feature flag \" { featureFlag . Key } \" .") ) ;
940+ return Result < FeatureDecision > . NewResult ( holdoutDecision . ResultObject , reasons ) ;
941+ }
942+ break ;
932943 }
933944 }
934945
935- // Check if the feature flag has an experiment and the user is bucketed into that experiment.
936- var experimentDecision = GetVariationForFeatureExperiment ( featureFlag , user ,
937- filteredAttributes , projectConfig , options , userProfileTracker ) ;
938- reasons += experimentDecision . DecisionReasons ;
946+ if ( globalHoldoutDecision == null )
947+ {
948+ var experimentDecision = GetVariationForFeatureExperiment ( featureFlag , user ,
949+ filteredAttributes , projectConfig , options , userProfileTracker ) ;
950+ reasons += experimentDecision . DecisionReasons ;
939951
940- if ( experimentDecision . ResultObject != null )
952+ if ( experimentDecision . ResultObject != null )
953+ {
954+ return Result < FeatureDecision > . NewResult ( experimentDecision . ResultObject , reasons ) ;
955+ }
956+ }
957+ else
941958 {
942- return Result < FeatureDecision > . NewResult ( experimentDecision . ResultObject , reasons ) ;
959+ Logger . Log ( LogLevel . INFO ,
960+ reasons . AddInfo (
961+ $ "Skipping experiment rules for user \" { userId } \" — holdout applies to A/B and MAB rules.") ) ;
962+
963+ if ( matchedHoldout != null && matchedHoldout . ExcludeTargetedDeliveries )
964+ {
965+ Logger . Log ( LogLevel . INFO ,
966+ reasons . AddInfo (
967+ $ "Holdout \" { matchedHoldout . Key } \" has excludeTargetedDeliveries enabled, continuing to rollout evaluation.") ) ;
968+ }
943969 }
944970
945- // Check if the feature flag has rollout and the user is bucketed into one of its rules.
946971 var rolloutDecision = GetVariationForFeatureRollout ( featureFlag , user , projectConfig ) ;
947972 reasons += rolloutDecision . DecisionReasons ;
948973
@@ -951,17 +976,29 @@ public virtual Result<FeatureDecision> GetDecisionForFlag(
951976 Logger . Log ( LogLevel . INFO ,
952977 reasons . AddInfo (
953978 $ "The user \" { userId } \" is bucketed into a rollout for feature flag \" { featureFlag . Key } \" .") ) ;
979+ if ( globalHoldoutDecision != null )
980+ {
981+ rolloutDecision . ResultObject . HoldoutDecision = globalHoldoutDecision ;
982+ }
954983 return Result < FeatureDecision > . NewResult ( rolloutDecision . ResultObject , reasons ) ;
955984 }
956- else
985+
986+ if ( globalHoldoutDecision != null )
957987 {
988+ var nullDecision = new FeatureDecision ( null , null , FeatureDecision . DECISION_SOURCE_ROLLOUT ) ;
989+ nullDecision . HoldoutDecision = globalHoldoutDecision ;
958990 Logger . Log ( LogLevel . INFO ,
959991 reasons . AddInfo (
960- $ "The user \" { userId } \" is not bucketed into a rollout for feature flag \" { featureFlag . Key } \" .") ) ;
961- return Result < FeatureDecision > . NewResult (
962- new FeatureDecision ( null , null , FeatureDecision . DECISION_SOURCE_ROLLOUT ) ,
963- reasons ) ;
992+ $ "The user \" { userId } \" is not bucketed into any targeted delivery for feature flag \" { featureFlag . Key } \" . Holdout impression will be sent but holdout variation is not applied as fallback.") ) ;
993+ return Result < FeatureDecision > . NewResult ( nullDecision , reasons ) ;
964994 }
995+
996+ Logger . Log ( LogLevel . INFO ,
997+ reasons . AddInfo (
998+ $ "The user \" { userId } \" is not bucketed into a rollout for feature flag \" { featureFlag . Key } \" .") ) ;
999+ return Result < FeatureDecision > . NewResult (
1000+ new FeatureDecision ( null , null , FeatureDecision . DECISION_SOURCE_ROLLOUT ) ,
1001+ reasons ) ;
9651002 }
9661003
9671004 public virtual List < Result < FeatureDecision > > GetVariationsForFeatureList (
0 commit comments