@@ -240,10 +240,22 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non
240240
241241 List <DecisionResponse <FeatureDecision >> decisions = new ArrayList <>();
242242
243- for (FeatureFlag featureFlag : featureFlags ) {
243+ flagLoop : for (FeatureFlag featureFlag : featureFlags ) {
244244 DecisionReasons reasons = DefaultDecisionReasons .newInstance ();
245245 reasons .merge (upsReasons );
246246
247+ List <Holdout > holdouts = projectConfig .getHoldoutForFlag (featureFlag .getId ());
248+ if (!holdouts .isEmpty ()) {
249+ for (Holdout holdout : holdouts ) {
250+ DecisionResponse <Variation > holdoutDecision = getVariationForHoldout (holdout , user , projectConfig );
251+ reasons .merge (holdoutDecision .getReasons ());
252+ if (holdoutDecision .getResult () != null ) {
253+ decisions .add (new DecisionResponse <>(new FeatureDecision (holdout , holdoutDecision .getResult (), FeatureDecision .DecisionSource .HOLDOUT ), reasons ));
254+ continue flagLoop ;
255+ }
256+ }
257+ }
258+
247259 DecisionResponse <FeatureDecision > decisionVariationResponse = getVariationFromExperiment (projectConfig , featureFlag , user , options , userProfileTracker );
248260 reasons .merge (decisionVariationResponse .getReasons ());
249261
@@ -419,6 +431,50 @@ DecisionResponse<Variation> getWhitelistedVariation(@Nonnull Experiment experime
419431 return new DecisionResponse (null , reasons );
420432 }
421433
434+ /**
435+ * Determines the variation for a holdout rule.
436+ *
437+ * @param holdout The holdout rule to evaluate.
438+ * @param user The user context.
439+ * @param projectConfig The current project configuration.
440+ * @return A {@link DecisionResponse} with the variation (if any) and reasons.
441+ */
442+ @ Nonnull
443+ DecisionResponse <Variation > getVariationForHoldout (@ Nonnull Holdout holdout ,
444+ @ Nonnull OptimizelyUserContext user ,
445+ @ Nonnull ProjectConfig projectConfig ) {
446+ DecisionReasons reasons = DefaultDecisionReasons .newInstance ();
447+
448+ if (!holdout .isActive ()) {
449+ String message = reasons .addInfo ("Holdout \" %s\" is not running." , holdout .getKey ());
450+ logger .info (message );
451+ return new DecisionResponse <>(null , reasons );
452+ }
453+
454+ DecisionResponse <Boolean > decisionMeetAudience = ExperimentUtils .doesUserMeetAudienceConditions (projectConfig , holdout , user , EXPERIMENT , holdout .getKey ());
455+ reasons .merge (decisionMeetAudience .getReasons ());
456+
457+ if (decisionMeetAudience .getResult ()) {
458+ String bucketingId = getBucketingId (user .getUserId (), user .getAttributes ());
459+ DecisionResponse <Variation > decisionVariation = bucketer .bucket (holdout , bucketingId , projectConfig );
460+ reasons .merge (decisionVariation .getReasons ());
461+ Variation variation = decisionVariation .getResult ();
462+
463+ if (variation != null ) {
464+ String message = reasons .addInfo ("User (%s) is in variation (%s) of holdout (%s)." , user .getUserId (), variation .getKey (), holdout .getKey ());
465+ logger .info (message );
466+ } else {
467+ String message = reasons .addInfo ("User (%s) is in no holdout variation." , user .getUserId ());
468+ logger .info (message );
469+ }
470+ return new DecisionResponse <>(variation , reasons );
471+ }
472+
473+ String message = reasons .addInfo ("User (%s) does not meet conditions for holdout (%s)." , user .getUserId (), holdout .getKey ());
474+ logger .info (message );
475+ return new DecisionResponse <>(null , reasons );
476+ }
477+
422478
423479 // TODO: Logically, it makes sense to move this method to UserProfileTracker. But some tests are also calling this
424480 // method, requiring us to refactor those tests as well. We'll look to refactor this later.
0 commit comments