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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ public DatafileProjectConfig(String accountId,
List<Experiment> allExperiments = new ArrayList<Experiment>();
allExperiments.addAll(experiments);
allExperiments.addAll(aggregateGroupExperiments(groups));

// Inject "everyone else" variation into feature_rollout experiments
allExperiments = injectFeatureRolloutVariations(allExperiments, this.featureFlags, this.rollouts);

this.experiments = Collections.unmodifiableList(allExperiments);

if (holdouts == null) {
Expand Down Expand Up @@ -357,6 +361,110 @@ public Experiment getExperimentForVariationId(String variationId) {
return this.variationIdToExperimentMapping.get(variationId);
}

/**
* Injects the "everyone else" variation from the flag's rollout into any experiment
* with type "feature_rollout". This enables Feature Rollout experiments to fall back
* to the everyone else variation when users are outside the rollout percentage.
*/
private List<Experiment> injectFeatureRolloutVariations(
List<Experiment> allExperiments,
List<FeatureFlag> featureFlags,
List<Rollout> rollouts
) {
if (featureFlags == null || featureFlags.isEmpty()) {
return allExperiments;
}

// Build rollout ID to Rollout mapping
Map<String, Rollout> rolloutMap = new HashMap<>();
if (rollouts != null) {
for (Rollout rollout : rollouts) {
rolloutMap.put(rollout.getId(), rollout);
}
}

// Build experiment ID to index mapping for quick lookup
Map<String, Integer> experimentIndexMap = new HashMap<>();
for (int i = 0; i < allExperiments.size(); i++) {
experimentIndexMap.put(allExperiments.get(i).getId(), i);
}

List<Experiment> result = new ArrayList<>(allExperiments);

for (FeatureFlag flag : featureFlags) {
Variation everyoneElseVariation = getEveryoneElseVariation(flag, rolloutMap);
if (everyoneElseVariation == null) {
continue;
}

for (String experimentId : flag.getExperimentIds()) {
Integer index = experimentIndexMap.get(experimentId);
if (index == null) {
continue;
}
Experiment experiment = result.get(index);
if (!"feature_rollout".equals(experiment.getType())) {
continue;
}

// Create new experiment with injected variation and traffic allocation
List<Variation> newVariations = new ArrayList<>(experiment.getVariations());
newVariations.add(everyoneElseVariation);

List<TrafficAllocation> newTrafficAllocation = new ArrayList<>(experiment.getTrafficAllocation());
newTrafficAllocation.add(new TrafficAllocation(everyoneElseVariation.getId(), 10000));

Experiment updatedExperiment = new Experiment(
experiment.getId(),
experiment.getKey(),
experiment.getStatus(),
experiment.getLayerId(),
experiment.getAudienceIds(),
experiment.getAudienceConditions(),
newVariations,
experiment.getUserIdToVariationKeyMap(),
newTrafficAllocation,
experiment.getGroupId(),
experiment.getCmab(),
experiment.getType()
);

result.set(index, updatedExperiment);
}
}

return result;
}

/**
* Gets the "everyone else" variation from the flag's rollout.
* The everyone else rule is the last experiment in the rollout,
* and the variation is the first variation of that rule.
*
* @return the everyone else variation, or null if it cannot be resolved
*/
@Nullable
private Variation getEveryoneElseVariation(FeatureFlag flag, Map<String, Rollout> rolloutMap) {
String rolloutId = flag.getRolloutId();
if (rolloutId == null || rolloutId.isEmpty()) {
return null;
}
Rollout rollout = rolloutMap.get(rolloutId);
if (rollout == null) {
return null;
}
List<Experiment> rolloutExperiments = rollout.getExperiments();
if (rolloutExperiments == null || rolloutExperiments.isEmpty()) {
return null;
}
Experiment everyoneElseRule = rolloutExperiments.get(rolloutExperiments.size() - 1);
List<Variation> variations = everyoneElseRule.getVariations();
if (variations == null || variations.isEmpty()) {
return null;
}
return variations.get(0);
}

private List<Experiment> aggregateGroupExperiments(List<Group> groups) {
List<Experiment> groupExperiments = new ArrayList<Experiment>();
for (Group group : groups) {
Expand Down
42 changes: 36 additions & 6 deletions core-api/src/main/java/com/optimizely/ab/config/Experiment.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class Experiment implements ExperimentCore {
private final String status;
private final String layerId;
private final String groupId;
private final String type;
private final Cmab cmab;

private final List<String> audienceIds;
Expand Down Expand Up @@ -72,7 +73,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, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null, null);
}

@VisibleForTesting
Expand All @@ -81,7 +82,7 @@ public Experiment(String id, String key, String status, String layerId,
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
List<TrafficAllocation> trafficAllocation, String groupId) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
userIdToVariationKeyMap, trafficAllocation, groupId, null); // Default cmab=null
userIdToVariationKeyMap, trafficAllocation, groupId, null, null); // Default cmab=null, type=null
}

@VisibleForTesting
Expand All @@ -90,7 +91,27 @@ public Experiment(String id, String key, String status, String layerId,
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
List<TrafficAllocation> trafficAllocation) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
userIdToVariationKeyMap, trafficAllocation, "", null); // Default groupId="" and cmab=null
userIdToVariationKeyMap, trafficAllocation, "", null, null); // Default groupId="", cmab=null, type=null
}

@VisibleForTesting
public Experiment(String id, String key, String status, String layerId,
List<String> audienceIds, Condition audienceConditions,
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
List<TrafficAllocation> trafficAllocation,
Cmab cmab) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
userIdToVariationKeyMap, trafficAllocation, "", cmab, null); // Default groupId="" and type=null
}

@VisibleForTesting
public Experiment(String id, String key, String status, String layerId,
List<String> audienceIds, Condition audienceConditions,
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
List<TrafficAllocation> trafficAllocation, String groupId,
Cmab cmab) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
userIdToVariationKeyMap, trafficAllocation, groupId, cmab, null); // Default type=null
}

@JsonCreator
Expand All @@ -103,8 +124,9 @@ public Experiment(@JsonProperty("id") String id,
@JsonProperty("variations") List<Variation> variations,
@JsonProperty("forcedVariations") Map<String, String> userIdToVariationKeyMap,
@JsonProperty("trafficAllocation") List<TrafficAllocation> trafficAllocation,
@JsonProperty("cmab") Cmab cmab) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab);
@JsonProperty("cmab") Cmab cmab,
@JsonProperty("type") String type) {
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab, type);
}

public Experiment(@Nonnull String id,
Expand All @@ -117,7 +139,8 @@ public Experiment(@Nonnull String id,
@Nonnull Map<String, String> userIdToVariationKeyMap,
@Nonnull List<TrafficAllocation> trafficAllocation,
@Nonnull String groupId,
@Nullable Cmab cmab) {
@Nullable Cmab cmab,
@Nullable String type) {
this.id = id;
this.key = key;
this.status = status == null ? ExperimentStatus.NOT_STARTED.toString() : status;
Expand All @@ -131,6 +154,7 @@ public Experiment(@Nonnull String id,
this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations);
this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations);
this.cmab = cmab;
this.type = type;
}

public String getId() {
Expand Down Expand Up @@ -181,6 +205,11 @@ public String getGroupId() {
return groupId;
}

@Nullable
public String getType() {
return type;
}

public Cmab getCmab() {
return cmab;
}
Expand Down Expand Up @@ -211,6 +240,7 @@ public String toString() {
", variationKeyToVariationMap=" + variationKeyToVariationMap +
", userIdToVariationKeyMap=" + userIdToVariationKeyMap +
", trafficAllocation=" + trafficAllocation +
", type='" + type + '\'' +
", cmab=" + cmab +
'}';
}
Expand Down
3 changes: 2 additions & 1 deletion core-api/src/main/java/com/optimizely/ab/config/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public Group(@JsonProperty("id") String id,
experiment.getUserIdToVariationKeyMap(),
experiment.getTrafficAllocation(),
id,
experiment.getCmab()
experiment.getCmab(),
experiment.getType()
);
}
this.experiments.add(experiment);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,16 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso
}
}

String type = null;
if (experimentJson.has("type")) {
JsonElement typeElement = experimentJson.get("type");
if (!typeElement.isJsonNull()) {
type = typeElement.getAsString();
}
}

return new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
trafficAllocations, groupId, cmab);
trafficAllocations, groupId, cmab, type);
}

static Experiment parseExperiment(JsonObject experimentJson, JsonDeserializationContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
cmab = parseCmab(cmabObject);
}

String type = experimentObject.optString("type", null);

experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
trafficAllocations, groupId, cmab));
trafficAllocations, groupId, cmab, type));
}

return experiments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,17 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
}
}

experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations,
userIdToVariationKeyMap, trafficAllocations, groupId, cmab));
// Parse type field
String type = null;
if (experimentObject.containsKey("type")) {
Object typeObj = experimentObject.get("type");
if (typeObj != null) {
type = (String) typeObj;
}
}

experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations,
userIdToVariationKeyMap, trafficAllocations, groupId, cmab, type));
}

return experiments;
Expand Down
Loading
Loading