@@ -240,10 +240,22 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non
240
240
241
241
List <DecisionResponse <FeatureDecision >> decisions = new ArrayList <>();
242
242
243
- for (FeatureFlag featureFlag : featureFlags ) {
243
+ flagLoop : for (FeatureFlag featureFlag : featureFlags ) {
244
244
DecisionReasons reasons = DefaultDecisionReasons .newInstance ();
245
245
reasons .merge (upsReasons );
246
246
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
+
247
259
DecisionResponse <FeatureDecision > decisionVariationResponse = getVariationFromExperiment (projectConfig , featureFlag , user , options , userProfileTracker );
248
260
reasons .merge (decisionVariationResponse .getReasons ());
249
261
@@ -419,6 +431,50 @@ DecisionResponse<Variation> getWhitelistedVariation(@Nonnull Experiment experime
419
431
return new DecisionResponse (null , reasons );
420
432
}
421
433
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
+
422
478
423
479
// TODO: Logically, it makes sense to move this method to UserProfileTracker. But some tests are also calling this
424
480
// method, requiring us to refactor those tests as well. We'll look to refactor this later.
0 commit comments