From e19c9750f656cf1ddfd24c3cb38ebe250c5589b1 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 2 Jun 2025 20:34:41 +0600 Subject: [PATCH 01/19] Add holdout and experiment core --- .../optimizely/ab/config/ExperimentCore.java | 39 +++ .../com/optimizely/ab/config/Holdout.java | 284 ++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java create mode 100644 core-api/src/main/java/com/optimizely/ab/config/Holdout.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java new file mode 100644 index 000000000..1211fd2f9 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java @@ -0,0 +1,39 @@ +/** + * + * Copyright 2016-2019, 2021, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config; + +import com.optimizely.ab.config.audience.AndCondition; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.EmptyCondition; +import com.optimizely.ab.config.audience.NotCondition; +import com.optimizely.ab.config.audience.OrCondition; + +import java.util.List; +import java.util.Map; + +public interface ExperimentCore extends IdKeyMapped { + String getLayerId(); + String getGroupId(); + List getAudienceIds(); + Condition getAudienceConditions(); + List getVariations(); + List getTrafficAllocation(); + Map getVariationKeyToVariationMap(); + Map getVariationIdToVariationMap(); + Map getUserIdToVariationKeyMap(); +} \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java new file mode 100644 index 000000000..2e8a61f3e --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -0,0 +1,284 @@ +/** + * + * Copyright 2016-2019, 2021, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.optimizely.ab.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.optimizely.ab.annotations.VisibleForTesting; +import com.optimizely.ab.config.audience.AndCondition; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.EmptyCondition; +import com.optimizely.ab.config.audience.NotCondition; +import com.optimizely.ab.config.audience.OrCondition; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@JsonIgnoreProperties(ignoreUnknown = true) +public class Holdout implements ExperimentCore { + + private final String id; + private final String key; + private final String status; + private final String layerId; + private final String groupId; + + private final String AND = "AND"; + private final String OR = "OR"; + private final String NOT = "NOT"; + + private final List audienceIds; + private final Condition audienceConditions; + private final List variations; + private final List trafficAllocation; + + private final Map variationKeyToVariationMap; + private final Map variationIdToVariationMap; + private final Map userIdToVariationKeyMap; + + public enum HoldoutStatus { + RUNNING("Running"), + DRAFT("Draft"), + CONCLUDED("Concluded"), + ARCHIVED("Archived"); + + private final String holdoutStatus; + + HoldoutStatus(String holdoutStatus) { + this.holdoutStatus = holdoutStatus; + } + + public String toString() { + return holdoutStatus; + } + } + + @VisibleForTesting + public Holdout(String id, String key, String layerId) { + this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), ""); + } + + @JsonCreator + public Holdout(@JsonProperty("id") String id, + @JsonProperty("key") String key, + @JsonProperty("status") String status, + @JsonProperty("layerId") String layerId, + @JsonProperty("audienceIds") List audienceIds, + @JsonProperty("audienceConditions") Condition audienceConditions, + @JsonProperty("variations") List variations, + @JsonProperty("forcedVariations") Map userIdToVariationKeyMap, + @JsonProperty("trafficAllocation") List trafficAllocation) { + this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, ""); + } + + public Holdout(@Nonnull String id, + @Nonnull String key, + @Nullable String status, + @Nullable String layerId, + @Nonnull List audienceIds, + @Nullable Condition audienceConditions, + @Nonnull List variations, + @Nonnull Map userIdToVariationKeyMap, + @Nonnull List trafficAllocation, + @Nonnull String groupId) { + this.id = id; + this.key = key; + this.status = status == null ? HoldoutStatus.DRAFT.toString() : status; + this.layerId = layerId; + this.audienceIds = Collections.unmodifiableList(audienceIds); + this.audienceConditions = audienceConditions; + this.variations = Collections.unmodifiableList(variations); + this.trafficAllocation = Collections.unmodifiableList(trafficAllocation); + this.groupId = groupId; + this.userIdToVariationKeyMap = userIdToVariationKeyMap; + this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations); + this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations); + } + + public String getId() { + return id; + } + + public String getKey() { + return key; + } + + public String getStatus() { + return status; + } + + public String getLayerId() { + return layerId; + } + + public List getAudienceIds() { + return audienceIds; + } + + public Condition getAudienceConditions() { + return audienceConditions; + } + + public List getVariations() { + return variations; + } + + public Map getVariationKeyToVariationMap() { + return variationKeyToVariationMap; + } + + public Map getVariationIdToVariationMap() { + return variationIdToVariationMap; + } + + public Map getUserIdToVariationKeyMap() { + return userIdToVariationKeyMap; + } + + public List getTrafficAllocation() { + return trafficAllocation; + } + + public String getGroupId() { + return groupId; + } + + public boolean isActive() { + return status.equals(Holdout.HoldoutStatus.RUNNING.toString()); + } + + public boolean isRunning() { + return status.equals(Holdout.HoldoutStatus.RUNNING.toString()); + } + + public String serializeConditions(Map audiencesMap) { + Condition condition = this.audienceConditions; + return condition instanceof EmptyCondition ? "" : this.serialize(condition, audiencesMap); + } + + private String getNameFromAudienceId(String audienceId, Map audiencesMap) { + StringBuilder audienceName = new StringBuilder(); + if (audiencesMap != null && audiencesMap.get(audienceId) != null) { + audienceName.append("\"" + audiencesMap.get(audienceId) + "\""); + } else { + audienceName.append("\"" + audienceId + "\""); + } + return audienceName.toString(); + } + + private String getOperandOrAudienceId(Condition condition, Map audiencesMap) { + if (condition != null) { + if (condition instanceof AudienceIdCondition) { + return this.getNameFromAudienceId(condition.getOperandOrId(), audiencesMap); + } else { + return condition.getOperandOrId(); + } + } else { + return ""; + } + } + + public String serialize(Condition condition, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + List conditions; + + String operand = this.getOperandOrAudienceId(condition, audiencesMap); + switch (operand){ + case (AND): + conditions = ((AndCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (OR): + conditions = ((OrCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (NOT): + stringBuilder.append(operand + " "); + Condition notCondition = ((NotCondition) condition).getCondition(); + if (notCondition instanceof AudienceIdCondition) { + stringBuilder.append(serialize(notCondition, audiencesMap)); + } else { + stringBuilder.append("(" + serialize(notCondition, audiencesMap) + ")"); + } + break; + default: + stringBuilder.append(operand); + break; + } + + return stringBuilder.toString(); + } + + public String getNameOrNextCondition(String operand, List conditions, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + int index = 0; + if (conditions.isEmpty()) { + return ""; + } else if (conditions.size() == 1) { + return serialize(conditions.get(0), audiencesMap); + } else { + for (Condition con : conditions) { + index++; + if (index + 1 <= conditions.size()) { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append( audienceName + " "); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ") "); + } + stringBuilder.append(operand); + stringBuilder.append(" "); + } else { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append(audienceName); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ")"); + } + } + } + } + return stringBuilder.toString(); + } + + @Override + public String toString() { + return "Experiment{" + + "id='" + id + '\'' + + ", key='" + key + '\'' + + ", groupId='" + groupId + '\'' + + ", status='" + status + '\'' + + ", audienceIds=" + audienceIds + + ", audienceConditions=" + audienceConditions + + ", variations=" + variations + + ", variationKeyToVariationMap=" + variationKeyToVariationMap + + ", userIdToVariationKeyMap=" + userIdToVariationKeyMap + + ", trafficAllocation=" + trafficAllocation + + '}'; + } +} From 94cba07315520a6fed38dcc99baa2d2bfdb0dc49 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 2 Jun 2025 21:41:50 +0600 Subject: [PATCH 02/19] Seperate common logic from Experiment and Holdout --- .../com/optimizely/ab/config/Experiment.java | 98 +------------------ .../optimizely/ab/config/ExperimentCore.java | 96 ++++++++++++++++++ .../com/optimizely/ab/config/Holdout.java | 98 +------------------ 3 files changed, 98 insertions(+), 194 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/Experiment.java b/core-api/src/main/java/com/optimizely/ab/config/Experiment.java index 11530735c..4201d7db7 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Experiment.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Experiment.java @@ -34,7 +34,7 @@ */ @Immutable @JsonIgnoreProperties(ignoreUnknown = true) -public class Experiment implements IdKeyMapped { +public class Experiment implements ExperimentCore { private final String id; private final String key; @@ -42,10 +42,6 @@ public class Experiment implements IdKeyMapped { private final String layerId; private final String groupId; - private final String AND = "AND"; - private final String OR = "OR"; - private final String NOT = "NOT"; - private final List audienceIds; private final Condition audienceConditions; private final List variations; @@ -176,98 +172,6 @@ public boolean isLaunched() { return status.equals(ExperimentStatus.LAUNCHED.toString()); } - public String serializeConditions(Map audiencesMap) { - Condition condition = this.audienceConditions; - return condition instanceof EmptyCondition ? "" : this.serialize(condition, audiencesMap); - } - - private String getNameFromAudienceId(String audienceId, Map audiencesMap) { - StringBuilder audienceName = new StringBuilder(); - if (audiencesMap != null && audiencesMap.get(audienceId) != null) { - audienceName.append("\"" + audiencesMap.get(audienceId) + "\""); - } else { - audienceName.append("\"" + audienceId + "\""); - } - return audienceName.toString(); - } - - private String getOperandOrAudienceId(Condition condition, Map audiencesMap) { - if (condition != null) { - if (condition instanceof AudienceIdCondition) { - return this.getNameFromAudienceId(condition.getOperandOrId(), audiencesMap); - } else { - return condition.getOperandOrId(); - } - } else { - return ""; - } - } - - public String serialize(Condition condition, Map audiencesMap) { - StringBuilder stringBuilder = new StringBuilder(); - List conditions; - - String operand = this.getOperandOrAudienceId(condition, audiencesMap); - switch (operand){ - case (AND): - conditions = ((AndCondition) condition).getConditions(); - stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); - break; - case (OR): - conditions = ((OrCondition) condition).getConditions(); - stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); - break; - case (NOT): - stringBuilder.append(operand + " "); - Condition notCondition = ((NotCondition) condition).getCondition(); - if (notCondition instanceof AudienceIdCondition) { - stringBuilder.append(serialize(notCondition, audiencesMap)); - } else { - stringBuilder.append("(" + serialize(notCondition, audiencesMap) + ")"); - } - break; - default: - stringBuilder.append(operand); - break; - } - - return stringBuilder.toString(); - } - - public String getNameOrNextCondition(String operand, List conditions, Map audiencesMap) { - StringBuilder stringBuilder = new StringBuilder(); - int index = 0; - if (conditions.isEmpty()) { - return ""; - } else if (conditions.size() == 1) { - return serialize(conditions.get(0), audiencesMap); - } else { - for (Condition con : conditions) { - index++; - if (index + 1 <= conditions.size()) { - if (con instanceof AudienceIdCondition) { - String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), - audiencesMap); - stringBuilder.append( audienceName + " "); - } else { - stringBuilder.append("(" + serialize(con, audiencesMap) + ") "); - } - stringBuilder.append(operand); - stringBuilder.append(" "); - } else { - if (con instanceof AudienceIdCondition) { - String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), - audiencesMap); - stringBuilder.append(audienceName); - } else { - stringBuilder.append("(" + serialize(con, audiencesMap) + ")"); - } - } - } - } - return stringBuilder.toString(); - } - @Override public String toString() { return "Experiment{" + diff --git a/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java index 1211fd2f9..05af575b0 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java @@ -27,6 +27,10 @@ import java.util.Map; public interface ExperimentCore extends IdKeyMapped { + String AND = "AND"; + String OR = "OR"; + String NOT = "NOT"; + String getLayerId(); String getGroupId(); List getAudienceIds(); @@ -36,4 +40,96 @@ public interface ExperimentCore extends IdKeyMapped { Map getVariationKeyToVariationMap(); Map getVariationIdToVariationMap(); Map getUserIdToVariationKeyMap(); + + default String serializeConditions(Map audiencesMap) { + Condition condition = this.getAudienceConditions(); + return condition instanceof EmptyCondition ? "" : this.serialize(condition, audiencesMap); + } + + default String getNameFromAudienceId(String audienceId, Map audiencesMap) { + StringBuilder audienceName = new StringBuilder(); + if (audiencesMap != null && audiencesMap.get(audienceId) != null) { + audienceName.append("\"" + audiencesMap.get(audienceId) + "\""); + } else { + audienceName.append("\"" + audienceId + "\""); + } + return audienceName.toString(); + } + + default String getOperandOrAudienceId(Condition condition, Map audiencesMap) { + if (condition != null) { + if (condition instanceof AudienceIdCondition) { + return this.getNameFromAudienceId(condition.getOperandOrId(), audiencesMap); + } else { + return condition.getOperandOrId(); + } + } else { + return ""; + } + } + + default String serialize(Condition condition, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + List conditions; + + String operand = this.getOperandOrAudienceId(condition, audiencesMap); + switch (operand){ + case (AND): + conditions = ((AndCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (OR): + conditions = ((OrCondition) condition).getConditions(); + stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); + break; + case (NOT): + stringBuilder.append(operand + " "); + Condition notCondition = ((NotCondition) condition).getCondition(); + if (notCondition instanceof AudienceIdCondition) { + stringBuilder.append(serialize(notCondition, audiencesMap)); + } else { + stringBuilder.append("(" + serialize(notCondition, audiencesMap) + ")"); + } + break; + default: + stringBuilder.append(operand); + break; + } + + return stringBuilder.toString(); + } + + default String getNameOrNextCondition(String operand, List conditions, Map audiencesMap) { + StringBuilder stringBuilder = new StringBuilder(); + int index = 0; + if (conditions.isEmpty()) { + return ""; + } else if (conditions.size() == 1) { + return serialize(conditions.get(0), audiencesMap); + } else { + for (Condition con : conditions) { + index++; + if (index + 1 <= conditions.size()) { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append( audienceName + " "); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ") "); + } + stringBuilder.append(operand); + stringBuilder.append(" "); + } else { + if (con instanceof AudienceIdCondition) { + String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), + audiencesMap); + stringBuilder.append(audienceName); + } else { + stringBuilder.append("(" + serialize(con, audiencesMap) + ")"); + } + } + } + } + return stringBuilder.toString(); + } } \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index 2e8a61f3e..d2f91ca02 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -46,10 +46,6 @@ public class Holdout implements ExperimentCore { private final String layerId; private final String groupId; - private final String AND = "AND"; - private final String OR = "OR"; - private final String NOT = "NOT"; - private final List audienceIds; private final Condition audienceConditions; private final List variations; @@ -174,101 +170,9 @@ public boolean isRunning() { return status.equals(Holdout.HoldoutStatus.RUNNING.toString()); } - public String serializeConditions(Map audiencesMap) { - Condition condition = this.audienceConditions; - return condition instanceof EmptyCondition ? "" : this.serialize(condition, audiencesMap); - } - - private String getNameFromAudienceId(String audienceId, Map audiencesMap) { - StringBuilder audienceName = new StringBuilder(); - if (audiencesMap != null && audiencesMap.get(audienceId) != null) { - audienceName.append("\"" + audiencesMap.get(audienceId) + "\""); - } else { - audienceName.append("\"" + audienceId + "\""); - } - return audienceName.toString(); - } - - private String getOperandOrAudienceId(Condition condition, Map audiencesMap) { - if (condition != null) { - if (condition instanceof AudienceIdCondition) { - return this.getNameFromAudienceId(condition.getOperandOrId(), audiencesMap); - } else { - return condition.getOperandOrId(); - } - } else { - return ""; - } - } - - public String serialize(Condition condition, Map audiencesMap) { - StringBuilder stringBuilder = new StringBuilder(); - List conditions; - - String operand = this.getOperandOrAudienceId(condition, audiencesMap); - switch (operand){ - case (AND): - conditions = ((AndCondition) condition).getConditions(); - stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); - break; - case (OR): - conditions = ((OrCondition) condition).getConditions(); - stringBuilder.append(this.getNameOrNextCondition(operand, conditions, audiencesMap)); - break; - case (NOT): - stringBuilder.append(operand + " "); - Condition notCondition = ((NotCondition) condition).getCondition(); - if (notCondition instanceof AudienceIdCondition) { - stringBuilder.append(serialize(notCondition, audiencesMap)); - } else { - stringBuilder.append("(" + serialize(notCondition, audiencesMap) + ")"); - } - break; - default: - stringBuilder.append(operand); - break; - } - - return stringBuilder.toString(); - } - - public String getNameOrNextCondition(String operand, List conditions, Map audiencesMap) { - StringBuilder stringBuilder = new StringBuilder(); - int index = 0; - if (conditions.isEmpty()) { - return ""; - } else if (conditions.size() == 1) { - return serialize(conditions.get(0), audiencesMap); - } else { - for (Condition con : conditions) { - index++; - if (index + 1 <= conditions.size()) { - if (con instanceof AudienceIdCondition) { - String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), - audiencesMap); - stringBuilder.append( audienceName + " "); - } else { - stringBuilder.append("(" + serialize(con, audiencesMap) + ") "); - } - stringBuilder.append(operand); - stringBuilder.append(" "); - } else { - if (con instanceof AudienceIdCondition) { - String audienceName = this.getNameFromAudienceId(((AudienceIdCondition) con).getAudienceId(), - audiencesMap); - stringBuilder.append(audienceName); - } else { - stringBuilder.append("(" + serialize(con, audiencesMap) + ")"); - } - } - } - } - return stringBuilder.toString(); - } - @Override public String toString() { - return "Experiment{" + + return "Holdout {" + "id='" + id + '\'' + ", key='" + key + '\'' + ", groupId='" + groupId + '\'' + From 016fdcdb1771333b98576726384c28e6af65600f Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 17 Jul 2025 18:22:38 +0600 Subject: [PATCH 03/19] Remove layerId --- .../main/java/com/optimizely/ab/config/Holdout.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index d2f91ca02..f151bc4c7 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -43,7 +43,6 @@ public class Holdout implements ExperimentCore { private final String id; private final String key; private final String status; - private final String layerId; private final String groupId; private final List audienceIds; @@ -54,6 +53,9 @@ public class Holdout implements ExperimentCore { private final Map variationKeyToVariationMap; private final Map variationIdToVariationMap; private final Map userIdToVariationKeyMap; + // Not necessary for HO + private final String layerId = ""; + public enum HoldoutStatus { RUNNING("Running"), @@ -73,27 +75,25 @@ public String toString() { } @VisibleForTesting - public Holdout(String id, String key, String layerId) { - this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), ""); + public Holdout(String id, String key) { + this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), ""); } @JsonCreator public Holdout(@JsonProperty("id") String id, @JsonProperty("key") String key, @JsonProperty("status") String status, - @JsonProperty("layerId") String layerId, @JsonProperty("audienceIds") List audienceIds, @JsonProperty("audienceConditions") Condition audienceConditions, @JsonProperty("variations") List variations, @JsonProperty("forcedVariations") Map userIdToVariationKeyMap, @JsonProperty("trafficAllocation") List trafficAllocation) { - this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, ""); + this(id, key, status, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, ""); } public Holdout(@Nonnull String id, @Nonnull String key, @Nullable String status, - @Nullable String layerId, @Nonnull List audienceIds, @Nullable Condition audienceConditions, @Nonnull List variations, @@ -103,7 +103,6 @@ public Holdout(@Nonnull String id, this.id = id; this.key = key; this.status = status == null ? HoldoutStatus.DRAFT.toString() : status; - this.layerId = layerId; this.audienceIds = Collections.unmodifiableList(audienceIds); this.audienceConditions = audienceConditions; this.variations = Collections.unmodifiableList(variations); From 0213674a35b36cdffae1cb48d87e7038dbf7fdce Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 17 Jul 2025 18:46:01 +0600 Subject: [PATCH 04/19] Test cases added --- .../com/optimizely/ab/config/HoldoutTest.java | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java diff --git a/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java new file mode 100644 index 000000000..2e0255a8e --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java @@ -0,0 +1,284 @@ +/** + * + * Copyright 2025, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.optimizely.ab.config.audience.*; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +public class HoldoutTest { + + // MARK: - Sample Data + private static final String id = "11111"; + private static final String key = "background"; + private static final String status = Holdout.HoldoutStatus.RUNNING.toString(); + + // Create a simple Variation for testing + private static Variation createVariation() { + return new Variation("553339214", "house", false, Collections.emptyList()); + } + + // Create a simple TrafficAllocation for testing + private static TrafficAllocation createTrafficAllocation() { + return new TrafficAllocation("553339214", 5000); + } + + // MARK: - JSON Parsing Tests + + @Test + public void testDeserializeSuccessWithJSONValid() { + // Create a Holdout object directly + List audienceIds = Collections.singletonList("33333"); + List variations = Collections.singletonList(createVariation()); + List trafficAllocations = Collections.singletonList(createTrafficAllocation()); + + // Create a simple audience condition + AudienceIdCondition audienceCondition = new AudienceIdCondition("33333"); + + Holdout holdout = new Holdout(id, key, status, audienceIds, audienceCondition, + variations, Collections.emptyMap(), trafficAllocations); + + assertEquals(id, holdout.getId()); + assertEquals(key, holdout.getKey()); + assertEquals(Holdout.HoldoutStatus.RUNNING.toString(), holdout.getStatus()); + assertEquals(1, holdout.getVariations().size()); + assertEquals(1, holdout.getTrafficAllocation().size()); + assertEquals(audienceIds, holdout.getAudienceIds()); + assertNotNull(holdout.getAudienceConditions()); + assertEquals("", holdout.getLayerId()); // Always empty string + } + + @Test + public void testIsActive() { + // Test RUNNING status + Holdout runningHoldout = new Holdout(id, key, Holdout.HoldoutStatus.RUNNING.toString(), + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyList()); + assertTrue(runningHoldout.isActive()); + assertTrue(runningHoldout.isRunning()); + + // Test DRAFT status + Holdout draftHoldout = new Holdout(id, key, Holdout.HoldoutStatus.DRAFT.toString(), + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyList()); + assertFalse(draftHoldout.isActive()); + assertFalse(draftHoldout.isRunning()); + + // Test CONCLUDED status + Holdout concludedHoldout = new Holdout(id, key, Holdout.HoldoutStatus.CONCLUDED.toString(), + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyList()); + assertFalse(concludedHoldout.isActive()); + assertFalse(concludedHoldout.isRunning()); + + // Test ARCHIVED status + Holdout archivedHoldout = new Holdout(id, key, Holdout.HoldoutStatus.ARCHIVED.toString(), + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyMap(), Collections.emptyList()); + assertFalse(archivedHoldout.isActive()); + assertFalse(archivedHoldout.isRunning()); + } + + @Test + public void testDefaultStatus() { + // Create a Holdout with null status + Holdout holdout = new Holdout(id, key, null, Collections.emptyList(), null, + Collections.emptyList(), Collections.emptyMap(), + Collections.emptyList()); + + assertEquals(Holdout.HoldoutStatus.DRAFT.toString(), holdout.getStatus()); + } + + // MARK: - Audience Serialization Tests + + @Test + public void testSerializeConditionScenarios() { + List audienceConditionsScenarios = getAudienceConditionsList(); + Map expectedScenarioStringsMap = getExpectedScenariosMap(); + Map audiencesMap = new HashMap<>(); + audiencesMap.put("1", "us"); + audiencesMap.put("2", "female"); + audiencesMap.put("3", "adult"); + audiencesMap.put("11", "fr"); + audiencesMap.put("12", "male"); + audiencesMap.put("13", "kid"); + + if (expectedScenarioStringsMap.size() == audienceConditionsScenarios.size()) { + for (int i = 0; i < audienceConditionsScenarios.size() - 1; i++) { + Holdout holdout = makeMockHoldoutWithStatus(Holdout.HoldoutStatus.RUNNING, audienceConditionsScenarios.get(i)); + String audiences = holdout.serializeConditions(audiencesMap); + assertEquals(expectedScenarioStringsMap.get(i+1), audiences); + } + } + } + + public Map getExpectedScenariosMap() { + Map expectedScenarioStringsMap = new HashMap<>(); + expectedScenarioStringsMap.put(1, ""); + expectedScenarioStringsMap.put(2, "\"us\" OR \"female\""); + expectedScenarioStringsMap.put(3, "\"us\" AND \"female\" AND \"adult\""); + expectedScenarioStringsMap.put(4, "NOT \"us\""); + expectedScenarioStringsMap.put(5, "\"us\""); + expectedScenarioStringsMap.put(6, "\"us\""); + expectedScenarioStringsMap.put(7, "\"us\""); + expectedScenarioStringsMap.put(8, "\"us\" OR \"female\""); + expectedScenarioStringsMap.put(9, "(\"us\" OR \"female\") AND \"adult\""); + expectedScenarioStringsMap.put(10, "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))"); + expectedScenarioStringsMap.put(11, "NOT (\"us\" AND \"female\")"); + expectedScenarioStringsMap.put(12, "\"us\" OR \"100000\""); + expectedScenarioStringsMap.put(13, ""); + + return expectedScenarioStringsMap; + } + + public List getAudienceConditionsList() { + AudienceIdCondition one = new AudienceIdCondition("1"); + AudienceIdCondition two = new AudienceIdCondition("2"); + AudienceIdCondition three = new AudienceIdCondition("3"); + AudienceIdCondition eleven = new AudienceIdCondition("11"); + AudienceIdCondition twelve = new AudienceIdCondition("12"); + AudienceIdCondition thirteen = new AudienceIdCondition("13"); + + // Scenario 1 - [] + EmptyCondition scenario1 = new EmptyCondition(); + + // Scenario 2 - ["or", "1", "2"] + List scenario2List = new ArrayList<>(); + scenario2List.add(one); + scenario2List.add(two); + OrCondition scenario2 = new OrCondition(scenario2List); + + // Scenario 3 - ["and", "1", "2", "3"] + List scenario3List = new ArrayList<>(); + scenario3List.add(one); + scenario3List.add(two); + scenario3List.add(three); + AndCondition scenario3 = new AndCondition(scenario3List); + + // Scenario 4 - ["not", "1"] + NotCondition scenario4 = new NotCondition(one); + + // Scenario 5 - ["or", "1"] + List scenario5List = new ArrayList<>(); + scenario5List.add(one); + OrCondition scenario5 = new OrCondition(scenario5List); + + // Scenario 6 - ["and", "1"] + List scenario6List = new ArrayList<>(); + scenario6List.add(one); + AndCondition scenario6 = new AndCondition(scenario6List); + + // Scenario 7 - ["1"] + AudienceIdCondition scenario7 = one; + + // Scenario 8 - ["1", "2"] + // Defaults to Or in Datafile Parsing resulting in an OrCondition + OrCondition scenario8 = scenario2; + + // Scenario 9 - ["and", ["or", "1", "2"], "3"] + List Scenario9List = new ArrayList<>(); + Scenario9List.add(scenario2); + Scenario9List.add(three); + AndCondition scenario9 = new AndCondition(Scenario9List); + + // Scenario 10 - ["and", ["or", "1", ["and", "2", "3"]], ["and", "11, ["or", "12", "13"]]] + List scenario10List = new ArrayList<>(); + + List or1213List = new ArrayList<>(); + or1213List.add(twelve); + or1213List.add(thirteen); + OrCondition or1213 = new OrCondition(or1213List); + + List and11Or1213List = new ArrayList<>(); + and11Or1213List.add(eleven); + and11Or1213List.add(or1213); + AndCondition and11Or1213 = new AndCondition(and11Or1213List); + + List and23List = new ArrayList<>(); + and23List.add(two); + and23List.add(three); + AndCondition and23 = new AndCondition(and23List); + + List or1And23List = new ArrayList<>(); + or1And23List.add(one); + or1And23List.add(and23); + OrCondition or1And23 = new OrCondition(or1And23List); + + scenario10List.add(or1And23); + scenario10List.add(and11Or1213); + AndCondition scenario10 = new AndCondition(scenario10List); + + // Scenario 11 - ["not", ["and", "1", "2"]] + List and12List = new ArrayList<>(); + and12List.add(one); + and12List.add(two); + AndCondition and12 = new AndCondition(and12List); + + NotCondition scenario11 = new NotCondition(and12); + + // Scenario 12 - ["or", "1", "100000"] + List scenario12List = new ArrayList<>(); + scenario12List.add(one); + AudienceIdCondition unknownAudience = new AudienceIdCondition("100000"); + scenario12List.add(unknownAudience); + + OrCondition scenario12 = new OrCondition(scenario12List); + + // Scenario 13 - ["and", ["and", invalidAudienceIdCondition]] which becomes// the scenario of ["and", "and"] and results in empty string. + AudienceIdCondition invalidAudience = new AudienceIdCondition("5"); + List invalidIdList = new ArrayList<>(); + invalidIdList.add(invalidAudience); + AndCondition andCondition = new AndCondition(invalidIdList); + List andInvalidAudienceId = new ArrayList<>(); + andInvalidAudienceId.add(andCondition); + AndCondition scenario13 = new AndCondition(andInvalidAudienceId); + + List conditionTestScenarios = new ArrayList<>(); + conditionTestScenarios.add(scenario1); + conditionTestScenarios.add(scenario2); + conditionTestScenarios.add(scenario3); + conditionTestScenarios.add(scenario4); + conditionTestScenarios.add(scenario5); + conditionTestScenarios.add(scenario6); + conditionTestScenarios.add(scenario7); + conditionTestScenarios.add(scenario8); + conditionTestScenarios.add(scenario9); + conditionTestScenarios.add(scenario10); + conditionTestScenarios.add(scenario11); + conditionTestScenarios.add(scenario12); + conditionTestScenarios.add(scenario13); + + return conditionTestScenarios; + } + + private Holdout makeMockHoldoutWithStatus(Holdout.HoldoutStatus status, Condition audienceConditions) { + return new Holdout("12345", + "mockHoldoutKey", + status.toString(), + Collections.emptyList(), + audienceConditions, + Collections.emptyList(), + Collections.emptyMap(), + Collections.emptyList() + ); + } +} From 52da860c5aaafb42aebe17c675a8693d406ebb36 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 18 Jul 2025 17:40:48 +0600 Subject: [PATCH 05/19] Add included and excluded list Add test holdout test --- .../com/optimizely/ab/config/Holdout.java | 16 ++- .../com/optimizely/ab/config/HoldoutTest.java | 97 ++----------------- 2 files changed, 20 insertions(+), 93 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index f151bc4c7..80370da7f 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -49,6 +49,8 @@ public class Holdout implements ExperimentCore { private final Condition audienceConditions; private final List variations; private final List trafficAllocation; + private final List includedFlags; + private final List excludedFlags; private final Map variationKeyToVariationMap; private final Map variationIdToVariationMap; @@ -76,7 +78,7 @@ public String toString() { @VisibleForTesting public Holdout(String id, String key) { - this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), ""); + this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), ""); } @JsonCreator @@ -87,8 +89,10 @@ public Holdout(@JsonProperty("id") String id, @JsonProperty("audienceConditions") Condition audienceConditions, @JsonProperty("variations") List variations, @JsonProperty("forcedVariations") Map userIdToVariationKeyMap, - @JsonProperty("trafficAllocation") List trafficAllocation) { - this(id, key, status, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, ""); + @JsonProperty("trafficAllocation") List trafficAllocation, + @JsonProperty("includedFlags") List includedFlags, + @JsonProperty("excludedFlags") List excludedFlags) { + this(id, key, status, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, includedFlags, excludedFlags, ""); } public Holdout(@Nonnull String id, @@ -99,14 +103,18 @@ public Holdout(@Nonnull String id, @Nonnull List variations, @Nonnull Map userIdToVariationKeyMap, @Nonnull List trafficAllocation, + @Nullable List includedFlags, + @Nullable List excludedFlags, @Nonnull String groupId) { this.id = id; this.key = key; - this.status = status == null ? HoldoutStatus.DRAFT.toString() : status; + this.status = status == null ? HoldoutStatus.RUNNING.toString() : status; this.audienceIds = Collections.unmodifiableList(audienceIds); this.audienceConditions = audienceConditions; this.variations = Collections.unmodifiableList(variations); this.trafficAllocation = Collections.unmodifiableList(trafficAllocation); + this.includedFlags = includedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(includedFlags); + this.excludedFlags = excludedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(excludedFlags); this.groupId = groupId; this.userIdToVariationKeyMap = userIdToVariationKeyMap; this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations); diff --git a/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java index 2e0255a8e..9e558bf0f 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java @@ -16,8 +16,6 @@ */ package com.optimizely.ab.config; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.optimizely.ab.config.audience.*; import org.junit.Test; import static org.junit.Assert.*; @@ -27,91 +25,8 @@ public class HoldoutTest { - // MARK: - Sample Data - private static final String id = "11111"; - private static final String key = "background"; - private static final String status = Holdout.HoldoutStatus.RUNNING.toString(); - - // Create a simple Variation for testing - private static Variation createVariation() { - return new Variation("553339214", "house", false, Collections.emptyList()); - } - - // Create a simple TrafficAllocation for testing - private static TrafficAllocation createTrafficAllocation() { - return new TrafficAllocation("553339214", 5000); - } - - // MARK: - JSON Parsing Tests - - @Test - public void testDeserializeSuccessWithJSONValid() { - // Create a Holdout object directly - List audienceIds = Collections.singletonList("33333"); - List variations = Collections.singletonList(createVariation()); - List trafficAllocations = Collections.singletonList(createTrafficAllocation()); - - // Create a simple audience condition - AudienceIdCondition audienceCondition = new AudienceIdCondition("33333"); - - Holdout holdout = new Holdout(id, key, status, audienceIds, audienceCondition, - variations, Collections.emptyMap(), trafficAllocations); - - assertEquals(id, holdout.getId()); - assertEquals(key, holdout.getKey()); - assertEquals(Holdout.HoldoutStatus.RUNNING.toString(), holdout.getStatus()); - assertEquals(1, holdout.getVariations().size()); - assertEquals(1, holdout.getTrafficAllocation().size()); - assertEquals(audienceIds, holdout.getAudienceIds()); - assertNotNull(holdout.getAudienceConditions()); - assertEquals("", holdout.getLayerId()); // Always empty string - } - @Test - public void testIsActive() { - // Test RUNNING status - Holdout runningHoldout = new Holdout(id, key, Holdout.HoldoutStatus.RUNNING.toString(), - Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyMap(), Collections.emptyList()); - assertTrue(runningHoldout.isActive()); - assertTrue(runningHoldout.isRunning()); - - // Test DRAFT status - Holdout draftHoldout = new Holdout(id, key, Holdout.HoldoutStatus.DRAFT.toString(), - Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyMap(), Collections.emptyList()); - assertFalse(draftHoldout.isActive()); - assertFalse(draftHoldout.isRunning()); - - // Test CONCLUDED status - Holdout concludedHoldout = new Holdout(id, key, Holdout.HoldoutStatus.CONCLUDED.toString(), - Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyMap(), Collections.emptyList()); - assertFalse(concludedHoldout.isActive()); - assertFalse(concludedHoldout.isRunning()); - - // Test ARCHIVED status - Holdout archivedHoldout = new Holdout(id, key, Holdout.HoldoutStatus.ARCHIVED.toString(), - Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyMap(), Collections.emptyList()); - assertFalse(archivedHoldout.isActive()); - assertFalse(archivedHoldout.isRunning()); - } - - @Test - public void testDefaultStatus() { - // Create a Holdout with null status - Holdout holdout = new Holdout(id, key, null, Collections.emptyList(), null, - Collections.emptyList(), Collections.emptyMap(), - Collections.emptyList()); - - assertEquals(Holdout.HoldoutStatus.DRAFT.toString(), holdout.getStatus()); - } - - // MARK: - Audience Serialization Tests - - @Test - public void testSerializeConditionScenarios() { + public void testStringifyConditionScenarios() { List audienceConditionsScenarios = getAudienceConditionsList(); Map expectedScenarioStringsMap = getExpectedScenariosMap(); Map audiencesMap = new HashMap<>(); @@ -124,7 +39,8 @@ public void testSerializeConditionScenarios() { if (expectedScenarioStringsMap.size() == audienceConditionsScenarios.size()) { for (int i = 0; i < audienceConditionsScenarios.size() - 1; i++) { - Holdout holdout = makeMockHoldoutWithStatus(Holdout.HoldoutStatus.RUNNING, audienceConditionsScenarios.get(i)); + Holdout holdout = makeMockHoldoutWithStatus(Holdout.HoldoutStatus.RUNNING, + audienceConditionsScenarios.get(i)); String audiences = holdout.serializeConditions(audiencesMap); assertEquals(expectedScenarioStringsMap.get(i+1), audiences); } @@ -243,7 +159,8 @@ public List getAudienceConditionsList() { OrCondition scenario12 = new OrCondition(scenario12List); - // Scenario 13 - ["and", ["and", invalidAudienceIdCondition]] which becomes// the scenario of ["and", "and"] and results in empty string. + // Scenario 13 - ["and", ["and", invalidAudienceIdCondition]] which becomes + // the scenario of ["and", "and"] and results in empty string. AudienceIdCondition invalidAudience = new AudienceIdCondition("5"); List invalidIdList = new ArrayList<>(); invalidIdList.add(invalidAudience); @@ -278,7 +195,9 @@ private Holdout makeMockHoldoutWithStatus(Holdout.HoldoutStatus status, Conditio audienceConditions, Collections.emptyList(), Collections.emptyMap(), - Collections.emptyList() + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() ); } } From dc44c7e9842d9423ccd1ac47dd97a8129906d0f1 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 18 Jul 2025 19:31:31 +0600 Subject: [PATCH 06/19] Add holdout config --- .../ab/config/DatafileProjectConfig.java | 42 +++++ .../com/optimizely/ab/config/Holdout.java | 4 + .../optimizely/ab/config/HoldoutConfig.java | 149 ++++++++++++++++++ .../optimizely/ab/config/ProjectConfig.java | 2 + 4 files changed, 197 insertions(+) create mode 100644 core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java index 28ad519a5..86d0cb696 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java @@ -70,6 +70,7 @@ public class DatafileProjectConfig implements ProjectConfig { private final List typedAudiences; private final List events; private final List experiments; + private final List holdouts; private final List featureFlags; private final List groups; private final List rollouts; @@ -95,6 +96,8 @@ public class DatafileProjectConfig implements ProjectConfig { // other mappings private final Map variationIdToExperimentMapping; + private final HoldoutConfig holdoutConfig; + private String datafile; // v2 constructor @@ -124,6 +127,34 @@ public DatafileProjectConfig(String accountId, String projectId, String version, eventType, experiments, null, + null, + groups, + null, + null + ); + } + + // v3 constructor + public DatafileProjectConfig(String accountId, String projectId, String version, String revision, List groups, + List experiments, List holdouts, List attributes, List eventType, + List audiences, boolean anonymizeIP) { + this( + accountId, + anonymizeIP, + false, + null, + projectId, + revision, + null, + null, + version, + attributes, + audiences, + null, + eventType, + experiments, + holdouts, + null, groups, null, null @@ -145,6 +176,7 @@ public DatafileProjectConfig(String accountId, List typedAudiences, List events, List experiments, + List holdouts, List featureFlags, List groups, List rollouts, @@ -186,6 +218,12 @@ public DatafileProjectConfig(String accountId, allExperiments.addAll(experiments); allExperiments.addAll(aggregateGroupExperiments(groups)); this.experiments = Collections.unmodifiableList(allExperiments); + if (holdouts == null) { + this.holdouts = Collections.emptyList(); + } else { + this.holdouts = Collections.unmodifiableList(holdouts); + } + this.holdoutConfig = new HoldoutConfig(this.holdouts); String publicKeyForODP = ""; String hostForODP = ""; @@ -434,6 +472,10 @@ public List getExperiments() { return experiments; } + @Override + public List getHoldouts() { + return holdoutConfig.getAllHoldouts(); } + @Override public Set getAllSegments() { return this.allSegments; diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index 80370da7f..4d1473f19 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -165,6 +165,10 @@ public List getTrafficAllocation() { return trafficAllocation; } + public List getIncludedFlags() { return includedFlags; } + + public List getExcludedFlags() { return excludedFlags; } + public String getGroupId() { return groupId; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java new file mode 100644 index 000000000..b9e8c2e79 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java @@ -0,0 +1,149 @@ +package com.optimizely.ab.config; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * HoldoutConfig manages collections of Holdout objects and their relationships to flags. + */ +public class HoldoutConfig { + private List allHoldouts; + private List global; + private Map holdoutIdMap; + private Map> flagHoldoutsMap; + private Map> includedHoldouts; + private Map> excludedHoldouts; + + /** + * Initializes a new HoldoutConfig with an empty list of holdouts. + */ + public HoldoutConfig() { + this(Collections.emptyList()); + } + + /** + * Initializes a new HoldoutConfig with the specified holdouts. + * + * @param allHoldouts The list of holdouts to manage + */ + public HoldoutConfig(@Nonnull List allHoldouts) { + this.allHoldouts = new ArrayList<>(allHoldouts); + this.global = new ArrayList<>(); + this.holdoutIdMap = new HashMap<>(); + this.flagHoldoutsMap = new ConcurrentHashMap<>(); + this.includedHoldouts = new HashMap<>(); + this.excludedHoldouts = new HashMap<>(); + updateHoldoutMapping(); + } + + /** + * Updates internal mappings of holdouts including the id map, global list, + * and per-flag inclusion/exclusion maps. + */ + public void updateHoldoutMapping() { + holdoutIdMap.clear(); + for (Holdout holdout : allHoldouts) { + holdoutIdMap.put(holdout.getId(), holdout); + } + + flagHoldoutsMap.clear(); + global.clear(); + includedHoldouts.clear(); + excludedHoldouts.clear(); + + for (Holdout holdout : allHoldouts) { + boolean hasIncludedFlags = !holdout.getIncludedFlags().isEmpty(); + boolean hasExcludedFlags = !holdout.getExcludedFlags().isEmpty(); + + if (!hasIncludedFlags && !hasExcludedFlags) { + // Global holdout (applies to all flags) + global.add(holdout); + } else if (hasIncludedFlags) { + // Holdout only applies to specific included flags + for (String flagId : holdout.getIncludedFlags()) { + includedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout); + } + } else { + // Global holdout with specific exclusions + global.add(holdout); + + for (String flagId : holdout.getExcludedFlags()) { + excludedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout); + } + } + } + } + + /** + * Returns the applicable holdouts for the given flag ID by combining global holdouts + * (excluding any specified) and included holdouts, in that order. + * Caches the result for future calls. + * + * @param id The flag identifier + * @return A list of Holdout objects relevant to the given flag + */ + public List getHoldoutForFlag(@Nonnull String id) { + if (allHoldouts.isEmpty()) { + return Collections.emptyList(); + } + + // Check cache and return persistent holdouts + if (flagHoldoutsMap.containsKey(id)) { + return flagHoldoutsMap.get(id); + } + + // Prioritize global holdouts first + List activeHoldouts = new ArrayList<>(); + List excluded = excludedHoldouts.getOrDefault(id, Collections.emptyList()); + + if (!excluded.isEmpty()) { + for (Holdout holdout : global) { + if (!excluded.contains(holdout)) { + activeHoldouts.add(holdout); + } + } + } else { + activeHoldouts.addAll(global); + } + + // Add included holdouts + activeHoldouts.addAll(includedHoldouts.getOrDefault(id, Collections.emptyList())); + + // Cache the result + flagHoldoutsMap.put(id, activeHoldouts); + + return activeHoldouts; + } + + /** + * Get a Holdout object for an Id. + * + * @param id The holdout identifier + * @return The Holdout object if found, null otherwise + */ + @Nullable + public Holdout getHoldout(@Nonnull String id) { + return holdoutIdMap.get(id); + } + + /** + * Returns all holdouts managed by this config. + * + * @return An unmodifiable list of all holdouts + */ + public List getAllHoldouts() { + return Collections.unmodifiableList(allHoldouts); + } + + /** + * Returns the global holdouts (those that apply to all flags by default). + * + * @return An unmodifiable list of global holdouts + */ + public List getGlobal() { + return Collections.unmodifiableList(global); + } + +} \ No newline at end of file diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 2073be9ef..366e2a1ff 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -70,6 +70,8 @@ Experiment getExperimentForKey(@Nonnull String experimentKey, List getExperiments(); + List getHoldouts(); + Set getAllSegments(); List getExperimentsForEventKey(String eventKey); From e3a596e43d3440e7a516ef27c40f21f5b48bc16e Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 18 Jul 2025 20:59:24 +0600 Subject: [PATCH 07/19] Holdouts parsing from data file --- .../parser/DatafileGsonDeserializer.java | 10 +++ .../parser/DatafileJacksonDeserializer.java | 8 ++ .../ab/config/parser/GsonHelpers.java | 47 ++++++++++++ .../ab/config/parser/JsonConfigParser.java | 76 +++++++++++++++++++ .../config/parser/JsonSimpleConfigParser.java | 67 ++++++++++++++++ 5 files changed, 208 insertions(+) diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java index f349805fa..499a5fc5c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java @@ -51,6 +51,8 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa }.getType(); Type experimentsType = new TypeToken>() { }.getType(); + Type holdoutsType = new TypeToken>() { + }.getType(); Type attributesType = new TypeToken>() { }.getType(); Type eventsType = new TypeToken>() { @@ -64,6 +66,13 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa List experiments = context.deserialize(jsonObject.get("experiments").getAsJsonArray(), experimentsType); + List holdouts; + if (jsonObject.has("holdouts")) { + holdouts = context.deserialize(jsonObject.get("holdouts").getAsJsonArray(), holdoutsType); + } else { + holdouts = Collections.emptyList(); + } + List attributes; attributes = context.deserialize(jsonObject.get("attributes"), attributesType); @@ -127,6 +136,7 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa typedAudiences, events, experiments, + holdouts, featureFlags, groups, rollouts, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java index 4ef104428..e38425cf4 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java @@ -46,6 +46,13 @@ public DatafileProjectConfig deserialize(JsonParser parser, DeserializationConte List attributes = JacksonHelpers.arrayNodeToList(node.get("attributes"), Attribute.class, codec); List events = JacksonHelpers.arrayNodeToList(node.get("events"), EventType.class, codec); + List holdouts; + if (node.has("holdouts")) { + holdouts = JacksonHelpers.arrayNodeToList(node.get("holdouts"), Holdout.class, codec); + } else { + holdouts = Collections.emptyList(); + } + List audiences = Collections.emptyList(); if (node.has("audiences")) { audiences = JacksonHelpers.arrayNodeToList(node.get("audiences"), Audience.class, codec); @@ -103,6 +110,7 @@ public DatafileProjectConfig deserialize(JsonParser parser, DeserializationConte (List) (List) typedAudiences, events, experiments, + holdouts, featureFlags, groups, rollouts, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java index 1399497b2..f4d900e80 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java @@ -25,7 +25,9 @@ import com.google.gson.reflect.TypeToken; import com.optimizely.ab.bucketing.DecisionService; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Holdout; import com.optimizely.ab.config.Experiment.ExperimentStatus; +import com.optimizely.ab.config.Holdout.HoldoutStatus; import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.FeatureVariableUsageInstance; @@ -151,6 +153,51 @@ static Experiment parseExperiment(JsonObject experimentJson, JsonDeserialization return parseExperiment(experimentJson, "", context); } + static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserializationContext context) { + String id = holdoutJson.get("id").getAsString(); + String key = holdoutJson.get("key").getAsString(); + JsonElement holdoutStatusJson = holdoutJson.get("status"); + String status = holdoutJson.get("status").getAsString(); + + JsonArray audienceIdsJson = holdoutJson.getAsJsonArray("audienceIds"); + List audienceIds = new ArrayList<>(audienceIdsJson.size()); + for (JsonElement audienceIdObj : audienceIdsJson) { + audienceIds.add(audienceIdObj.getAsString()); + } + + Condition conditions = parseAudienceConditions(holdoutJson); + + // parse the child objects + List variations = parseVariations(holdoutJson.getAsJsonArray("variations"), context); + Map userIdToVariationKeyMap = + parseForcedVariations(holdoutJson.getAsJsonObject("forcedVariations")); + List trafficAllocations = + parseTrafficAllocation(holdoutJson.getAsJsonArray("trafficAllocation")); + + List includedFlags = new ArrayList<>(); + if (holdoutJson.has("includedFlags")) { + JsonArray includedIdsJson = holdoutJson.getAsJsonArray("includedFlags"); + for (JsonElement hoIdObj : includedIdsJson) { + includedFlags.add(hoIdObj.getAsString()); + } + } + + List excludedFlags = new ArrayList<>(); + if (holdoutJson.has("excludedFlags")) { + JsonArray excludedIdsJson = holdoutJson.getAsJsonArray("excludedFlags"); + for (JsonElement hoIdObj : excludedIdsJson) { + excludedFlags.add(hoIdObj.getAsString()); + } + } + + return new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, + trafficAllocations, includedFlags, excludedFlags, groupId); + } + + static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext context) { + return parseHoldout(holdoutJson, "", context); + } + static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) { String id = featureFlagJson.get("id").getAsString(); String key = featureFlagJson.get("key").getAsString(); diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index ea5101054..bdf20cd00 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -48,6 +48,13 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List experiments = parseExperiments(rootObject.getJSONArray("experiments")); + List holdouts; + if (rootObject.has("holdouts")) { + holdouts = parseHoldouts(rootObject.getJSONArray("holdouts")); + } else { + holdouts = Collections.emptyList(); + } + List attributes; attributes = parseAttributes(rootObject.getJSONArray("attributes")); @@ -108,6 +115,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse typedAudiences, events, experiments, + holdouts, featureFlags, groups, rollouts, @@ -166,6 +174,74 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } + private List parseHoldouts(JSONArray holdoutJson) { + return parseHoldouts(holdoutJson, ""); + } + + private List parseHoldouts(JSONArray holdoutJson, String groupId) { + List holdouts = new ArrayList(holdoutJson.length()); + + for (int i = 0; i < holdoutJson.length(); i++) { + Object obj = holdoutJson.get(i); + JSONObject holdoutObject = (JSONObject) obj; + String id = holdoutObject.getString("id"); + String key = holdoutObject.getString("key"); + String status = holdoutObject.getString("status"); + + JSONArray audienceIdsJson = holdoutObject.getJSONArray("audienceIds"); + List audienceIds = new ArrayList(audienceIdsJson.length()); + + for (int j = 0; j < audienceIdsJson.length(); j++) { + Object audienceIdObj = audienceIdsJson.get(j); + audienceIds.add((String) audienceIdObj); + } + + Condition conditions = null; + if (holdoutObject.has("audienceConditions")) { + Object jsonCondition = holdoutObject.get("audienceConditions"); + conditions = ConditionUtils.parseConditions(AudienceIdCondition.class, jsonCondition); + } + + // parse the child objects + List variations = parseVariations(holdoutObject.getJSONArray("variations")); + Map userIdToVariationKeyMap = + parseForcedVariations(holdoutObject.getJSONObject("forcedVariations")); + List trafficAllocations = + parseTrafficAllocation(holdoutObject.getJSONArray("trafficAllocation")); + + List includedFlags; + if (holdoutObject.has("includedFlags")) { + JSONArray includedIdsJson = holdoutObject.getJSONArray("includedFlags"); + includedFlags = new ArrayList<>(includedIdsJson.length()); + + for (int j = 0; j < includedIdsJson.length(); j++) { + Object idObj = includedIdsJson.get(j); + includedFlags.add((String) idObj); + } + } else { + includedFlags = Collections.emptyList(); + } + + List excludedFlags; + if (holdoutObject.has("excludedFlags")) { + JSONArray excludedIdsJson = holdoutObject.getJSONArray("excludedFlags"); + excludedFlags = new ArrayList<>(excludedIdsJson.length()); + + for (int j = 0; j < excludedIdsJson.length(); j++) { + Object idObj = excludedIdsJson.get(j); + excludedFlags.add((String) idObj); + } + } else { + excludedFlags = Collections.emptyList(); + } + + holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, + trafficAllocations, includedFlags, excludedFlags, groupId)); + } + + return holdouts; + } + private List parseExperimentIds(JSONArray experimentIdsJson) { ArrayList experimentIds = new ArrayList(experimentIdsJson.length()); diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index c65eb6213..31a3e3df1 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -57,6 +57,13 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse List experiments = parseExperiments((JSONArray) rootObject.get("experiments")); + List holdouts; + if (rootObject.containsKey("holdouts")) { + holdouts = parseHoldouts((JSONArray) rootObject.get("holdouts")); + } else { + holdouts = Collections.emptyList(); + } + List attributes; attributes = parseAttributes((JSONArray) rootObject.get("attributes")); @@ -111,6 +118,7 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse typedAudiences, events, experiments, + holdouts, featureFlags, groups, rollouts, @@ -173,6 +181,65 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } + + private List parseHoldouts(JSONArray holdoutJson) { + return parseHoldouts(holdoutJson, ""); + } + + private List parseHoldouts(JSONArray holdoutJson, String groupId) { + List holdouts = new ArrayList(holdoutJson.size()); + + for (Object obj : holdoutJson) { + JSONObject hoObject = (JSONObject) obj; + String id = (String) hoObject.get("id"); + String key = (String) hoObject.get("key"); + String status = (String) hoObject.get("status"); + + JSONArray audienceIdsJson = (JSONArray) hoObject.get("audienceIds"); + List audienceIds = new ArrayList(audienceIdsJson.size()); + + for (Object audienceIdObj : audienceIdsJson) { + audienceIds.add((String) audienceIdObj); + } + + Condition conditions = null; + if (hoObject.containsKey("audienceConditions")) { + Object jsonCondition = hoObject.get("audienceConditions"); + try { + conditions = ConditionUtils.parseConditions(AudienceIdCondition.class, jsonCondition); + } catch (Exception e) { + // unable to parse conditions. + Logger.getAnonymousLogger().log(Level.ALL, "problem parsing audience conditions", e); + } + } + // parse the child objects + List variations = parseVariations((JSONArray) hoObject.get("variations")); + Map userIdToVariationKeyMap = + parseForcedVariations((JSONObject) hoObject.get("forcedVariations")); + List trafficAllocations = + parseTrafficAllocation((JSONArray) hoObject.get("trafficAllocation")); + + List includedFlags; + if (hoObject.containsKey("includedFlags")) { + includedFlags = new ArrayList((JSONArray) hoObject.get("includedFlags")); + } else { + includedFlags = Collections.emptyList(); + } + + List excludedFlags; + if (hoObject.containsKey("excludedFlags")) { + excludedFlags = new ArrayList((JSONArray) hoObject.get("excludedFlags")); + } else { + excludedFlags = Collections.emptyList(); + } + + holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, + trafficAllocations, includedFlags, excludedFlags, groupId)); + } + + return holdouts; + } + private List parseExperimentIds(JSONArray experimentIdsJsonArray) { List experimentIds = new ArrayList(experimentIdsJsonArray.size()); From 714de6dcef440dd527fb775a80664140d75c8a51 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sat, 19 Jul 2025 12:36:28 +0600 Subject: [PATCH 08/19] Json simple config parser test cases add for holdout --- .../ab/config/DatafileProjectConfig.java | 10 +- .../com/optimizely/ab/config/Holdout.java | 13 +- .../com/optimizely/ab/config/Variation.java | 9 +- .../ab/config/parser/GsonHelpers.java | 5 +- .../ab/config/parser/JsonConfigParser.java | 5 +- .../config/parser/JsonSimpleConfigParser.java | 5 +- .../DatafileProjectConfigTestUtils.java | 72 +- .../com/optimizely/ab/config/HoldoutTest.java | 18 +- .../ab/config/ValidProjectConfigV4.java | 124 +++ .../parser/JsonSimpleConfigParserTest.java | 45 +- .../OptimizelyConfigServiceTest.java | 1 + .../config/holdouts-project-config.json | 977 ++++++++++++++++++ 12 files changed, 1222 insertions(+), 62 deletions(-) create mode 100644 core-api/src/test/resources/config/holdouts-project-config.json diff --git a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java index 86d0cb696..c0c80b0f1 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java @@ -70,7 +70,6 @@ public class DatafileProjectConfig implements ProjectConfig { private final List typedAudiences; private final List events; private final List experiments; - private final List holdouts; private final List featureFlags; private final List groups; private final List rollouts; @@ -218,12 +217,12 @@ public DatafileProjectConfig(String accountId, allExperiments.addAll(experiments); allExperiments.addAll(aggregateGroupExperiments(groups)); this.experiments = Collections.unmodifiableList(allExperiments); + if (holdouts == null) { - this.holdouts = Collections.emptyList(); + this.holdoutConfig = new HoldoutConfig(); } else { - this.holdouts = Collections.unmodifiableList(holdouts); + this.holdoutConfig = new HoldoutConfig(holdouts); } - this.holdoutConfig = new HoldoutConfig(this.holdouts); String publicKeyForODP = ""; String hostForODP = ""; @@ -473,8 +472,7 @@ public List getExperiments() { } @Override - public List getHoldouts() { - return holdoutConfig.getAllHoldouts(); } + public List getHoldouts() { return holdoutConfig.getAllHoldouts(); } @Override public Set getAllSegments() { diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index 4d1473f19..9660a9f45 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -54,7 +54,6 @@ public class Holdout implements ExperimentCore { private final Map variationKeyToVariationMap; private final Map variationIdToVariationMap; - private final Map userIdToVariationKeyMap; // Not necessary for HO private final String layerId = ""; @@ -78,7 +77,7 @@ public String toString() { @VisibleForTesting public Holdout(String id, String key) { - this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), ""); + this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null, null, ""); } @JsonCreator @@ -88,11 +87,10 @@ public Holdout(@JsonProperty("id") String id, @JsonProperty("audienceIds") List audienceIds, @JsonProperty("audienceConditions") Condition audienceConditions, @JsonProperty("variations") List variations, - @JsonProperty("forcedVariations") Map userIdToVariationKeyMap, @JsonProperty("trafficAllocation") List trafficAllocation, @JsonProperty("includedFlags") List includedFlags, @JsonProperty("excludedFlags") List excludedFlags) { - this(id, key, status, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, includedFlags, excludedFlags, ""); + this(id, key, status, audienceIds, audienceConditions, variations, trafficAllocation, includedFlags, excludedFlags, ""); } public Holdout(@Nonnull String id, @@ -101,7 +99,6 @@ public Holdout(@Nonnull String id, @Nonnull List audienceIds, @Nullable Condition audienceConditions, @Nonnull List variations, - @Nonnull Map userIdToVariationKeyMap, @Nonnull List trafficAllocation, @Nullable List includedFlags, @Nullable List excludedFlags, @@ -116,7 +113,6 @@ public Holdout(@Nonnull String id, this.includedFlags = includedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(includedFlags); this.excludedFlags = excludedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(excludedFlags); this.groupId = groupId; - this.userIdToVariationKeyMap = userIdToVariationKeyMap; this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations); this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations); } @@ -157,10 +153,6 @@ public Map getVariationIdToVariationMap() { return variationIdToVariationMap; } - public Map getUserIdToVariationKeyMap() { - return userIdToVariationKeyMap; - } - public List getTrafficAllocation() { return trafficAllocation; } @@ -192,7 +184,6 @@ public String toString() { ", audienceConditions=" + audienceConditions + ", variations=" + variations + ", variationKeyToVariationMap=" + variationKeyToVariationMap + - ", userIdToVariationKeyMap=" + userIdToVariationKeyMap + ", trafficAllocation=" + trafficAllocation + '}'; } diff --git a/core-api/src/main/java/com/optimizely/ab/config/Variation.java b/core-api/src/main/java/com/optimizely/ab/config/Variation.java index 0bb1765c2..db1e3e7c8 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Variation.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Variation.java @@ -42,7 +42,7 @@ public class Variation implements IdKeyMapped { private final Map variableIdToFeatureVariableUsageInstanceMap; public Variation(String id, String key) { - this(id, key, null); + this(id, key, false, null); } public Variation(String id, @@ -51,6 +51,13 @@ public Variation(String id, this(id, key, false, featureVariableUsageInstances); } + public Variation(String id, + String key, + Boolean featureEnabled) { + this(id, key, featureEnabled, null); + } + + @JsonCreator public Variation(@JsonProperty("id") String id, @JsonProperty("key") String key, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java index f4d900e80..5dad7880b 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java @@ -169,8 +169,6 @@ static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserial // parse the child objects List variations = parseVariations(holdoutJson.getAsJsonArray("variations"), context); - Map userIdToVariationKeyMap = - parseForcedVariations(holdoutJson.getAsJsonObject("forcedVariations")); List trafficAllocations = parseTrafficAllocation(holdoutJson.getAsJsonArray("trafficAllocation")); @@ -190,8 +188,7 @@ static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserial } } - return new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, - trafficAllocations, includedFlags, excludedFlags, groupId); + return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags, groupId); } static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext context) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index bdf20cd00..4765f9860 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -204,8 +204,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { // parse the child objects List variations = parseVariations(holdoutObject.getJSONArray("variations")); - Map userIdToVariationKeyMap = - parseForcedVariations(holdoutObject.getJSONObject("forcedVariations")); + List trafficAllocations = parseTrafficAllocation(holdoutObject.getJSONArray("trafficAllocation")); @@ -235,7 +234,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { excludedFlags = Collections.emptyList(); } - holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, + holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags, groupId)); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index 31a3e3df1..2ffdd6fd6 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -214,8 +214,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { } // parse the child objects List variations = parseVariations((JSONArray) hoObject.get("variations")); - Map userIdToVariationKeyMap = - parseForcedVariations((JSONObject) hoObject.get("forcedVariations")); + List trafficAllocations = parseTrafficAllocation((JSONArray) hoObject.get("trafficAllocation")); @@ -233,7 +232,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { excludedFlags = Collections.emptyList(); } - holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, userIdToVariationKeyMap, + holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags, groupId)); } diff --git a/core-api/src/test/java/com/optimizely/ab/config/DatafileProjectConfigTestUtils.java b/core-api/src/test/java/com/optimizely/ab/config/DatafileProjectConfigTestUtils.java index 9b65421bb..ef9a8ccc2 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/DatafileProjectConfigTestUtils.java +++ b/core-api/src/test/java/com/optimizely/ab/config/DatafileProjectConfigTestUtils.java @@ -16,34 +16,35 @@ */ package com.optimizely.ab.config; -import com.google.common.base.Charsets; -import com.google.common.io.Resources; -import com.optimizely.ab.config.audience.AndCondition; -import com.optimizely.ab.config.audience.Audience; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.NotCondition; -import com.optimizely.ab.config.audience.OrCondition; -import com.optimizely.ab.config.audience.UserAttribute; - -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import static java.util.Arrays.asList; import java.util.Collections; +import static java.util.Collections.singletonList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import com.optimizely.ab.config.audience.AndCondition; +import com.optimizely.ab.config.audience.Audience; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.NotCondition; +import com.optimizely.ab.config.audience.OrCondition; +import com.optimizely.ab.config.audience.UserAttribute; + /** * Helper class that provides common functionality and resources for testing {@link DatafileProjectConfig}. */ @@ -382,11 +383,16 @@ private static ProjectConfig generateNoAudienceProjectConfigV3() { } private static final ProjectConfig VALID_PROJECT_CONFIG_V4 = generateValidProjectConfigV4(); + private static final ProjectConfig VALID_PROJECT_CONFIG_V4_HOLDOUT = generateValidProjectConfigV4_holdout(); private static ProjectConfig generateValidProjectConfigV4() { return ValidProjectConfigV4.generateValidProjectConfigV4(); } + private static ProjectConfig generateValidProjectConfigV4_holdout() { + return ValidProjectConfigV4.generateValidProjectConfigV4_holdout(); + } + private DatafileProjectConfigTestUtils() { } @@ -410,6 +416,10 @@ public static String validConfigJsonV4() throws IOException { return Resources.toString(Resources.getResource("config/valid-project-config-v4.json"), Charsets.UTF_8); } + public static String validConfigHoldoutJsonV4() throws IOException { + return Resources.toString(Resources.getResource("config/holdouts-project-config.json"), Charsets.UTF_8); + } + public static String nullFeatureEnabledConfigJsonV4() throws IOException { return Resources.toString(Resources.getResource("config/null-featureEnabled-config-v4.json"), Charsets.UTF_8); } @@ -446,6 +456,10 @@ public static ProjectConfig validProjectConfigV4() { return VALID_PROJECT_CONFIG_V4; } + public static ProjectConfig validProjectConfigV4_holdout() { + return VALID_PROJECT_CONFIG_V4_HOLDOUT; + } + /** * @return the expected {@link DatafileProjectConfig} for the json produced by {@link #invalidProjectConfigV5()} */ @@ -471,6 +485,7 @@ public static void verifyProjectConfig(@CheckForNull ProjectConfig actual, @Nonn verifyAudiences(actual.getTypedAudiences(), expected.getTypedAudiences()); verifyEvents(actual.getEventTypes(), expected.getEventTypes()); verifyExperiments(actual.getExperiments(), expected.getExperiments()); + verifyHoldouts(actual.getHoldouts(), expected.getHoldouts()); verifyFeatureFlags(actual.getFeatureFlags(), expected.getFeatureFlags()); verifyGroups(actual.getGroups(), expected.getGroups()); verifyRollouts(actual.getRollouts(), expected.getRollouts()); @@ -502,6 +517,37 @@ private static void verifyExperiments(List actual, List } } + private static void verifyHoldouts(List actual, List expected) { + // print the holdouts for debugging BEFORE assertion + // System.out.println("Actual holdouts: " + actual); + // System.out.println("Expected holdouts: " + expected); + // System.out.println("Actual size: " + actual.size()); + // System.out.println("Expected size: " + expected.size()); + + assertThat(actual.size(), is(expected.size())); + + + for (int i = 0; i < actual.size(); i++) { + Holdout actualHoldout = actual.get(i); + Holdout expectedHoldout = expected.get(i); + + assertThat(actualHoldout.getId(), is(expectedHoldout.getId())); + assertThat(actualHoldout.getKey(), is(expectedHoldout.getKey())); + assertThat(actualHoldout.getGroupId(), is(expectedHoldout.getGroupId())); + assertThat(actualHoldout.getStatus(), is(expectedHoldout.getStatus())); + assertThat(actualHoldout.getAudienceIds(), is(expectedHoldout.getAudienceIds())); + /// debug print audience conditions + // System.out.println("Actual audience conditions: " + actualHoldout.getAudienceConditions()); + // System.out.println("Expected audience conditions: " + expectedHoldout.getAudienceConditions()); + assertThat(actualHoldout.getAudienceConditions(), is(expectedHoldout.getAudienceConditions())); + assertThat(actualHoldout.getIncludedFlags(), is(expectedHoldout.getIncludedFlags())); + assertThat(actualHoldout.getExcludedFlags(), is(expectedHoldout.getExcludedFlags())); + verifyVariations(actualHoldout.getVariations(), expectedHoldout.getVariations()); + verifyTrafficAllocations(actualHoldout.getTrafficAllocation(), + expectedHoldout.getTrafficAllocation()); + } + } + private static void verifyFeatureFlags(List actual, List expected) { assertEquals(expected.size(), actual.size()); for (int i = 0; i < actual.size(); i++) { diff --git a/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java index 9e558bf0f..f61925137 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/HoldoutTest.java @@ -16,12 +16,21 @@ */ package com.optimizely.ab.config; -import com.optimizely.ab.config.audience.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; import org.junit.Test; -import static org.junit.Assert.*; -import java.io.IOException; -import java.util.*; +import com.optimizely.ab.config.audience.AndCondition; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.EmptyCondition; +import com.optimizely.ab.config.audience.NotCondition; +import com.optimizely.ab.config.audience.OrCondition; public class HoldoutTest { @@ -194,7 +203,6 @@ private Holdout makeMockHoldoutWithStatus(Holdout.HoldoutStatus status, Conditio Collections.emptyList(), audienceConditions, Collections.emptyList(), - Collections.emptyMap(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList() diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index faacfda76..c6ee80483 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -20,6 +20,7 @@ import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.AudienceIdCondition; import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.EmptyCondition; import com.optimizely.ab.config.audience.OrCondition; import com.optimizely.ab.config.audience.UserAttribute; @@ -488,6 +489,11 @@ public class ValidProjectConfigV4 { VARIATION_BASIC_EXPERIMENT_VARIATION_A_KEY, Collections.emptyList() ); + private static final Variation VARIATION_HOLDOUT_VARIATION_OFF = new Variation( + "$opt_dummy_variation_id", + "ho_off_key", + false + ); private static final String VARIATION_BASIC_EXPERIMENT_VARIATION_B_ID = "3433458314"; private static final String VARIATION_BASIC_EXPERIMENT_VARIATION_B_KEY = "B"; private static final Variation VARIATION_BASIC_EXPERIMENT_VARIATION_B = new Variation( @@ -529,6 +535,25 @@ public class ValidProjectConfigV4 { ) ) ); + private static final Holdout HOLDOUT_BASIC_HOLDOUT = new Holdout( + "10075323428", + "basic_holdout", + Holdout.HoldoutStatus.RUNNING.toString(), + Collections.emptyList(), + null, + DatafileProjectConfigTestUtils.createListOfObjects( + VARIATION_HOLDOUT_VARIATION_OFF + ), + DatafileProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + "327323", + 100 + ) + ), + Collections.emptyList(), + Collections.emptyList(), + "" + ); private static final String LAYER_TYPEDAUDIENCE_EXPERIMENT_ID = "1630555627"; private static final String EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT_ID = "1323241597"; public static final String EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT_KEY = "typed_audience_experiment"; @@ -1461,6 +1486,105 @@ public static ProjectConfig generateValidProjectConfigV4() { typedAudiences, events, experiments, + null, + featureFlags, + groups, + rollouts, + integrations + ); + } + + public static ProjectConfig generateValidProjectConfigV4_holdout() { + + // list attributes + List attributes = new ArrayList(); + attributes.add(ATTRIBUTE_HOUSE); + attributes.add(ATTRIBUTE_NATIONALITY); + attributes.add(ATTRIBUTE_OPT); + attributes.add(ATTRIBUTE_BOOLEAN); + attributes.add(ATTRIBUTE_INTEGER); + attributes.add(ATTRIBUTE_DOUBLE); + attributes.add(ATTRIBUTE_EMPTY); + + // list audiences + List audiences = new ArrayList(); + audiences.add(AUDIENCE_GRYFFINDOR); + audiences.add(AUDIENCE_SLYTHERIN); + audiences.add(AUDIENCE_ENGLISH_CITIZENS); + audiences.add(AUDIENCE_WITH_MISSING_VALUE); + + List typedAudiences = new ArrayList(); + typedAudiences.add(TYPED_AUDIENCE_BOOL); + typedAudiences.add(TYPED_AUDIENCE_EXACT_INT); + typedAudiences.add(TYPED_AUDIENCE_INT); + typedAudiences.add(TYPED_AUDIENCE_DOUBLE); + typedAudiences.add(TYPED_AUDIENCE_GRYFFINDOR); + typedAudiences.add(TYPED_AUDIENCE_SLYTHERIN); + typedAudiences.add(TYPED_AUDIENCE_ENGLISH_CITIZENS); + typedAudiences.add(AUDIENCE_WITH_MISSING_VALUE); + + // list events + List events = new ArrayList(); + events.add(EVENT_BASIC_EVENT); + events.add(EVENT_PAUSED_EXPERIMENT); + events.add(EVENT_LAUNCHED_EXPERIMENT_ONLY); + + // list experiments + List experiments = new ArrayList(); + experiments.add(EXPERIMENT_BASIC_EXPERIMENT); + experiments.add(EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT); + experiments.add(EXPERIMENT_TYPEDAUDIENCE_WITH_AND_EXPERIMENT); + experiments.add(EXPERIMENT_TYPEDAUDIENCE_LEAF_EXPERIMENT); + experiments.add(EXPERIMENT_MULTIVARIATE_EXPERIMENT); + experiments.add(EXPERIMENT_DOUBLE_FEATURE_EXPERIMENT); + experiments.add(EXPERIMENT_PAUSED_EXPERIMENT); + experiments.add(EXPERIMENT_LAUNCHED_EXPERIMENT); + experiments.add(EXPERIMENT_WITH_MALFORMED_AUDIENCE); + + // list holdouts + List holdouts = new ArrayList(); + holdouts.add(HOLDOUT_BASIC_HOLDOUT); + + // list featureFlags + List featureFlags = new ArrayList(); + featureFlags.add(FEATURE_FLAG_BOOLEAN_FEATURE); + featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_DOUBLE); + featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_INTEGER); + featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN); + featureFlags.add(FEATURE_FLAG_SINGLE_VARIABLE_STRING); + featureFlags.add(FEATURE_FLAG_MULTI_VARIATE_FEATURE); + featureFlags.add(FEATURE_FLAG_MULTI_VARIATE_FUTURE_FEATURE); + featureFlags.add(FEATURE_FLAG_MUTEX_GROUP_FEATURE); + + List groups = new ArrayList(); + groups.add(GROUP_1); + groups.add(GROUP_2); + + // list rollouts + List rollouts = new ArrayList(); + rollouts.add(ROLLOUT_1); + rollouts.add(ROLLOUT_2); + rollouts.add(ROLLOUT_3); + + List integrations = new ArrayList<>(); + integrations.add(odpIntegration); + + return new DatafileProjectConfig( + ACCOUNT_ID, + ANONYMIZE_IP, + SEND_FLAG_DECISIONS, + BOT_FILTERING, + PROJECT_ID, + REVISION, + SDK_KEY, + ENVIRONMENT_KEY, + VERSION, + attributes, + audiences, + typedAudiences, + events, + experiments, + holdouts, featureFlags, groups, rollouts, diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java index 1844fa967..135db70f6 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonSimpleConfigParserTest.java @@ -16,35 +16,39 @@ */ package com.optimizely.ab.config.parser; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.FeatureVariable; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.audience.AudienceIdCondition; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.UserAttribute; -import com.optimizely.ab.internal.ConditionUtils; -import com.optimizely.ab.internal.InvalidAudienceCondition; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.json.JSONArray; import org.json.JSONObject; -import org.junit.Ignore; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigHoldoutJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV2; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4_holdout; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; -import static org.junit.Assert.*; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.UserAttribute; +import com.optimizely.ab.internal.ConditionUtils; +import com.optimizely.ab.internal.InvalidAudienceCondition; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Tests for {@link JsonSimpleConfigParser}. @@ -81,6 +85,15 @@ public void parseProjectConfigV4() throws Exception { verifyProjectConfig(actual, expected); } + @Test + public void parseProjectConfigWithHoldouts() throws Exception { + JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigHoldoutJsonV4()); + ProjectConfig expected = validProjectConfigV4_holdout(); + + verifyProjectConfig(actual, expected); + } + @Test public void parseNullFeatureEnabledProjectConfigV4() throws Exception { JsonSimpleConfigParser parser = new JsonSimpleConfigParser(); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java index 418cb2494..7d165ffbc 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java @@ -333,6 +333,7 @@ private ProjectConfig generateOptimizelyConfig() { ) ) ), + null, asList( new FeatureFlag( "4195505407", diff --git a/core-api/src/test/resources/config/holdouts-project-config.json b/core-api/src/test/resources/config/holdouts-project-config.json new file mode 100644 index 000000000..d2adcacce --- /dev/null +++ b/core-api/src/test/resources/config/holdouts-project-config.json @@ -0,0 +1,977 @@ +{ + "accountId": "2360254204", + "anonymizeIP": true, + "botFiltering": true, + "sendFlagDecisions": true, + "projectId": "3918735994", + "revision": "1480511547", + "sdkKey": "ValidProjectConfigV4", + "environmentKey": "production", + "version": "4", + "audiences": [ + { + "id": "3468206642", + "name": "Gryffindors", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\":\"Gryffindor\"}]]]" + }, + { + "id": "3988293898", + "name": "Slytherins", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\":\"Slytherin\"}]]]" + }, + { + "id": "4194404272", + "name": "english_citizens", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"nationality\", \"type\": \"custom_attribute\", \"value\":\"English\"}]]]" + }, + { + "id": "2196265320", + "name": "audience_with_missing_value", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"nationality\", \"type\": \"custom_attribute\", \"value\": \"English\"}, {\"name\": \"nationality\", \"type\": \"custom_attribute\"}]]]" + } + ], + "typedAudiences": [ + { + "id": "3468206643", + "name": "BOOL", + "conditions": ["and", ["or", ["or", {"name": "booleanKey", "type": "custom_attribute", "match":"exact", "value":true}]]] + }, + { + "id": "3468206646", + "name": "INTEXACT", + "conditions": ["and", ["or", ["or", {"name": "integerKey", "type": "custom_attribute", "match":"exact", "value":1.0}]]] + }, + { + "id": "3468206644", + "name": "INT", + "conditions": ["and", ["or", ["or", {"name": "integerKey", "type": "custom_attribute", "match":"gt", "value":1.0}]]] + }, + { + "id": "3468206645", + "name": "DOUBLE", + "conditions": ["and", ["or", ["or", {"name": "doubleKey", "type": "custom_attribute", "match":"lt", "value":100.0}]]] + }, + { + "id": "3468206642", + "name": "Gryffindors", + "conditions": ["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "match":"exact", "value":"Gryffindor"}]]] + }, + { + "id": "3988293898", + "name": "Slytherins", + "conditions": ["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "match":"substring", "value":"Slytherin"}]]] + }, + { + "id": "4194404272", + "name": "english_citizens", + "conditions": ["and", ["or", ["or", {"name": "nationality", "type": "custom_attribute", "match":"exact", "value":"English"}]]] + }, + { + "id": "2196265320", + "name": "audience_with_missing_value", + "conditions": ["and", ["or", ["or", {"name": "nationality", "type": "custom_attribute", "value": "English"}, {"name": "nationality", "type": "custom_attribute"}]]] + } + ], + "attributes": [ + { + "id": "553339214", + "key": "house" + }, + { + "id": "58339410", + "key": "nationality" + }, + { + "id": "583394100", + "key": "$opt_test" + }, + { + "id": "323434545", + "key": "booleanKey" + }, + { + "id": "616727838", + "key": "integerKey" + }, + { + "id": "808797686", + "key": "doubleKey" + }, + { + "id": "808797686", + "key": "" + } + ], + "events": [ + { + "id": "3785620495", + "key": "basic_event", + "experimentIds": [ + "1323241596", + "2738374745", + "3042640549", + "3262035800", + "3072915611" + ] + }, + { + "id": "3195631717", + "key": "event_with_paused_experiment", + "experimentIds": [ + "2667098701" + ] + }, + { + "id": "1987018666", + "key": "event_with_launched_experiments_only", + "experimentIds": [ + "3072915611" + ] + } + ], + "experiments": [ + { + "id": "1323241596", + "key": "basic_experiment", + "layerId": "1630555626", + "status": "Running", + "variations": [ + { + "id": "1423767502", + "key": "A", + "variables": [] + }, + { + "id": "3433458314", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1423767502", + "endOfRange": 5000 + }, + { + "entityId": "3433458314", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "forcedVariations": { + "Harry Potter": "A", + "Tom Riddle": "B" + } + }, + { + "id": "1323241597", + "key": "typed_audience_experiment", + "layerId": "1630555627", + "status": "Running", + "variations": [ + { + "id": "1423767503", + "key": "A", + "variables": [] + }, + { + "id": "3433458315", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1423767503", + "endOfRange": 5000 + }, + { + "entityId": "3433458315", + "endOfRange": 10000 + } + ], + "audienceIds": ["3468206643", "3468206644", "3468206646", "3468206645"], + "audienceConditions" : ["or", "3468206643", "3468206644", "3468206646", "3468206645" ], + "forcedVariations": {} + }, + { + "id": "1323241598", + "key": "typed_audience_experiment_with_and", + "layerId": "1630555628", + "status": "Running", + "variations": [ + { + "id": "1423767504", + "key": "A", + "variables": [] + }, + { + "id": "3433458316", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1423767504", + "endOfRange": 5000 + }, + { + "entityId": "3433458316", + "endOfRange": 10000 + } + ], + "audienceIds": ["3468206643", "3468206644", "3468206645"], + "audienceConditions" : ["and", "3468206643", "3468206644", "3468206645"], + "forcedVariations": {} + }, + { + "id": "1323241599", + "key": "typed_audience_experiment_leaf_condition", + "layerId": "1630555629", + "status": "Running", + "variations": [ + { + "id": "1423767505", + "key": "A", + "variables": [] + }, + { + "id": "3433458317", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1423767505", + "endOfRange": 5000 + }, + { + "entityId": "3433458317", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "audienceConditions" : "3468206643", + "forcedVariations": {} + }, + { + "id": "3262035800", + "key": "multivariate_experiment", + "layerId": "3262035800", + "status": "Running", + "variations": [ + { + "id": "1880281238", + "key": "Fred", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "F" + }, + { + "id": "4052219963", + "value": "red" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s1\",\"k2\":103.5,\"k3\":false,\"k4\":{\"kk1\":\"ss1\",\"kk2\":true}}" + } + ] + }, + { + "id": "3631049532", + "key": "Feorge", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "F" + }, + { + "id": "4052219963", + "value": "eorge" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s2\",\"k2\":203.5,\"k3\":true,\"k4\":{\"kk1\":\"ss2\",\"kk2\":true}}" + } + ] + }, + { + "id": "4204375027", + "key": "Gred", + "featureEnabled": false, + "variables": [ + { + "id": "675244127", + "value": "G" + }, + { + "id": "4052219963", + "value": "red" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s3\",\"k2\":303.5,\"k3\":true,\"k4\":{\"kk1\":\"ss3\",\"kk2\":false}}" + } + ] + }, + { + "id": "2099211198", + "key": "George", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "G" + }, + { + "id": "4052219963", + "value": "eorge" + }, + { + "id": "4111661000", + "value": "{\"k1\":\"s4\",\"k2\":403.5,\"k3\":false,\"k4\":{\"kk1\":\"ss4\",\"kk2\":true}}" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1880281238", + "endOfRange": 2500 + }, + { + "entityId": "3631049532", + "endOfRange": 5000 + }, + { + "entityId": "4204375027", + "endOfRange": 7500 + }, + { + "entityId": "2099211198", + "endOfRange": 10000 + } + ], + "audienceIds": [ + "3468206642" + ], + "forcedVariations": { + "Fred": "Fred", + "Feorge": "Feorge", + "Gred": "Gred", + "George": "George" + } + }, + { + "id": "2201520193", + "key": "double_single_variable_feature_experiment", + "layerId": "1278722008", + "status": "Running", + "variations": [ + { + "id": "1505457580", + "key": "pi_variation", + "featureEnabled": true, + "variables": [ + { + "id": "4111654444", + "value": "3.14" + } + ] + }, + { + "id": "119616179", + "key": "euler_variation", + "variables": [ + { + "id": "4111654444", + "value": "2.718" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1505457580", + "endOfRange": 4000 + }, + { + "entityId": "119616179", + "endOfRange": 8000 + } + ], + "audienceIds": ["3988293898"], + "forcedVariations": {} + }, + { + "id": "2667098701", + "key": "paused_experiment", + "layerId": "3949273892", + "status": "Paused", + "variations": [ + { + "id": "391535909", + "key": "Control", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "391535909", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "forcedVariations": { + "Harry Potter": "Control" + } + }, + { + "id": "3072915611", + "key": "launched_experiment", + "layerId": "3587821424", + "status": "Launched", + "variations": [ + { + "id": "1647582435", + "key": "launch_control", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1647582435", + "endOfRange": 8000 + } + ], + "audienceIds": [], + "forcedVariations": {} + }, + { + "id": "748215081", + "key": "experiment_with_malformed_audience", + "layerId": "1238149537", + "status": "Running", + "variations": [ + { + "id": "535538389", + "key": "var1", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "535538389", + "endOfRange": 10000 + } + ], + "audienceIds": ["2196265320"], + "forcedVariations": {} + } + ], + "holdouts": [ + { + "audienceIds": [], + "id": "10075323428", + "key": "basic_holdout", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 100, + "entityId": "327323" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "$opt_dummy_variation_id", + "key": "ho_off_key" + } + ] + } + ], + "groups": [ + { + "id": "1015968292", + "policy": "random", + "experiments": [ + { + "id": "2738374745", + "key": "first_grouped_experiment", + "layerId": "3301900159", + "status": "Running", + "variations": [ + { + "id": "2377378132", + "key": "A", + "variables": [] + }, + { + "id": "1179171250", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "2377378132", + "endOfRange": 5000 + }, + { + "entityId": "1179171250", + "endOfRange": 10000 + } + ], + "audienceIds": [ + "3468206642" + ], + "forcedVariations": { + "Harry Potter": "A", + "Tom Riddle": "B" + } + }, + { + "id": "3042640549", + "key": "second_grouped_experiment", + "layerId": "2625300442", + "status": "Running", + "variations": [ + { + "id": "1558539439", + "key": "A", + "variables": [] + }, + { + "id": "2142748370", + "key": "B", + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "1558539439", + "endOfRange": 5000 + }, + { + "entityId": "2142748370", + "endOfRange": 10000 + } + ], + "audienceIds": [ + "3468206642" + ], + "forcedVariations": { + "Hermione Granger": "A", + "Ronald Weasley": "B" + } + } + ], + "trafficAllocation": [ + { + "entityId": "2738374745", + "endOfRange": 4000 + }, + { + "entityId": "3042640549", + "endOfRange": 8000 + } + ] + }, + { + "id": "2606208781", + "policy": "random", + "experiments": [ + { + "id": "4138322202", + "key": "mutex_group_2_experiment_1", + "layerId": "3755588495", + "status": "Running", + "variations": [ + { + "id": "1394671166", + "key": "mutex_group_2_experiment_1_variation_1", + "featureEnabled": true, + "variables": [ + { + "id": "2059187672", + "value": "mutex_group_2_experiment_1_variation_1" + } + ] + } + ], + "audienceIds": [], + "forcedVariations": {}, + "trafficAllocation": [ + { + "entityId": "1394671166", + "endOfRange": 10000 + } + ] + }, + { + "id": "1786133852", + "key": "mutex_group_2_experiment_2", + "layerId": "3818002538", + "status": "Running", + "variations": [ + { + "id": "1619235542", + "key": "mutex_group_2_experiment_2_variation_2", + "featureEnabled": true, + "variables": [ + { + "id": "2059187672", + "value": "mutex_group_2_experiment_2_variation_2" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1619235542", + "endOfRange": 10000 + } + ], + "audienceIds": [], + "forcedVariations": {} + } + ], + "trafficAllocation": [ + { + "entityId": "4138322202", + "endOfRange": 5000 + }, + { + "entityId": "1786133852", + "endOfRange": 10000 + } + ] + } + ], + "featureFlags": [ + { + "id": "4195505407", + "key": "boolean_feature", + "rolloutId": "", + "experimentIds": [], + "variables": [] + }, + { + "id": "3926744821", + "key": "double_single_variable_feature", + "rolloutId": "", + "experimentIds": ["2201520193"], + "variables": [ + { + "id": "4111654444", + "key": "double_variable", + "type": "double", + "defaultValue": "14.99" + } ] + }, + { + "id": "3281420120", + "key": "integer_single_variable_feature", + "rolloutId": "2048875663", + "experimentIds": [], + "variables": [ + { + "id": "593964691", + "key": "integer_variable", + "type": "integer", + "defaultValue": "7" + } + ] + }, + { + "id": "2591051011", + "key": "boolean_single_variable_feature", + "rolloutId": "", + "experimentIds": [], + "variables": [ + { + "id": "3974680341", + "key": "boolean_variable", + "type": "boolean", + "defaultValue": "true" + } + ] + }, + { + "id": "2079378557", + "key": "string_single_variable_feature", + "rolloutId": "1058508303", + "experimentIds": [], + "variables": [ + { + "id": "2077511132", + "key": "string_variable", + "type": "string", + "defaultValue": "wingardium leviosa" + } + ] + }, + { + "id": "3263342226", + "key": "multi_variate_feature", + "rolloutId": "813411034", + "experimentIds": ["3262035800"], + "variables": [ + { + "id": "675244127", + "key": "first_letter", + "type": "string", + "defaultValue": "H" + }, + { + "id": "4052219963", + "key": "rest_of_name", + "type": "string", + "defaultValue": "arry" + }, + { + "id": "4111661000", + "key": "json_patched", + "type": "string", + "subType": "json", + "defaultValue": "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}" + } + ] + }, + { + "id": "3263342227", + "key": "multi_variate_future_feature", + "rolloutId": "813411034", + "experimentIds": ["3262035800"], + "variables": [ + { + "id": "4111661001", + "key": "json_native", + "type": "json", + "defaultValue": "{\"k1\":\"v1\",\"k2\":3.5,\"k3\":true,\"k4\":{\"kk1\":\"vv1\",\"kk2\":false}}" + }, + { + "id": "4111661002", + "key": "future_variable", + "type": "future_type", + "defaultValue": "future_value" + } + ] + }, + { + "id": "3263342226", + "key": "mutex_group_feature", + "rolloutId": "", + "experimentIds": ["4138322202", "1786133852"], + "variables": [ + { + "id": "2059187672", + "key": "correlating_variation_name", + "type": "string", + "defaultValue": "null" + } + ] + } + ], + "rollouts": [ + { + "id": "1058508303", + "experiments": [ + { + "id": "1785077004", + "key": "1785077004", + "status": "Running", + "layerId": "1058508303", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "1566407342", + "key": "1566407342", + "featureEnabled": true, + "variables": [ + { + "id": "2077511132", + "value": "lumos" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "1566407342", + "endOfRange": 5000 + } + ] + } + ] + }, + { + "id": "813411034", + "experiments": [ + { + "id": "3421010877", + "key": "3421010877", + "status": "Running", + "layerId": "813411034", + "audienceIds": ["3468206642"], + "forcedVariations": {}, + "variations": [ + { + "id": "521740985", + "key": "521740985", + "variables": [ + { + "id": "675244127", + "value": "G" + }, + { + "id": "4052219963", + "value": "odric" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "521740985", + "endOfRange": 5000 + } + ] + }, + { + "id": "600050626", + "key": "600050626", + "status": "Running", + "layerId": "813411034", + "audienceIds": ["3988293898"], + "forcedVariations": {}, + "variations": [ + { + "id": "180042646", + "key": "180042646", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "S" + }, + { + "id": "4052219963", + "value": "alazar" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "180042646", + "endOfRange": 5000 + } + ] + }, + { + "id": "2637642575", + "key": "2637642575", + "status": "Running", + "layerId": "813411034", + "audienceIds": ["4194404272"], + "forcedVariations": {}, + "variations": [ + { + "id": "2346257680", + "key": "2346257680", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "D" + }, + { + "id": "4052219963", + "value": "udley" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "2346257680", + "endOfRange": 5000 + } + ] + }, + { + "id": "828245624", + "key": "828245624", + "status": "Running", + "layerId": "813411034", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "3137445031", + "key": "3137445031", + "featureEnabled": true, + "variables": [ + { + "id": "675244127", + "value": "M" + }, + { + "id": "4052219963", + "value": "uggle" + } + ] + } + ], + "trafficAllocation": [ + { + "entityId": "3137445031", + "endOfRange": 5000 + } + ] + } + ] + }, + { + "id": "2048875663", + "experiments": [ + { + "id": "3794675122", + "key": "3794675122", + "status": "Running", + "layerId": "2048875663", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "589640735", + "key": "589640735", + "featureEnabled": true, + "variables": [] + } + ], + "trafficAllocation": [ + { + "entityId": "589640735", + "endOfRange": 10000 + } + ] + } + ] + } + ], + "integrations": [ + { + "key": "odp", + "host": "https://example.com", + "publicKey": "test-key" + } + ] +} From f2ce3aa38e4c7b4d2443d632fc2b23bf68b5a9e0 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sun, 20 Jul 2025 14:47:54 +0600 Subject: [PATCH 09/19] Sampel json parset test done for holdout --- .../optimizely/ab/config/ExperimentCore.java | 1 - .../ab/config/ValidProjectConfigV4.java | 103 +++++++++++++++++- .../config/holdouts-project-config.json | 89 ++++++++++++++- 3 files changed, 188 insertions(+), 5 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java index 05af575b0..9c67c942b 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ExperimentCore.java @@ -39,7 +39,6 @@ public interface ExperimentCore extends IdKeyMapped { List getTrafficAllocation(); Map getVariationKeyToVariationMap(); Map getVariationIdToVariationMap(); - Map getUserIdToVariationKeyMap(); default String serializeConditions(Map audiencesMap) { Condition condition = this.getAudienceConditions(); diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index c6ee80483..2cd27fb71 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -547,13 +547,106 @@ public class ValidProjectConfigV4 { DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( "327323", - 100 + 500 ) ), - Collections.emptyList(), - Collections.emptyList(), + null, + null, "" ); + + private static final Holdout HOLDOUT_ZERO_TRAFFIC_HOLDOUT = new Holdout( + "1007532345428", + "holdout_zero_traffic", + Holdout.HoldoutStatus.RUNNING.toString(), + Collections.emptyList(), + null, + DatafileProjectConfigTestUtils.createListOfObjects( + VARIATION_HOLDOUT_VARIATION_OFF + ), + DatafileProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + "327323", + 0 + ) + ), + null, + null, + "" + ); + + private static final Holdout HOLDOUT_INCLUDED_FLAGS_HOLDOUT = new Holdout( + "1007543323427", + "holdout_included_flags", + Holdout.HoldoutStatus.RUNNING.toString(), + Collections.emptyList(), + null, + DatafileProjectConfigTestUtils.createListOfObjects( + VARIATION_HOLDOUT_VARIATION_OFF + ), + DatafileProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + "327323", + 2000 + ) + ), + DatafileProjectConfigTestUtils.createListOfObjects( + "4195505407", + "3926744821", + "3281420120" + ), + null, + "" + ); + + private static final Holdout HOLDOUT_EXCLUDED_FLAGS_HOLDOUT = new Holdout( + "100753234214", + "holdout_excluded_flags", + Holdout.HoldoutStatus.RUNNING.toString(), + Collections.emptyList(), + null, + DatafileProjectConfigTestUtils.createListOfObjects( + VARIATION_HOLDOUT_VARIATION_OFF + ), + DatafileProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + "327323", + 1500 + ) + ), + null, + DatafileProjectConfigTestUtils.createListOfObjects( + "2591051011", + "2079378557", + "3263342226" + ), + "" + ); + + private static final Holdout HOLDOUT_TYPEDAUDIENCE_HOLDOUT = new Holdout( + "10075323429", + "typed_audience_holdout", + Holdout.HoldoutStatus.RUNNING.toString(), + DatafileProjectConfigTestUtils.createListOfObjects( + AUDIENCE_BOOL_ID, + AUDIENCE_INT_ID, + AUDIENCE_INT_EXACT_ID, + AUDIENCE_DOUBLE_ID + ), + AUDIENCE_COMBINATION, + DatafileProjectConfigTestUtils.createListOfObjects( + VARIATION_HOLDOUT_VARIATION_OFF + ), + DatafileProjectConfigTestUtils.createListOfObjects( + new TrafficAllocation( + "327323", + 1000 + ) + ), + Collections.emptyList(), + Collections.emptyList(), + "" + ); private static final String LAYER_TYPEDAUDIENCE_EXPERIMENT_ID = "1630555627"; private static final String EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT_ID = "1323241597"; public static final String EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT_KEY = "typed_audience_experiment"; @@ -1543,7 +1636,11 @@ public static ProjectConfig generateValidProjectConfigV4_holdout() { // list holdouts List holdouts = new ArrayList(); + holdouts.add(HOLDOUT_ZERO_TRAFFIC_HOLDOUT); + holdouts.add(HOLDOUT_INCLUDED_FLAGS_HOLDOUT); holdouts.add(HOLDOUT_BASIC_HOLDOUT); + holdouts.add(HOLDOUT_TYPEDAUDIENCE_HOLDOUT); + holdouts.add(HOLDOUT_EXCLUDED_FLAGS_HOLDOUT); // list featureFlags List featureFlags = new ArrayList(); diff --git a/core-api/src/test/resources/config/holdouts-project-config.json b/core-api/src/test/resources/config/holdouts-project-config.json index d2adcacce..639340674 100644 --- a/core-api/src/test/resources/config/holdouts-project-config.json +++ b/core-api/src/test/resources/config/holdouts-project-config.json @@ -475,6 +475,49 @@ } ], "holdouts": [ + { + "audienceIds": [], + "id": "1007532345428", + "key": "holdout_zero_traffic", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 0, + "entityId": "327323" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "$opt_dummy_variation_id", + "key": "ho_off_key" + } + ] + }, + { + "audienceIds": [], + "id": "1007543323427", + "key": "holdout_included_flags", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 2000, + "entityId": "327323" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "$opt_dummy_variation_id", + "key": "ho_off_key" + } + ], + "includedFlags": [ + "4195505407", + "3926744821", + "3281420120" + ] + }, { "audienceIds": [], "id": "10075323428", @@ -482,7 +525,7 @@ "status": "Running", "trafficAllocation": [ { - "endOfRange": 100, + "endOfRange": 500, "entityId": "327323" } ], @@ -493,6 +536,50 @@ "key": "ho_off_key" } ] + }, + { + "id": "10075323429", + "key": "typed_audience_holdout", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 1000, + "entityId": "327323" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "$opt_dummy_variation_id", + "key": "ho_off_key" + } + ], + "audienceIds": ["3468206643", "3468206644", "3468206646", "3468206645"], + "audienceConditions" : ["or", "3468206643", "3468206644", "3468206646", "3468206645" ], + }, + { + "audienceIds": [], + "id": "100753234214", + "key": "holdout_excluded_flags", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 1500, + "entityId": "327323" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "$opt_dummy_variation_id", + "key": "ho_off_key" + } + ], + "excludedFlags": [ + "2591051011", + "2079378557", + "3263342226" + ] } ], "groups": [ From 9c985ac8283ba9db89a24c1c4969508be2aef7fd Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sun, 20 Jul 2025 14:52:08 +0600 Subject: [PATCH 10/19] Add liscense for holdoutconfig --- .../optimizely/ab/config/HoldoutConfig.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java index b9e8c2e79..227a7bd28 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java @@ -1,9 +1,32 @@ +/** + * + * Copyright 2016-2019, 2021, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package com.optimizely.ab.config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; /** * HoldoutConfig manages collections of Holdout objects and their relationships to flags. From ba6de31053f508feca1c168f41e978c86e9b76ca Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sun, 20 Jul 2025 15:54:11 +0600 Subject: [PATCH 11/19] Added holdout verification for GsonConfigParserTest --- .../ab/config/parser/GsonConfigParser.java | 12 +++-- .../ab/config/parser/GsonHelpers.java | 1 - .../parser/HoldoutGsonDeserializer.java | 38 +++++++++++++ .../config/parser/GsonConfigParserTest.java | 54 +++++++++++-------- .../config/holdouts-project-config.json | 2 +- 5 files changed, 81 insertions(+), 26 deletions(-) create mode 100644 core-api/src/main/java/com/optimizely/ab/config/parser/HoldoutGsonDeserializer.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java index 972d76431..314f2dd23 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonConfigParser.java @@ -16,14 +16,19 @@ */ package com.optimizely.ab.config.parser; +import javax.annotation.Nonnull; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.optimizely.ab.config.*; +import com.optimizely.ab.config.DatafileProjectConfig; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.Group; +import com.optimizely.ab.config.Holdout; +import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.Audience; import com.optimizely.ab.config.audience.TypedAudience; -import javax.annotation.Nonnull; - /** * {@link Gson}-based config parser implementation. */ @@ -35,6 +40,7 @@ public GsonConfigParser() { .registerTypeAdapter(Audience.class, new AudienceGsonDeserializer()) .registerTypeAdapter(TypedAudience.class, new AudienceGsonDeserializer()) .registerTypeAdapter(Experiment.class, new ExperimentGsonDeserializer()) + .registerTypeAdapter(Holdout.class, new HoldoutGsonDeserializer()) .registerTypeAdapter(FeatureFlag.class, new FeatureFlagGsonDeserializer()) .registerTypeAdapter(Group.class, new GroupGsonDeserializer()) .registerTypeAdapter(DatafileProjectConfig.class, new DatafileGsonDeserializer()) diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java index 5dad7880b..6cc54edd9 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java @@ -156,7 +156,6 @@ static Experiment parseExperiment(JsonObject experimentJson, JsonDeserialization static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserializationContext context) { String id = holdoutJson.get("id").getAsString(); String key = holdoutJson.get("key").getAsString(); - JsonElement holdoutStatusJson = holdoutJson.get("status"); String status = holdoutJson.get("status").getAsString(); JsonArray audienceIdsJson = holdoutJson.getAsJsonArray("audienceIds"); diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/HoldoutGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/HoldoutGsonDeserializer.java new file mode 100644 index 000000000..f64f355d4 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/HoldoutGsonDeserializer.java @@ -0,0 +1,38 @@ +/** + * + * Copyright 2016-2017, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config.parser; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.optimizely.ab.config.Holdout; + +final class HoldoutGsonDeserializer implements JsonDeserializer { + + @Override + public Holdout deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + + JsonObject jsonObject = json.getAsJsonObject(); + + return GsonHelpers.parseHoldout(jsonObject, context); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java index ea0d9cac8..ec02aaad0 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/GsonConfigParserTest.java @@ -16,40 +16,43 @@ */ package com.optimizely.ab.config.parser; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.FeatureVariable; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.audience.Audience; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.TypedAudience; -import com.optimizely.ab.internal.InvalidAudienceCondition; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - import java.lang.reflect.Type; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigHoldoutJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV2; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4_holdout; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.*; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.Audience; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.TypedAudience; +import com.optimizely.ab.internal.InvalidAudienceCondition; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Tests for {@link GsonConfigParser}. @@ -86,6 +89,15 @@ public void parseProjectConfigV4() throws Exception { verifyProjectConfig(actual, expected); } + @Test + public void parseProjectConfigHoldoutV4() throws Exception { + GsonConfigParser parser = new GsonConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigHoldoutJsonV4()); + ProjectConfig expected = validProjectConfigV4_holdout(); + + verifyProjectConfig(actual, expected); + } + @Test public void parseNullFeatureEnabledProjectConfigV4() throws Exception { GsonConfigParser parser = new GsonConfigParser(); diff --git a/core-api/src/test/resources/config/holdouts-project-config.json b/core-api/src/test/resources/config/holdouts-project-config.json index 639340674..5a83fad17 100644 --- a/core-api/src/test/resources/config/holdouts-project-config.json +++ b/core-api/src/test/resources/config/holdouts-project-config.json @@ -555,7 +555,7 @@ } ], "audienceIds": ["3468206643", "3468206644", "3468206646", "3468206645"], - "audienceConditions" : ["or", "3468206643", "3468206644", "3468206646", "3468206645" ], + "audienceConditions" : ["or", "3468206643", "3468206644", "3468206646", "3468206645"] }, { "audienceIds": [], From 0ed59e829d9aa100234d23d3fed8645fc40fcffc Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sun, 20 Jul 2025 18:18:31 +0600 Subject: [PATCH 12/19] Add holdout check for Jasckson and Json parser config test --- .../parser/JacksonConfigParserTest.java | 45 ++++++++++++------ .../config/parser/JsonConfigParserTest.java | 47 ++++++++++++------- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java index 733ae49a5..336c6f576 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JacksonConfigParserTest.java @@ -16,33 +16,38 @@ */ package com.optimizely.ab.config.parser; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.FeatureVariable; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.audience.Audience; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.TypedAudience; -import com.optimizely.ab.internal.InvalidAudienceCondition; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.Ignore; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.util.HashMap; -import java.util.Map; - +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigHoldoutJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV2; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4_holdout; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; -import static org.junit.Assert.*; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.Audience; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.TypedAudience; +import com.optimizely.ab.internal.InvalidAudienceCondition; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Tests for {@link JacksonConfigParser}. @@ -80,6 +85,16 @@ public void parseProjectConfigV4() throws Exception { verifyProjectConfig(actual, expected); } + @SuppressFBWarnings("NP_NULL_PARAM_DEREF") + @Test + public void parseProjectConfigHoldoutV4() throws Exception { + JacksonConfigParser parser = new JacksonConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigHoldoutJsonV4()); + ProjectConfig expected = validProjectConfigV4_holdout(); + + verifyProjectConfig(actual, expected); + } + @Test public void parseNullFeatureEnabledProjectConfigV4() throws Exception { JacksonConfigParser parser = new JacksonConfigParser(); diff --git a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java index 844d7448b..7ff22338f 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/parser/JsonConfigParserTest.java @@ -16,35 +16,40 @@ */ package com.optimizely.ab.config.parser; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.FeatureVariable; -import com.optimizely.ab.config.ProjectConfig; -import com.optimizely.ab.config.audience.AudienceIdCondition; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.UserAttribute; -import com.optimizely.ab.internal.ConditionUtils; -import com.optimizely.ab.internal.InvalidAudienceCondition; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.json.JSONArray; import org.json.JSONObject; -import org.junit.Ignore; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.nullFeatureEnabledConfigJsonV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigHoldoutJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV2; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV4; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV2; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validConfigJsonV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4_holdout; import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.verifyProjectConfig; -import static org.junit.Assert.*; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.FeatureVariable; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; +import com.optimizely.ab.config.audience.UserAttribute; +import com.optimizely.ab.internal.ConditionUtils; +import com.optimizely.ab.internal.InvalidAudienceCondition; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Tests for {@link JsonConfigParser}. @@ -81,6 +86,16 @@ public void parseProjectConfigV4() throws Exception { verifyProjectConfig(actual, expected); } + @Test + public void parseProjectConfigHoldoutV4() throws Exception { + JsonConfigParser parser = new JsonConfigParser(); + ProjectConfig actual = parser.parseProjectConfig(validConfigHoldoutJsonV4()); + ProjectConfig expected = validProjectConfigV4_holdout(); + + verifyProjectConfig(actual, expected); + } + + @Test public void parseNullFeatureEnabledProjectConfigV4() throws Exception { JsonConfigParser parser = new JsonConfigParser(); From 3d953a3b339c86b7df5f25ca17ed93815661a893 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 22 Jul 2025 10:11:14 +0600 Subject: [PATCH 13/19] Add mcp json to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index aefc53cb6..dcf3ee891 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ classes .vagrant .DS_Store .venv + +.vscode/mcp.json \ No newline at end of file From 006989ce92fb8d0c1c90acde0594405894698311 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 22 Jul 2025 23:10:21 +0600 Subject: [PATCH 14/19] Holdout config file test case added --- .../com/optimizely/ab/config/Holdout.java | 84 +++---- .../optimizely/ab/config/HoldoutConfig.java | 12 +- .../ab/config/HoldoutConfigTest.java | 233 ++++++++++++++++++ 3 files changed, 275 insertions(+), 54 deletions(-) create mode 100644 core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index 9660a9f45..81489be5e 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -14,20 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.optimizely.ab.config; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.optimizely.ab.annotations.VisibleForTesting; -import com.optimizely.ab.config.audience.AndCondition; -import com.optimizely.ab.config.audience.AudienceIdCondition; -import com.optimizely.ab.config.audience.Condition; -import com.optimizely.ab.config.audience.EmptyCondition; -import com.optimizely.ab.config.audience.NotCondition; -import com.optimizely.ab.config.audience.OrCondition; - import java.util.Collections; import java.util.List; import java.util.Map; @@ -36,6 +24,13 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.optimizely.ab.annotations.VisibleForTesting; +import com.optimizely.ab.config.audience.AudienceIdCondition; +import com.optimizely.ab.config.audience.Condition; + @Immutable @JsonIgnoreProperties(ignoreUnknown = true) public class Holdout implements ExperimentCore { @@ -57,7 +52,6 @@ public class Holdout implements ExperimentCore { // Not necessary for HO private final String layerId = ""; - public enum HoldoutStatus { RUNNING("Running"), DRAFT("Draft"), @@ -82,27 +76,27 @@ public Holdout(String id, String key) { @JsonCreator public Holdout(@JsonProperty("id") String id, - @JsonProperty("key") String key, - @JsonProperty("status") String status, - @JsonProperty("audienceIds") List audienceIds, - @JsonProperty("audienceConditions") Condition audienceConditions, - @JsonProperty("variations") List variations, - @JsonProperty("trafficAllocation") List trafficAllocation, - @JsonProperty("includedFlags") List includedFlags, - @JsonProperty("excludedFlags") List excludedFlags) { + @JsonProperty("key") String key, + @JsonProperty("status") String status, + @JsonProperty("audienceIds") List audienceIds, + @JsonProperty("audienceConditions") Condition audienceConditions, + @JsonProperty("variations") List variations, + @JsonProperty("trafficAllocation") List trafficAllocation, + @JsonProperty("includedFlags") List includedFlags, + @JsonProperty("excludedFlags") List excludedFlags) { this(id, key, status, audienceIds, audienceConditions, variations, trafficAllocation, includedFlags, excludedFlags, ""); } public Holdout(@Nonnull String id, - @Nonnull String key, - @Nullable String status, - @Nonnull List audienceIds, - @Nullable Condition audienceConditions, - @Nonnull List variations, - @Nonnull List trafficAllocation, - @Nullable List includedFlags, - @Nullable List excludedFlags, - @Nonnull String groupId) { + @Nonnull String key, + @Nullable String status, + @Nonnull List audienceIds, + @Nullable Condition audienceConditions, + @Nonnull List variations, + @Nonnull List trafficAllocation, + @Nullable List includedFlags, + @Nullable List excludedFlags, + @Nonnull String groupId) { this.id = id; this.key = key; this.status = status == null ? HoldoutStatus.RUNNING.toString() : status; @@ -157,9 +151,13 @@ public List getTrafficAllocation() { return trafficAllocation; } - public List getIncludedFlags() { return includedFlags; } + public List getIncludedFlags() { + return includedFlags; + } - public List getExcludedFlags() { return excludedFlags; } + public List getExcludedFlags() { + return excludedFlags; + } public String getGroupId() { return groupId; @@ -175,16 +173,16 @@ public boolean isRunning() { @Override public String toString() { - return "Holdout {" + - "id='" + id + '\'' + - ", key='" + key + '\'' + - ", groupId='" + groupId + '\'' + - ", status='" + status + '\'' + - ", audienceIds=" + audienceIds + - ", audienceConditions=" + audienceConditions + - ", variations=" + variations + - ", variationKeyToVariationMap=" + variationKeyToVariationMap + - ", trafficAllocation=" + trafficAllocation + - '}'; + return "Holdout {" + + "id='" + id + '\'' + + ", key='" + key + '\'' + + ", groupId='" + groupId + '\'' + + ", status='" + status + '\'' + + ", audienceIds=" + audienceIds + + ", audienceConditions=" + audienceConditions + + ", variations=" + variations + + ", variationKeyToVariationMap=" + variationKeyToVariationMap + + ", trafficAllocation=" + trafficAllocation + + '}'; } } diff --git a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java index 227a7bd28..18f8096ad 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java @@ -65,7 +65,7 @@ public HoldoutConfig(@Nonnull List allHoldouts) { * Updates internal mappings of holdouts including the id map, global list, * and per-flag inclusion/exclusion maps. */ - public void updateHoldoutMapping() { + private void updateHoldoutMapping() { holdoutIdMap.clear(); for (Holdout holdout : allHoldouts) { holdoutIdMap.put(holdout.getId(), holdout); @@ -159,14 +159,4 @@ public Holdout getHoldout(@Nonnull String id) { public List getAllHoldouts() { return Collections.unmodifiableList(allHoldouts); } - - /** - * Returns the global holdouts (those that apply to all flags by default). - * - * @return An unmodifiable list of global holdouts - */ - public List getGlobal() { - return Collections.unmodifiableList(global); - } - } \ No newline at end of file diff --git a/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java b/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java new file mode 100644 index 000000000..b384b93e1 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java @@ -0,0 +1,233 @@ +/** + * + * Copyright 2016-2019, 2021, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.config; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Before; +import org.junit.Test; + +public class HoldoutConfigTest { + + private Holdout globalHoldout; + private Holdout includedHoldout; + private Holdout excludedHoldout; + private Holdout mixedHoldout; + + @Before + public void setUp() { + // Global holdout (no included/excluded flags) + globalHoldout = new Holdout("global1", "global_holdout"); + + // Holdout with included flags + includedHoldout = new Holdout("included1", "included_holdout", null, + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyList(), Arrays.asList("flag1", "flag2"), null, ""); + + // Global holdout with excluded flags + excludedHoldout = new Holdout("excluded1", "excluded_holdout", null, + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyList(), null, Arrays.asList("flag3"), ""); + + // Another global holdout for testing + mixedHoldout = new Holdout("mixed1", "mixed_holdout"); + } + + @Test + public void testEmptyConstructor() { + HoldoutConfig config = new HoldoutConfig(); + + assertTrue(config.getAllHoldouts().isEmpty()); + assertTrue(config.getHoldoutForFlag("any_flag").isEmpty()); + assertNull(config.getHoldout("any_id")); + } + + @Test + public void testConstructorWithEmptyList() { + HoldoutConfig config = new HoldoutConfig(Collections.emptyList()); + + assertTrue(config.getAllHoldouts().isEmpty()); + assertTrue(config.getHoldoutForFlag("any_flag").isEmpty()); + assertNull(config.getHoldout("any_id")); + } + + @Test + public void testConstructorWithGlobalHoldouts() { + List holdouts = Arrays.asList(globalHoldout, mixedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + assertEquals(2, config.getAllHoldouts().size()); + assertTrue(config.getAllHoldouts().contains(globalHoldout)); + } + + @Test + public void testGetHoldout() { + List holdouts = Arrays.asList(globalHoldout, includedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + assertEquals(globalHoldout, config.getHoldout("global1")); + assertEquals(includedHoldout, config.getHoldout("included1")); + assertNull(config.getHoldout("nonexistent")); + } + + @Test + public void testGetHoldoutForFlagWithGlobalHoldouts() { + List holdouts = Arrays.asList(globalHoldout, mixedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + List flagHoldouts = config.getHoldoutForFlag("any_flag"); + assertEquals(2, flagHoldouts.size()); + assertTrue(flagHoldouts.contains(globalHoldout)); + assertTrue(flagHoldouts.contains(mixedHoldout)); + } + + @Test + public void testGetHoldoutForFlagWithIncludedHoldouts() { + List holdouts = Arrays.asList(globalHoldout, includedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + // Flag included in holdout + List flag1Holdouts = config.getHoldoutForFlag("flag1"); + assertEquals(2, flag1Holdouts.size()); + assertTrue(flag1Holdouts.contains(globalHoldout)); // Global first + assertTrue(flag1Holdouts.contains(includedHoldout)); // Included second + + List flag2Holdouts = config.getHoldoutForFlag("flag2"); + assertEquals(2, flag2Holdouts.size()); + assertTrue(flag2Holdouts.contains(globalHoldout)); + assertTrue(flag2Holdouts.contains(includedHoldout)); + + // Flag not included in holdout + List flag3Holdouts = config.getHoldoutForFlag("flag3"); + assertEquals(1, flag3Holdouts.size()); + assertTrue(flag3Holdouts.contains(globalHoldout)); // Only global + } + + @Test + public void testGetHoldoutForFlagWithExcludedHoldouts() { + List holdouts = Arrays.asList(globalHoldout, excludedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + // Flag excluded from holdout + List flag3Holdouts = config.getHoldoutForFlag("flag3"); + assertEquals(1, flag3Holdouts.size()); + assertTrue(flag3Holdouts.contains(globalHoldout)); // excludedHoldout should be filtered out + + // Flag not excluded + List flag1Holdouts = config.getHoldoutForFlag("flag1"); + assertEquals(2, flag1Holdouts.size()); + assertTrue(flag1Holdouts.contains(globalHoldout)); + assertTrue(flag1Holdouts.contains(excludedHoldout)); + } + + @Test + public void testGetHoldoutForFlagWithMixedHoldouts() { + List holdouts = Arrays.asList(globalHoldout, includedHoldout, excludedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + // flag1 is included in includedHoldout + List flag1Holdouts = config.getHoldoutForFlag("flag1"); + assertEquals(3, flag1Holdouts.size()); + assertTrue(flag1Holdouts.contains(globalHoldout)); + assertTrue(flag1Holdouts.contains(excludedHoldout)); + assertTrue(flag1Holdouts.contains(includedHoldout)); + + // flag3 is excluded from excludedHoldout + List flag3Holdouts = config.getHoldoutForFlag("flag3"); + assertEquals(1, flag3Holdouts.size()); + assertTrue(flag3Holdouts.contains(globalHoldout)); // Only global, excludedHoldout filtered out + + // flag4 has no specific inclusion/exclusion + List flag4Holdouts = config.getHoldoutForFlag("flag4"); + assertEquals(2, flag4Holdouts.size()); + assertTrue(flag4Holdouts.contains(globalHoldout)); + assertTrue(flag4Holdouts.contains(excludedHoldout)); + } + + @Test + public void testCachingBehavior() { + List holdouts = Arrays.asList(globalHoldout, includedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + // First call + List firstCall = config.getHoldoutForFlag("flag1"); + // Second call should return cached result (same object reference) + List secondCall = config.getHoldoutForFlag("flag1"); + + assertSame(firstCall, secondCall); + assertEquals(2, firstCall.size()); + } + + @Test + public void testGetAllHoldoutsIsUnmodifiable() { + List holdouts = Arrays.asList(globalHoldout, includedHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + List allHoldouts = config.getAllHoldouts(); + + try { + allHoldouts.add(mixedHoldout); + fail("Should throw UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // Expected + } + } + + @Test + public void testEmptyFlagHoldouts() { + HoldoutConfig config = new HoldoutConfig(); + + List flagHoldouts = config.getHoldoutForFlag("any_flag"); + assertTrue(flagHoldouts.isEmpty()); + + // Should return same empty list for subsequent calls (caching) + List secondCall = config.getHoldoutForFlag("any_flag"); + assertSame(flagHoldouts, secondCall); + } + + @Test + public void testHoldoutWithBothIncludedAndExcluded() { + // Create a holdout with both included and excluded flags (included takes precedence) + Holdout bothHoldout = new Holdout("both1", "both_holdout", null, + Collections.emptyList(), null, Collections.emptyList(), + Collections.emptyList(), Arrays.asList("flag1"), Arrays.asList("flag2"), ""); + + List holdouts = Arrays.asList(globalHoldout, bothHoldout); + HoldoutConfig config = new HoldoutConfig(holdouts); + + // flag1 should include bothHoldout (included takes precedence) + List flag1Holdouts = config.getHoldoutForFlag("flag1"); + assertEquals(2, flag1Holdouts.size()); + assertTrue(flag1Holdouts.contains(globalHoldout)); + assertTrue(flag1Holdouts.contains(bothHoldout)); + + // flag2 should not include bothHoldout (not in included list) + List flag2Holdouts = config.getHoldoutForFlag("flag2"); + assertEquals(1, flag2Holdouts.size()); + assertTrue(flag2Holdouts.contains(globalHoldout)); + assertFalse(flag2Holdouts.contains(bothHoldout)); + } + +} \ No newline at end of file From ccf697b18a24dc37a290d1a953cae18d0e9cd773 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 22 Jul 2025 23:52:06 +0600 Subject: [PATCH 15/19] Clean up constructor --- .../ab/config/DatafileProjectConfig.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java index c0c80b0f1..b3c6426b2 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java @@ -133,33 +133,6 @@ public DatafileProjectConfig(String accountId, String projectId, String version, ); } - // v3 constructor - public DatafileProjectConfig(String accountId, String projectId, String version, String revision, List groups, - List experiments, List holdouts, List attributes, List eventType, - List audiences, boolean anonymizeIP) { - this( - accountId, - anonymizeIP, - false, - null, - projectId, - revision, - null, - null, - version, - attributes, - audiences, - null, - eventType, - experiments, - holdouts, - null, - groups, - null, - null - ); - } - // v4 constructor public DatafileProjectConfig(String accountId, boolean anonymizeIP, From 427c1534100c187d8a920bdeaf2d13af0472bebb Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 23 Jul 2025 17:42:38 +0600 Subject: [PATCH 16/19] Remove groupId from holdout --- .../com/optimizely/ab/config/Holdout.java | 59 +++++++------------ .../ab/config/parser/GsonHelpers.java | 8 +-- .../ab/config/parser/JsonConfigParser.java | 8 +-- .../config/parser/JsonSimpleConfigParser.java | 7 +-- .../ab/config/HoldoutConfigTest.java | 12 ++-- .../ab/config/ValidProjectConfigV4.java | 15 ++--- 6 files changed, 38 insertions(+), 71 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java index 81489be5e..c757c072c 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Holdout.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Holdout.java @@ -38,8 +38,7 @@ public class Holdout implements ExperimentCore { private final String id; private final String key; private final String status; - private final String groupId; - + private final List audienceIds; private final Condition audienceConditions; private final List variations; @@ -71,44 +70,31 @@ public String toString() { @VisibleForTesting public Holdout(String id, String key) { - this(id, key, null, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null, null, ""); + this(id, key, "Running", Collections.emptyList(), null, Collections.emptyList(), Collections.emptyList(), null, null); } + // Keep only this constructor and add @JsonCreator to it @JsonCreator - public Holdout(@JsonProperty("id") String id, - @JsonProperty("key") String key, - @JsonProperty("status") String status, - @JsonProperty("audienceIds") List audienceIds, - @JsonProperty("audienceConditions") Condition audienceConditions, - @JsonProperty("variations") List variations, - @JsonProperty("trafficAllocation") List trafficAllocation, - @JsonProperty("includedFlags") List includedFlags, - @JsonProperty("excludedFlags") List excludedFlags) { - this(id, key, status, audienceIds, audienceConditions, variations, trafficAllocation, includedFlags, excludedFlags, ""); - } - - public Holdout(@Nonnull String id, - @Nonnull String key, - @Nullable String status, - @Nonnull List audienceIds, - @Nullable Condition audienceConditions, - @Nonnull List variations, - @Nonnull List trafficAllocation, - @Nullable List includedFlags, - @Nullable List excludedFlags, - @Nonnull String groupId) { + public Holdout(@JsonProperty("id") @Nonnull String id, + @JsonProperty("key") @Nonnull String key, + @JsonProperty("status") @Nonnull String status, + @JsonProperty("audienceIds") @Nonnull List audienceIds, + @JsonProperty("audienceConditions") @Nullable Condition audienceConditions, + @JsonProperty("variations") @Nonnull List variations, + @JsonProperty("trafficAllocation") @Nonnull List trafficAllocation, + @JsonProperty("includedFlags") @Nullable List includedFlags, + @JsonProperty("excludedFlags") @Nullable List excludedFlags) { this.id = id; this.key = key; - this.status = status == null ? HoldoutStatus.RUNNING.toString() : status; - this.audienceIds = Collections.unmodifiableList(audienceIds); + this.status = status; + this.audienceIds = audienceIds; this.audienceConditions = audienceConditions; - this.variations = Collections.unmodifiableList(variations); - this.trafficAllocation = Collections.unmodifiableList(trafficAllocation); + this.variations = variations; + this.trafficAllocation = trafficAllocation; this.includedFlags = includedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(includedFlags); this.excludedFlags = excludedFlags == null ? Collections.emptyList() : Collections.unmodifiableList(excludedFlags); - this.groupId = groupId; - this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations); - this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations); + this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(this.variations); + this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(this.variations); } public String getId() { @@ -151,6 +137,10 @@ public List getTrafficAllocation() { return trafficAllocation; } + public String getGroupId() { + return ""; + } + public List getIncludedFlags() { return includedFlags; } @@ -159,10 +149,6 @@ public List getExcludedFlags() { return excludedFlags; } - public String getGroupId() { - return groupId; - } - public boolean isActive() { return status.equals(Holdout.HoldoutStatus.RUNNING.toString()); } @@ -176,7 +162,6 @@ public String toString() { return "Holdout {" + "id='" + id + '\'' + ", key='" + key + '\'' - + ", groupId='" + groupId + '\'' + ", status='" + status + '\'' + ", audienceIds=" + audienceIds + ", audienceConditions=" + audienceConditions diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java index 6cc54edd9..97cf5b521 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java @@ -153,7 +153,7 @@ static Experiment parseExperiment(JsonObject experimentJson, JsonDeserialization return parseExperiment(experimentJson, "", context); } - static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserializationContext context) { + static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext context) { String id = holdoutJson.get("id").getAsString(); String key = holdoutJson.get("key").getAsString(); String status = holdoutJson.get("status").getAsString(); @@ -187,11 +187,7 @@ static Holdout parseHoldout(JsonObject holdoutJson, String groupId, JsonDeserial } } - return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags, groupId); - } - - static Holdout parseHoldout(JsonObject holdoutJson, JsonDeserializationContext context) { - return parseHoldout(holdoutJson, "", context); + return new Holdout(id, key, status, audienceIds, conditions, variations, trafficAllocations, includedFlags, excludedFlags); } static FeatureFlag parseFeatureFlag(JsonObject featureFlagJson, JsonDeserializationContext context) { diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index 4765f9860..4582e4749 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -173,12 +173,8 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } - + private List parseHoldouts(JSONArray holdoutJson) { - return parseHoldouts(holdoutJson, ""); - } - - private List parseHoldouts(JSONArray holdoutJson, String groupId) { List holdouts = new ArrayList(holdoutJson.length()); for (int i = 0; i < holdoutJson.length(); i++) { @@ -235,7 +231,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { } holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, - trafficAllocations, includedFlags, excludedFlags, groupId)); + trafficAllocations, includedFlags, excludedFlags)); } return holdouts; diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index 2ffdd6fd6..b9a170880 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -181,12 +181,7 @@ private List parseExperiments(JSONArray experimentJson, String group return experiments; } - private List parseHoldouts(JSONArray holdoutJson) { - return parseHoldouts(holdoutJson, ""); - } - - private List parseHoldouts(JSONArray holdoutJson, String groupId) { List holdouts = new ArrayList(holdoutJson.size()); for (Object obj : holdoutJson) { @@ -233,7 +228,7 @@ private List parseHoldouts(JSONArray holdoutJson, String groupId) { } holdouts.add(new Holdout(id, key, status, audienceIds, conditions, variations, - trafficAllocations, includedFlags, excludedFlags, groupId)); + trafficAllocations, includedFlags, excludedFlags)); } return holdouts; diff --git a/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java b/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java index b384b93e1..5c0b2fef1 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java +++ b/core-api/src/test/java/com/optimizely/ab/config/HoldoutConfigTest.java @@ -42,14 +42,14 @@ public void setUp() { globalHoldout = new Holdout("global1", "global_holdout"); // Holdout with included flags - includedHoldout = new Holdout("included1", "included_holdout", null, + includedHoldout = new Holdout("included1", "included_holdout", "Running", Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyList(), Arrays.asList("flag1", "flag2"), null, ""); + Collections.emptyList(), Arrays.asList("flag1", "flag2"), null); // Global holdout with excluded flags - excludedHoldout = new Holdout("excluded1", "excluded_holdout", null, + excludedHoldout = new Holdout("excluded1", "excluded_holdout", "Running", Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyList(), null, Arrays.asList("flag3"), ""); + Collections.emptyList(), null, Arrays.asList("flag3")); // Another global holdout for testing mixedHoldout = new Holdout("mixed1", "mixed_holdout"); @@ -210,9 +210,9 @@ public void testEmptyFlagHoldouts() { @Test public void testHoldoutWithBothIncludedAndExcluded() { // Create a holdout with both included and excluded flags (included takes precedence) - Holdout bothHoldout = new Holdout("both1", "both_holdout", null, + Holdout bothHoldout = new Holdout("both1", "both_holdout", "Running", Collections.emptyList(), null, Collections.emptyList(), - Collections.emptyList(), Arrays.asList("flag1"), Arrays.asList("flag2"), ""); + Collections.emptyList(), Arrays.asList("flag1"), Arrays.asList("flag2")); List holdouts = Arrays.asList(globalHoldout, bothHoldout); HoldoutConfig config = new HoldoutConfig(holdouts); diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index 2cd27fb71..a59721d4f 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -551,8 +551,7 @@ public class ValidProjectConfigV4 { ) ), null, - null, - "" + null ); private static final Holdout HOLDOUT_ZERO_TRAFFIC_HOLDOUT = new Holdout( @@ -571,8 +570,7 @@ public class ValidProjectConfigV4 { ) ), null, - null, - "" + null ); private static final Holdout HOLDOUT_INCLUDED_FLAGS_HOLDOUT = new Holdout( @@ -595,8 +593,7 @@ public class ValidProjectConfigV4 { "3926744821", "3281420120" ), - null, - "" + null ); private static final Holdout HOLDOUT_EXCLUDED_FLAGS_HOLDOUT = new Holdout( @@ -619,8 +616,7 @@ public class ValidProjectConfigV4 { "2591051011", "2079378557", "3263342226" - ), - "" + ) ); private static final Holdout HOLDOUT_TYPEDAUDIENCE_HOLDOUT = new Holdout( @@ -644,8 +640,7 @@ public class ValidProjectConfigV4 { ) ), Collections.emptyList(), - Collections.emptyList(), - "" + Collections.emptyList() ); private static final String LAYER_TYPEDAUDIENCE_EXPERIMENT_ID = "1630555627"; private static final String EXPERIMENT_TYPEDAUDIENCE_EXPERIMENT_ID = "1323241597"; From fcd2c6b4d389f8a9751fd5555a9828022873f510 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 23 Jul 2025 18:00:13 +0600 Subject: [PATCH 17/19] Add holdout public method to datafile project config --- .../ab/config/DatafileProjectConfig.java | 14 +++++++++++++- .../com/optimizely/ab/config/HoldoutConfig.java | 2 +- .../com/optimizely/ab/config/ProjectConfig.java | 15 ++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java index b3c6426b2..969eb8fb6 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java @@ -445,7 +445,19 @@ public List getExperiments() { } @Override - public List getHoldouts() { return holdoutConfig.getAllHoldouts(); } + public List getHoldouts() { + return holdoutConfig.getAllHoldouts(); + } + + @Override + public List getHoldoutForFlag(@Nonnull String id) { + return holdoutConfig.getHoldoutForFlag(id); + } + + @Override + public Holdout getHoldout(@Nonnull String id) { + return holdoutConfig.getHoldout(id); + } @Override public Set getAllSegments() { diff --git a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java index 18f8096ad..7981febac 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java @@ -159,4 +159,4 @@ public Holdout getHoldout(@Nonnull String id) { public List getAllHoldouts() { return Collections.unmodifiableList(allHoldouts); } -} \ No newline at end of file +} diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 366e2a1ff..96a0c6488 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -16,15 +16,16 @@ */ package com.optimizely.ab.config; -import com.optimizely.ab.config.audience.Audience; -import com.optimizely.ab.error.ErrorHandler; +import java.util.List; +import java.util.Map; +import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.List; -import java.util.Map; -import java.util.Set; + +import com.optimizely.ab.config.audience.Audience; +import com.optimizely.ab.error.ErrorHandler; /** * ProjectConfig is an interface capturing the experiment, variation and feature definitions. @@ -72,6 +73,10 @@ Experiment getExperimentForKey(@Nonnull String experimentKey, List getHoldouts(); + List getHoldoutForFlag(@Nonnull String id); + + Holdout getHoldout(@Nonnull String id); + Set getAllSegments(); List getExperimentsForEventKey(String eventKey); From 596d9d72d6c36badd45bd43d14ad76e083dd7d3c Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 23 Jul 2025 18:46:20 +0600 Subject: [PATCH 18/19] Downgrade nebula.optional-base --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 54426f6e7..845830761 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'com.github.kt3k.coveralls' version '2.12.2' id 'jacoco' id 'me.champeau.gradle.jmh' version '0.5.3' - id 'nebula.optional-base' version '3.2.0' + id 'nebula.optional-base' version '3.1.0' id 'com.github.hierynomus.license' version '0.16.1' id 'com.github.spotbugs' version "6.0.14" id 'maven-publish' @@ -116,6 +116,7 @@ configure(publishedProjects) { configurations.all { resolutionStrategy { force "junit:junit:${junitVersion}" + force 'com.netflix.nebula:nebula-gradle-interop:2.2.2' } } From 3d2d32a180d039d399bb74e8614850a750fb1a3b Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 30 Jul 2025 19:42:05 +0600 Subject: [PATCH 19/19] Replace ArrayList to HashSet for excludedHoldouts map --- .../main/java/com/optimizely/ab/config/HoldoutConfig.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java index 7981febac..69635b1ae 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/HoldoutConfig.java @@ -21,8 +21,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; @@ -37,7 +39,7 @@ public class HoldoutConfig { private Map holdoutIdMap; private Map> flagHoldoutsMap; private Map> includedHoldouts; - private Map> excludedHoldouts; + private Map> excludedHoldouts; /** * Initializes a new HoldoutConfig with an empty list of holdouts. @@ -93,7 +95,7 @@ private void updateHoldoutMapping() { global.add(holdout); for (String flagId : holdout.getExcludedFlags()) { - excludedHoldouts.computeIfAbsent(flagId, k -> new ArrayList<>()).add(holdout); + excludedHoldouts.computeIfAbsent(flagId, k -> new HashSet<>()).add(holdout); } } } @@ -119,7 +121,7 @@ public List getHoldoutForFlag(@Nonnull String id) { // Prioritize global holdouts first List activeHoldouts = new ArrayList<>(); - List excluded = excludedHoldouts.getOrDefault(id, Collections.emptyList()); + Set excluded = excludedHoldouts.getOrDefault(id, Collections.emptySet()); if (!excluded.isEmpty()) { for (Holdout holdout : global) {