Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,19 @@ private OptimizelyDecision createOptimizelyDecision(
cmabUuid);
}

if (flagDecision.holdoutDecision != null && !allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) {
decisionEventDispatched = sendImpression(
projectConfig,
flagDecision.holdoutDecision.experiment,
userId,
copiedAttributes,
flagDecision.holdoutDecision.variation,
flagKey,
flagDecision.holdoutDecision.decisionSource != null ? flagDecision.holdoutDecision.decisionSource.toString() : FeatureDecision.DecisionSource.HOLDOUT.toString(),
flagDecision.holdoutDecision.variation != null && flagDecision.holdoutDecision.variation.getFeatureEnabled(),
null) || decisionEventDispatched;
}

DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()
.withUserId(userId)
.withAttributes(copiedAttributes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2017-2022, 2024, Optimizely, Inc. and contributors *
* Copyright 2017-2022, 2024, 2026, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -328,43 +328,66 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non

// Evaluate global holdouts at flag level (before any rules are iterated)
List<Holdout> globalHoldouts = projectConfig.getGlobalHoldouts();
FeatureDecision globalHoldoutDecision = null;
boolean excludeTargetedDeliveries = false;
if (!globalHoldouts.isEmpty()) {
for (Holdout holdout : globalHoldouts) {
DecisionResponse<Variation> holdoutDecision = getVariationForHoldout(holdout, user, projectConfig);
reasons.merge(holdoutDecision.getReasons());
if (holdoutDecision.getResult() != null) {
decisions.add(new DecisionResponse<>(new FeatureDecision(holdout, holdoutDecision.getResult(), FeatureDecision.DecisionSource.HOLDOUT), reasons));
continue flagLoop;
globalHoldoutDecision = new FeatureDecision(holdout, holdoutDecision.getResult(), FeatureDecision.DecisionSource.HOLDOUT);
excludeTargetedDeliveries = holdout.isExcludeTargetedDeliveries();
break;
}
}
}

DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker, decisionPath);
reasons.merge(decisionVariationResponse.getReasons());
if (globalHoldoutDecision != null && !excludeTargetedDeliveries) {
decisions.add(new DecisionResponse<>(globalHoldoutDecision, reasons));
continue flagLoop;
}

if (globalHoldoutDecision != null && excludeTargetedDeliveries) {
reasons.addInfo("Holdout '%s' has excludeTargetedDeliveries enabled, continuing to rollout evaluation.", globalHoldoutDecision.experiment.getKey());
}

FeatureDecision decision = decisionVariationResponse.getResult();
boolean error = decisionVariationResponse.isError();
if (globalHoldoutDecision == null) {
DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker, decisionPath);
reasons.merge(decisionVariationResponse.getReasons());

if (decision != null) {
decisions.add(new DecisionResponse(decision, reasons, error, decision.cmabUuid));
continue;
FeatureDecision decision = decisionVariationResponse.getResult();
boolean error = decisionVariationResponse.isError();

if (decision != null) {
decisions.add(new DecisionResponse(decision, reasons, error, decision.cmabUuid));
continue flagLoop;
}
}

DecisionResponse<FeatureDecision> decisionFeatureResponse = getVariationForFeatureInRollout(featureFlag, user, projectConfig);
reasons.merge(decisionFeatureResponse.getReasons());
decision = decisionFeatureResponse.getResult();
FeatureDecision decision = decisionFeatureResponse.getResult();

String message;
if (decision.variation == null) {
message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".",
if (decision != null && decision.variation != null) {
if (globalHoldoutDecision != null) {
decision.setHoldoutDecision(globalHoldoutDecision);
}
String message = reasons.addInfo("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".",
user.getUserId(), featureFlag.getKey());
logger.info(message);
decisions.add(new DecisionResponse(decision, reasons));
} else {
message = reasons.addInfo("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".",
if (globalHoldoutDecision != null) {
if (decision == null) {
decision = new FeatureDecision(null, null, null);
}
decision.setHoldoutDecision(globalHoldoutDecision);
}
String message = reasons.addInfo("The user \"%s\" was not bucketed into a rollout for feature flag \"%s\".",
user.getUserId(), featureFlag.getKey());
logger.info(message);
decisions.add(new DecisionResponse(decision, reasons));
}
logger.info(message);

decisions.add(new DecisionResponse(decision, reasons));
}

if (userProfileService != null && !ignoreUPS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public class FeatureDecision {
@Nullable
public String cmabUuid;

@Nullable
public FeatureDecision holdoutDecision;

public void setHoldoutDecision(@Nullable FeatureDecision holdoutDecision) {
this.holdoutDecision = holdoutDecision;
}

public enum DecisionSource {
FEATURE_TEST("feature-test"),
ROLLOUT("rollout"),
Expand Down
15 changes: 12 additions & 3 deletions core-api/src/main/java/com/optimizely/ab/config/Holdout.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public class Holdout implements ExperimentCore {
@Nullable
private final List<String> includedRules;

private final boolean excludeTargetedDeliveries;

private final Map<String, Variation> variationKeyToVariationMap;
private final Map<String, Variation> variationIdToVariationMap;
// Not necessary for HO
Expand All @@ -75,7 +77,7 @@ public String toString() {

@VisibleForTesting
public Holdout(String id, String key) {
this(id, key, "Running", Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null);
this(id, key, "Running", Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null, false);
}

/**
Expand All @@ -88,7 +90,7 @@ public Holdout(@Nonnull String id,
@Nullable Condition audienceConditions,
@Nonnull List<Variation> variations,
@Nonnull List<TrafficAllocation> trafficAllocation) {
this(id, key, status, audienceIds, audienceConditions, variations, trafficAllocation, null);
this(id, key, status, audienceIds, audienceConditions, variations, trafficAllocation, null, false);
}

/**
Expand All @@ -105,7 +107,8 @@ public Holdout(@JsonProperty("id") @Nonnull String id,
@JsonProperty("audienceConditions") @Nullable Condition audienceConditions,
@JsonProperty("variations") @Nonnull List<Variation> variations,
@JsonProperty("trafficAllocation") @Nonnull List<TrafficAllocation> trafficAllocation,
@JsonProperty("includedRules") @Nullable List<String> includedRules) {
@JsonProperty("includedRules") @Nullable List<String> includedRules,
@JsonProperty("exclude_targeted_deliveries") @Nullable Boolean excludeTargetedDeliveries) {
this.id = id;
this.key = key;
this.status = status;
Expand All @@ -114,6 +117,7 @@ public Holdout(@JsonProperty("id") @Nonnull String id,
this.variations = variations;
this.trafficAllocation = trafficAllocation;
this.includedRules = includedRules;
this.excludeTargetedDeliveries = excludeTargetedDeliveries != null ? excludeTargetedDeliveries : false;
this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(this.variations);
this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(this.variations);
}
Expand Down Expand Up @@ -188,6 +192,10 @@ public List<String> getIncludedRules() {
*
* @return true if this is a global holdout, false if it is a local holdout
*/
public boolean isExcludeTargetedDeliveries() {
return excludeTargetedDeliveries;
}

public boolean isGlobal() {
return includedRules == null;
}
Expand All @@ -204,6 +212,7 @@ public String toString() {
+ ", variationKeyToVariationMap=" + variationKeyToVariationMap
+ ", trafficAllocation=" + trafficAllocation
+ ", includedRules=" + includedRules
+ ", excludeTargetedDeliveries=" + excludeTargetedDeliveries
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ private static Holdout stripIncludedRules(Holdout holdout) {
holdout.getAudienceConditions(),
holdout.getVariations(),
holdout.getTrafficAllocation(),
null
null,
holdout.isExcludeTargetedDeliveries()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
*
* Copyright 2016-2017, 2019, Optimizely and contributors
* Copyright 2016-2017, 2019, 2026, Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -212,7 +212,12 @@ static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext c
}
}

return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedRules);
boolean excludeTargetedDeliveries = false;
if (holdoutJson.has("exclude_targeted_deliveries") && !holdoutJson.get("exclude_targeted_deliveries").isJsonNull()) {
excludeTargetedDeliveries = holdoutJson.get("exclude_targeted_deliveries").getAsBoolean();
}

return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedRules, excludeTargetedDeliveries);
}

static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
*
* Copyright 2016-2022, Optimizely and contributors
* Copyright 2016-2022, 2026, Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -238,8 +238,13 @@ private List<Holdout> parseHoldouts(JSONArray holdoutJson) {
}
}

boolean excludeTargetedDeliveries = false;
if (holdoutObject.has("exclude_targeted_deliveries") && !holdoutObject.isNull("exclude_targeted_deliveries")) {
excludeTargetedDeliveries = holdoutObject.getBoolean("exclude_targeted_deliveries");
}

holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations,
trafficAllocations, includedRules));
trafficAllocations, includedRules, excludeTargetedDeliveries));
}

return holdouts;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
*
* Copyright 2016-2022, Optimizely and contributors
* Copyright 2016-2022, 2026, Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -257,8 +257,13 @@ private List<Holdout> parseHoldouts(JSONArray holdoutJson) {
}
}

boolean excludeTargetedDeliveries = false;
if (hoObject.containsKey("exclude_targeted_deliveries") && hoObject.get("exclude_targeted_deliveries") != null) {
excludeTargetedDeliveries = (Boolean) hoObject.get("exclude_targeted_deliveries");
}

holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations,
trafficAllocations, includedRules));
trafficAllocations, includedRules, excludeTargetedDeliveries));
}

return holdouts;
Expand Down
Loading
Loading