diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index c68ea4575..95f598eb2 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -399,6 +399,17 @@ DecisionResponse getVariationFromExperiment(@Nonnull ProjectCon for (String experimentId : featureFlag.getExperimentIds()) { Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId); + // Skip experiments with unsupported types. + // If the experiment type is null (not set in datafile), we still evaluate it. + // If the experiment type is set but not in the supported list, we skip it. + if (experiment != null && experiment.getType() != null && !Experiment.SUPPORTED_TYPES.contains(experiment.getType())) { + String skipMessage = reasons.addInfo( + "Skipping experiment \"%s\" with unsupported type \"%s\" for feature \"%s\".", + experiment.getKey(), experiment.getType(), featureFlag.getKey()); + logger.debug(skipMessage); + continue; + } + DecisionResponse decisionVariation = getVariationFromExperimentRule(projectConfig, featureFlag.getKey(), experiment, user, options, userProfileTracker, decisionPath); reasons.merge(decisionVariation.getReasons()); 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 7d687e9e9..cc5a9eb11 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 @@ -23,9 +23,12 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Represents the Optimizely Experiment configuration. @@ -36,8 +39,18 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class Experiment implements ExperimentCore { + /** + * The set of experiment types supported by this SDK. + * Experiments with a type not in this set will be skipped during flag decisions. + * If an experiment has no type (null), it is still evaluated. + */ + public static final Set SUPPORTED_TYPES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList("a/b", "mab", "cmab", "feature_rollouts")) + ); + private final String id; private final String key; + private final String type; private final String status; private final String layerId; private final String groupId; @@ -72,7 +85,7 @@ public String toString() { @VisibleForTesting public Experiment(String id, String key, String layerId) { - this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null); + this(id, key, null, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null); } @VisibleForTesting @@ -80,8 +93,8 @@ public Experiment(String id, String key, String status, String layerId, List audienceIds, Condition audienceConditions, List variations, Map userIdToVariationKeyMap, List trafficAllocation, String groupId) { - this(id, key, status, layerId, audienceIds, audienceConditions, variations, - userIdToVariationKeyMap, trafficAllocation, groupId, null); // Default cmab=null + this(id, key, null, status, layerId, audienceIds, audienceConditions, variations, + userIdToVariationKeyMap, trafficAllocation, groupId, null); // Default type=null, cmab=null } @VisibleForTesting @@ -89,13 +102,23 @@ public Experiment(String id, String key, String status, String layerId, List audienceIds, Condition audienceConditions, List variations, Map userIdToVariationKeyMap, List trafficAllocation) { - this(id, key, status, layerId, audienceIds, audienceConditions, variations, - userIdToVariationKeyMap, trafficAllocation, "", null); // Default groupId="" and cmab=null + this(id, key, null, status, layerId, audienceIds, audienceConditions, variations, + userIdToVariationKeyMap, trafficAllocation, "", null); // Default type=null, groupId="" and cmab=null + } + + @VisibleForTesting + public Experiment(String id, String key, String status, String layerId, + List audienceIds, Condition audienceConditions, + List variations, Map userIdToVariationKeyMap, + List trafficAllocation, Cmab cmab) { + this(id, key, null, status, layerId, audienceIds, audienceConditions, variations, + userIdToVariationKeyMap, trafficAllocation, "", cmab); // Default type=null, groupId="" } @JsonCreator public Experiment(@JsonProperty("id") String id, @JsonProperty("key") String key, + @JsonProperty("type") String type, @JsonProperty("status") String status, @JsonProperty("layerId") String layerId, @JsonProperty("audienceIds") List audienceIds, @@ -104,11 +127,12 @@ public Experiment(@JsonProperty("id") String id, @JsonProperty("forcedVariations") Map userIdToVariationKeyMap, @JsonProperty("trafficAllocation") List trafficAllocation, @JsonProperty("cmab") Cmab cmab) { - this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab); + this(id, key, type, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab); } public Experiment(@Nonnull String id, @Nonnull String key, + @Nullable String type, @Nullable String status, @Nullable String layerId, @Nonnull List audienceIds, @@ -120,6 +144,7 @@ public Experiment(@Nonnull String id, @Nullable Cmab cmab) { this.id = id; this.key = key; + this.type = type; this.status = status == null ? ExperimentStatus.NOT_STARTED.toString() : status; this.layerId = layerId; this.audienceIds = Collections.unmodifiableList(audienceIds); @@ -141,6 +166,11 @@ public String getKey() { return key; } + @Nullable + public String getType() { + return type; + } + public String getStatus() { return status; } @@ -203,6 +233,7 @@ public String toString() { return "Experiment{" + "id='" + id + '\'' + ", key='" + key + '\'' + + ", type='" + type + '\'' + ", groupId='" + groupId + '\'' + ", status='" + status + '\'' + ", audienceIds=" + audienceIds + diff --git a/core-api/src/main/java/com/optimizely/ab/config/Group.java b/core-api/src/main/java/com/optimizely/ab/config/Group.java index d0d9ff364..c3a750853 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/Group.java +++ b/core-api/src/main/java/com/optimizely/ab/config/Group.java @@ -55,6 +55,7 @@ public Group(@JsonProperty("id") String id, experiment = new Experiment( experiment.getId(), experiment.getKey(), + experiment.getType(), experiment.getStatus(), experiment.getLayerId(), experiment.getAudienceIds(), 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 624f9f159..c4da95fae 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 @@ -137,6 +137,8 @@ static Cmab parseCmab(JsonObject cmabJson, JsonDeserializationContext context) { static Experiment parseExperiment(JsonObject experimentJson, String groupId, JsonDeserializationContext context) { String id = experimentJson.get("id").getAsString(); String key = experimentJson.get("key").getAsString(); + String type = experimentJson.has("type") && !experimentJson.get("type").isJsonNull() + ? experimentJson.get("type").getAsString() : null; JsonElement experimentStatusJson = experimentJson.get("status"); String status = experimentStatusJson.isJsonNull() ? ExperimentStatus.NOT_STARTED.toString() : experimentStatusJson.getAsString(); @@ -168,7 +170,7 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso } } - return new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap, + return new Experiment(id, key, type, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap, trafficAllocations, groupId, cmab); } 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 10ca9685f..806554871 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 @@ -148,6 +148,8 @@ private List parseExperiments(JSONArray experimentJson, String group JSONObject experimentObject = (JSONObject) obj; String id = experimentObject.getString("id"); String key = experimentObject.getString("key"); + String type = experimentObject.has("type") && !experimentObject.isNull("type") + ? experimentObject.getString("type") : null; String status = experimentObject.isNull("status") ? ExperimentStatus.NOT_STARTED.toString() : experimentObject.getString("status"); String layerId = experimentObject.has("layerId") ? experimentObject.getString("layerId") : null; @@ -179,7 +181,7 @@ private List parseExperiments(JSONArray experimentJson, String group cmab = parseCmab(cmabObject); } - experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap, + experiments.add(new Experiment(id, key, type, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap, trafficAllocations, groupId, cmab)); } 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 56215acc3..f4c868282 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 @@ -150,6 +150,8 @@ private List parseExperiments(JSONArray experimentJson, String group JSONObject experimentObject = (JSONObject) obj; String id = (String) experimentObject.get("id"); String key = (String) experimentObject.get("key"); + Object typeObj = experimentObject.get("type"); + String type = typeObj != null ? (String) typeObj : null; Object statusJson = experimentObject.get("status"); String status = statusJson == null ? ExperimentStatus.NOT_STARTED.toString() : (String) experimentObject.get("status"); @@ -189,7 +191,7 @@ private List parseExperiments(JSONArray experimentJson, String group } } - experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, + experiments.add(new Experiment(id, key, type, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap, trafficAllocations, groupId, cmab)); }