@@ -93,7 +93,12 @@ 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+ # Legacy flag-level map kept for backward compatibility
9697 self .flag_holdouts_map : dict [str , list [entities .Holdout ]] = {}
98+ # Global holdouts (includedRules is None) — evaluated at flag level
99+ self .global_holdouts : list [entities .Holdout ] = []
100+ # Rule-level holdouts map: rule_id -> [Holdout] for local holdouts
101+ self .rule_holdouts_map : dict [str , list [entities .Holdout ]] = {}
97102
98103 # Convert holdout dicts to Holdout entities
99104 for holdout_data in holdouts_data :
@@ -108,6 +113,16 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
108113 # Map by ID for quick lookup
109114 self .holdout_id_map [holdout .id ] = holdout
110115
116+ # Categorize holdout as global or local
117+ if holdout .is_global :
118+ self .global_holdouts .append (holdout )
119+ else :
120+ # Local holdout: map each included rule ID to this holdout
121+ for rule_id in holdout .included_rules or []:
122+ if rule_id not in self .rule_holdouts_map :
123+ self .rule_holdouts_map [rule_id ] = []
124+ self .rule_holdouts_map [rule_id ].append (holdout )
125+
111126 # Utility maps for quick lookup
112127 self .group_id_map : dict [str , entities .Group ] = self ._generate_key_map (self .groups , 'id' , entities .Group )
113128 self .experiment_id_map : dict [str , entities .Experiment ] = self ._generate_key_map (
@@ -240,10 +255,9 @@ def __init__(self, datafile: str | bytes, logger: Logger, error_handler: Any):
240255 everyone_else_variation .variables , 'id' , entities .Variation .VariableUsage
241256 )
242257
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
258+ # Map global holdouts to this flag (for legacy flag-level access)
259+ if self .global_holdouts :
260+ self .flag_holdouts_map [feature .key ] = list (self .global_holdouts )
247261
248262 rollout = None if len (feature .rolloutId ) == 0 else self .rollout_id_map [feature .rolloutId ]
249263 if rollout :
@@ -881,17 +895,42 @@ def get_flag_variation(
881895 def get_holdouts_for_flag (self , flag_key : str ) -> list [entities .Holdout ]:
882896 """ Helper method to get holdouts from an applied feature flag.
883897
898+ Returns global holdouts for the given flag (backward-compatible).
899+
884900 Args:
885901 flag_key: Key of the feature flag.
886902
887903 Returns:
888- The holdouts that apply for a specific flag as Holdout entity objects.
904+ The global holdouts that apply for a specific flag as Holdout entity objects.
889905 """
890906 if not self .holdouts :
891907 return []
892908
893909 return self .flag_holdouts_map .get (flag_key , [])
894910
911+ def get_global_holdouts (self ) -> list [entities .Holdout ]:
912+ """Return all global holdouts (holdouts with includedRules == None).
913+
914+ Global holdouts are evaluated at flag level before forced decisions.
915+
916+ Returns:
917+ List of global Holdout entities.
918+ """
919+ return self .global_holdouts
920+
921+ def get_holdouts_for_rule (self , rule_id : str ) -> list [entities .Holdout ]:
922+ """Return local holdouts targeting a specific rule.
923+
924+ Local holdouts are evaluated per-rule before audience and traffic checks.
925+
926+ Args:
927+ rule_id: The experiment or delivery rule ID to look up.
928+
929+ Returns:
930+ List of Holdout entities targeting the given rule ID.
931+ """
932+ return self .rule_holdouts_map .get (rule_id , [])
933+
895934 def get_holdout (self , holdout_id : str ) -> Optional [entities .Holdout ]:
896935 """ Helper method to get holdout from holdout ID.
897936
0 commit comments