@@ -93,7 +93,10 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
9393 holdouts_data : list [types .HoldoutDict ] = config .get ('holdouts' , [])
9494 self .holdouts : list [entities .Holdout ] = []
9595 self .holdout_id_map : dict [str , entities .Holdout ] = {}
96- self .flag_holdouts_map : dict [str , list [entities .Holdout ]] = {}
96+ # Global holdouts (includedRules is None) — evaluated at flag level before any rule
97+ self .global_holdouts : list [entities .Holdout ] = []
98+ # Rule-level holdouts — map from rule ID to holdouts targeting that rule
99+ self .rule_holdouts_map : dict [str , list [entities .Holdout ]] = {}
97100
98101 # Convert holdout dicts to Holdout entities
99102 for holdout_data in holdouts_data :
@@ -108,6 +111,16 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
108111 # Map by ID for quick lookup
109112 self .holdout_id_map [holdout .id ] = holdout
110113
114+ # Classify holdout as global or local based on includedRules
115+ if holdout .is_global :
116+ self .global_holdouts .append (holdout )
117+ else :
118+ # Local holdout — register for each targeted rule ID
119+ for rule_id in (holdout .included_rules or []):
120+ if rule_id not in self .rule_holdouts_map :
121+ self .rule_holdouts_map [rule_id ] = []
122+ self .rule_holdouts_map [rule_id ].append (holdout )
123+
111124 # Utility maps for quick lookup
112125 self .group_id_map : dict [str , entities .Group ] = self ._generate_key_map (self .groups , 'id' , entities .Group )
113126 self .experiment_id_map : dict [str , entities .Experiment ] = self ._generate_key_map (
@@ -240,11 +253,6 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
240253 everyone_else_variation .variables , 'id' , entities .Variation .VariableUsage
241254 )
242255
243- # Map all running holdouts to this flag
244- applicable_holdouts = list (self .holdout_id_map .values ())
245- if applicable_holdouts :
246- self .flag_holdouts_map [feature .key ] = applicable_holdouts
247-
248256 rollout = None if len (feature .rolloutId ) == 0 else self .rollout_id_map [feature .rolloutId ]
249257 if rollout :
250258 for exp in rollout .experiments :
@@ -878,19 +886,30 @@ def get_flag_variation(
878886
879887 return None
880888
881- def get_holdouts_for_flag (self , flag_key : str ) -> list [entities .Holdout ]:
882- """ Helper method to get holdouts from an applied feature flag .
889+ def get_global_holdouts (self ) -> list [entities .Holdout ]:
890+ """Return all global holdouts (includedRules is None) .
883891
884- Args:
885- flag_key: Key of the feature flag.
892+ Global holdouts are evaluated at flag level before any rule is checked.
886893
887894 Returns:
888- The holdouts that apply for a specific flag as Holdout entity objects .
895+ List of global Holdout entities that are currently running .
889896 """
890- if not self .holdouts :
891- return []
897+ return self .global_holdouts
898+
899+ def get_holdouts_for_rule (self , rule_id : str ) -> list [entities .Holdout ]:
900+ """Return local holdouts that target a specific rule.
892901
893- return self .flag_holdouts_map .get (flag_key , [])
902+ Local holdouts are evaluated per-rule, before the rule's audience and
903+ traffic allocation checks. A rule ID not present in any holdout's
904+ includedRules simply returns an empty list — silently skipped.
905+
906+ Args:
907+ rule_id: The experiment or delivery rule ID to look up.
908+
909+ Returns:
910+ List of local Holdout entities targeting the given rule ID.
911+ """
912+ return self .rule_holdouts_map .get (rule_id , [])
894913
895914 def get_holdout (self , holdout_id : str ) -> Optional [entities .Holdout ]:
896915 """ Helper method to get holdout from holdout ID.
0 commit comments