@@ -38,12 +38,20 @@ public class Holdout implements ExperimentCore {
3838 private final String id ;
3939 private final String key ;
4040 private final String status ;
41-
41+
4242 private final List <String > audienceIds ;
4343 private final Condition <AudienceIdCondition > audienceConditions ;
4444 private final List <Variation > variations ;
4545 private final List <TrafficAllocation > trafficAllocation ;
4646
47+ /**
48+ * Optional list of rule IDs this holdout targets.
49+ * null = global holdout (applies to all rules across all flags)
50+ * non-null = local holdout (applies only to the specified rule IDs)
51+ */
52+ @ Nullable
53+ private final List <String > includedRules ;
54+
4755 private final Map <String , Variation > variationKeyToVariationMap ;
4856 private final Map <String , Variation > variationIdToVariationMap ;
4957 // Not necessary for HO
@@ -68,25 +76,39 @@ public String toString() {
6876
6977 @ VisibleForTesting
7078 public Holdout (String id , String key ) {
71- this (id , key , "Running" , Collections .emptyList (), null , Collections .emptyList (), Collections .emptyList ());
79+ this (id , key , "Running" , Collections .emptyList (), null , Collections .emptyList (), Collections .emptyList (), null );
80+ }
81+
82+ /**
83+ * Convenience constructor for tests that do not need includedRules (maintains backward compatibility).
84+ */
85+ public Holdout (@ Nonnull String id ,
86+ @ Nonnull String key ,
87+ @ Nonnull String status ,
88+ @ Nonnull List <String > audienceIds ,
89+ @ Nullable Condition audienceConditions ,
90+ @ Nonnull List <Variation > variations ,
91+ @ Nonnull List <TrafficAllocation > trafficAllocation ) {
92+ this (id , key , status , audienceIds , audienceConditions , variations , trafficAllocation , null );
7293 }
7394
74- // Keep only this constructor and add @JsonCreator to it
7595 @ JsonCreator
7696 public Holdout (@ JsonProperty ("id" ) @ Nonnull String id ,
7797 @ JsonProperty ("key" ) @ Nonnull String key ,
7898 @ JsonProperty ("status" ) @ Nonnull String status ,
7999 @ JsonProperty ("audienceIds" ) @ Nonnull List <String > audienceIds ,
80100 @ JsonProperty ("audienceConditions" ) @ Nullable Condition audienceConditions ,
81101 @ JsonProperty ("variations" ) @ Nonnull List <Variation > variations ,
82- @ JsonProperty ("trafficAllocation" ) @ Nonnull List <TrafficAllocation > trafficAllocation ) {
102+ @ JsonProperty ("trafficAllocation" ) @ Nonnull List <TrafficAllocation > trafficAllocation ,
103+ @ JsonProperty ("includedRules" ) @ Nullable List <String > includedRules ) {
83104 this .id = id ;
84105 this .key = key ;
85106 this .status = status ;
86107 this .audienceIds = audienceIds ;
87108 this .audienceConditions = audienceConditions ;
88109 this .variations = variations ;
89110 this .trafficAllocation = trafficAllocation ;
111+ this .includedRules = includedRules ;
90112 this .variationKeyToVariationMap = ProjectConfigUtils .generateNameMapping (this .variations );
91113 this .variationIdToVariationMap = ProjectConfigUtils .generateIdMapping (this .variations );
92114 }
@@ -135,6 +157,27 @@ public String getGroupId() {
135157 return "" ;
136158 }
137159
160+ /**
161+ * Returns the list of rule IDs this holdout is scoped to, or null if this is a global holdout.
162+ *
163+ * @return null for a global holdout; a (possibly empty) list of rule IDs for a local holdout
164+ */
165+ @ Nullable
166+ public List <String > getIncludedRules () {
167+ return includedRules ;
168+ }
169+
170+ /**
171+ * Returns {@code true} if this is a global holdout (applies to all rules across all flags).
172+ * A holdout is global when {@code includedRules} is {@code null}.
173+ * An empty list is NOT the same as global — it is a local holdout that targets no rules.
174+ *
175+ * @return true if global, false if local
176+ */
177+ public boolean isGlobal () {
178+ return includedRules == null ;
179+ }
180+
138181 public boolean isActive () {
139182 return status .equals (Holdout .HoldoutStatus .RUNNING .toString ());
140183 }
@@ -154,6 +197,7 @@ public String toString() {
154197 + ", variations=" + variations
155198 + ", variationKeyToVariationMap=" + variationKeyToVariationMap
156199 + ", trafficAllocation=" + trafficAllocation
200+ + ", includedRules=" + includedRules
157201 + '}' ;
158202 }
159203}
0 commit comments