Skip to content

Commit b7f169b

Browse files
feat: add holdout rule support in DecisionService
- Add logic to process holdout rules for feature flags - Implement a method to determine variation for a holdout rule - Create decision responses based on a holdout decision and its reasons - Update logger messages for holdout rule evaluation
1 parent 5cb4eb3 commit b7f169b

File tree

1 file changed

+57
-1
lines changed

1 file changed

+57
-1
lines changed

core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)