Skip to content

Commit 68c959f

Browse files
[AI-FSSDK] [FSSDK-12337] Add Feature Rollout support
1 parent 2e39657 commit 68c959f

8 files changed

Lines changed: 537 additions & 11 deletions

File tree

core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ public DatafileProjectConfig(String accountId,
194194
List<Experiment> allExperiments = new ArrayList<Experiment>();
195195
allExperiments.addAll(experiments);
196196
allExperiments.addAll(aggregateGroupExperiments(groups));
197+
198+
// Inject "everyone else" variation into feature_rollout experiments
199+
allExperiments = injectFeatureRolloutVariations(allExperiments, this.featureFlags, this.rollouts);
200+
197201
this.experiments = Collections.unmodifiableList(allExperiments);
198202

199203
if (holdouts == null) {
@@ -357,6 +361,110 @@ public Experiment getExperimentForVariationId(String variationId) {
357361
return this.variationIdToExperimentMapping.get(variationId);
358362
}
359363

364+
/**
365+
* Injects the "everyone else" variation from the flag's rollout into any experiment
366+
* with type "feature_rollout". This enables Feature Rollout experiments to fall back
367+
* to the everyone else variation when users are outside the rollout percentage.
368+
*/
369+
private List<Experiment> injectFeatureRolloutVariations(
370+
List<Experiment> allExperiments,
371+
List<FeatureFlag> featureFlags,
372+
List<Rollout> rollouts
373+
) {
374+
if (featureFlags == null || featureFlags.isEmpty()) {
375+
return allExperiments;
376+
}
377+
378+
// Build rollout ID to Rollout mapping
379+
Map<String, Rollout> rolloutMap = new HashMap<>();
380+
if (rollouts != null) {
381+
for (Rollout rollout : rollouts) {
382+
rolloutMap.put(rollout.getId(), rollout);
383+
}
384+
}
385+
386+
// Build experiment ID to index mapping for quick lookup
387+
Map<String, Integer> experimentIndexMap = new HashMap<>();
388+
for (int i = 0; i < allExperiments.size(); i++) {
389+
experimentIndexMap.put(allExperiments.get(i).getId(), i);
390+
}
391+
392+
List<Experiment> result = new ArrayList<>(allExperiments);
393+
394+
for (FeatureFlag flag : featureFlags) {
395+
Variation everyoneElseVariation = getEveryoneElseVariation(flag, rolloutMap);
396+
if (everyoneElseVariation == null) {
397+
continue;
398+
}
399+
400+
for (String experimentId : flag.getExperimentIds()) {
401+
Integer index = experimentIndexMap.get(experimentId);
402+
if (index == null) {
403+
continue;
404+
}
405+
Experiment experiment = result.get(index);
406+
if (!"feature_rollout".equals(experiment.getType())) {
407+
continue;
408+
}
409+
410+
// Create new experiment with injected variation and traffic allocation
411+
List<Variation> newVariations = new ArrayList<>(experiment.getVariations());
412+
newVariations.add(everyoneElseVariation);
413+
414+
List<TrafficAllocation> newTrafficAllocation = new ArrayList<>(experiment.getTrafficAllocation());
415+
newTrafficAllocation.add(new TrafficAllocation(everyoneElseVariation.getId(), 10000));
416+
417+
Experiment updatedExperiment = new Experiment(
418+
experiment.getId(),
419+
experiment.getKey(),
420+
experiment.getStatus(),
421+
experiment.getLayerId(),
422+
experiment.getAudienceIds(),
423+
experiment.getAudienceConditions(),
424+
newVariations,
425+
experiment.getUserIdToVariationKeyMap(),
426+
newTrafficAllocation,
427+
experiment.getGroupId(),
428+
experiment.getCmab(),
429+
experiment.getType()
430+
);
431+
432+
result.set(index, updatedExperiment);
433+
}
434+
}
435+
436+
return result;
437+
}
438+
439+
/**
440+
* Gets the "everyone else" variation from the flag's rollout.
441+
* The everyone else rule is the last experiment in the rollout,
442+
* and the variation is the first variation of that rule.
443+
*
444+
* @return the everyone else variation, or null if it cannot be resolved
445+
*/
446+
@Nullable
447+
private Variation getEveryoneElseVariation(FeatureFlag flag, Map<String, Rollout> rolloutMap) {
448+
String rolloutId = flag.getRolloutId();
449+
if (rolloutId == null || rolloutId.isEmpty()) {
450+
return null;
451+
}
452+
Rollout rollout = rolloutMap.get(rolloutId);
453+
if (rollout == null) {
454+
return null;
455+
}
456+
List<Experiment> rolloutExperiments = rollout.getExperiments();
457+
if (rolloutExperiments == null || rolloutExperiments.isEmpty()) {
458+
return null;
459+
}
460+
Experiment everyoneElseRule = rolloutExperiments.get(rolloutExperiments.size() - 1);
461+
List<Variation> variations = everyoneElseRule.getVariations();
462+
if (variations == null || variations.isEmpty()) {
463+
return null;
464+
}
465+
return variations.get(0);
466+
}
467+
360468
private List<Experiment> aggregateGroupExperiments(List<Group> groups) {
361469
List<Experiment> groupExperiments = new ArrayList<Experiment>();
362470
for (Group group : groups) {

core-api/src/main/java/com/optimizely/ab/config/Experiment.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class Experiment implements ExperimentCore {
4141
private final String status;
4242
private final String layerId;
4343
private final String groupId;
44+
private final String type;
4445
private final Cmab cmab;
4546

4647
private final List<String> audienceIds;
@@ -72,7 +73,7 @@ public String toString() {
7273

7374
@VisibleForTesting
7475
public Experiment(String id, String key, String layerId) {
75-
this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null);
76+
this(id, key, null, layerId, Collections.emptyList(), null, Collections.emptyList(), Collections.emptyMap(), Collections.emptyList(), "", null, null);
7677
}
7778

7879
@VisibleForTesting
@@ -81,7 +82,7 @@ public Experiment(String id, String key, String status, String layerId,
8182
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
8283
List<TrafficAllocation> trafficAllocation, String groupId) {
8384
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
84-
userIdToVariationKeyMap, trafficAllocation, groupId, null); // Default cmab=null
85+
userIdToVariationKeyMap, trafficAllocation, groupId, null, null); // Default cmab=null, type=null
8586
}
8687

8788
@VisibleForTesting
@@ -90,7 +91,27 @@ public Experiment(String id, String key, String status, String layerId,
9091
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
9192
List<TrafficAllocation> trafficAllocation) {
9293
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
93-
userIdToVariationKeyMap, trafficAllocation, "", null); // Default groupId="" and cmab=null
94+
userIdToVariationKeyMap, trafficAllocation, "", null, null); // Default groupId="", cmab=null, type=null
95+
}
96+
97+
@VisibleForTesting
98+
public Experiment(String id, String key, String status, String layerId,
99+
List<String> audienceIds, Condition audienceConditions,
100+
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
101+
List<TrafficAllocation> trafficAllocation,
102+
Cmab cmab) {
103+
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
104+
userIdToVariationKeyMap, trafficAllocation, "", cmab, null); // Default groupId="" and type=null
105+
}
106+
107+
@VisibleForTesting
108+
public Experiment(String id, String key, String status, String layerId,
109+
List<String> audienceIds, Condition audienceConditions,
110+
List<Variation> variations, Map<String, String> userIdToVariationKeyMap,
111+
List<TrafficAllocation> trafficAllocation, String groupId,
112+
Cmab cmab) {
113+
this(id, key, status, layerId, audienceIds, audienceConditions, variations,
114+
userIdToVariationKeyMap, trafficAllocation, groupId, cmab, null); // Default type=null
94115
}
95116

96117
@JsonCreator
@@ -103,8 +124,9 @@ public Experiment(@JsonProperty("id") String id,
103124
@JsonProperty("variations") List<Variation> variations,
104125
@JsonProperty("forcedVariations") Map<String, String> userIdToVariationKeyMap,
105126
@JsonProperty("trafficAllocation") List<TrafficAllocation> trafficAllocation,
106-
@JsonProperty("cmab") Cmab cmab) {
107-
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab);
127+
@JsonProperty("cmab") Cmab cmab,
128+
@JsonProperty("type") String type) {
129+
this(id, key, status, layerId, audienceIds, audienceConditions, variations, userIdToVariationKeyMap, trafficAllocation, "", cmab, type);
108130
}
109131

110132
public Experiment(@Nonnull String id,
@@ -117,7 +139,8 @@ public Experiment(@Nonnull String id,
117139
@Nonnull Map<String, String> userIdToVariationKeyMap,
118140
@Nonnull List<TrafficAllocation> trafficAllocation,
119141
@Nonnull String groupId,
120-
@Nullable Cmab cmab) {
142+
@Nullable Cmab cmab,
143+
@Nullable String type) {
121144
this.id = id;
122145
this.key = key;
123146
this.status = status == null ? ExperimentStatus.NOT_STARTED.toString() : status;
@@ -131,6 +154,7 @@ public Experiment(@Nonnull String id,
131154
this.variationKeyToVariationMap = ProjectConfigUtils.generateNameMapping(variations);
132155
this.variationIdToVariationMap = ProjectConfigUtils.generateIdMapping(variations);
133156
this.cmab = cmab;
157+
this.type = type;
134158
}
135159

136160
public String getId() {
@@ -181,6 +205,11 @@ public String getGroupId() {
181205
return groupId;
182206
}
183207

208+
@Nullable
209+
public String getType() {
210+
return type;
211+
}
212+
184213
public Cmab getCmab() {
185214
return cmab;
186215
}
@@ -211,6 +240,7 @@ public String toString() {
211240
", variationKeyToVariationMap=" + variationKeyToVariationMap +
212241
", userIdToVariationKeyMap=" + userIdToVariationKeyMap +
213242
", trafficAllocation=" + trafficAllocation +
243+
", type='" + type + '\'' +
214244
", cmab=" + cmab +
215245
'}';
216246
}

core-api/src/main/java/com/optimizely/ab/config/Group.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ public Group(@JsonProperty("id") String id,
6363
experiment.getUserIdToVariationKeyMap(),
6464
experiment.getTrafficAllocation(),
6565
id,
66-
experiment.getCmab()
66+
experiment.getCmab(),
67+
experiment.getType()
6768
);
6869
}
6970
this.experiments.add(experiment);

core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,16 @@ static Experiment parseExperiment(JsonObject experimentJson, String groupId, Jso
168168
}
169169
}
170170

171+
String type = null;
172+
if (experimentJson.has("type")) {
173+
JsonElement typeElement = experimentJson.get("type");
174+
if (!typeElement.isJsonNull()) {
175+
type = typeElement.getAsString();
176+
}
177+
}
178+
171179
return new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
172-
trafficAllocations, groupId, cmab);
180+
trafficAllocations, groupId, cmab, type);
173181
}
174182

175183
static Experiment parseExperiment(JsonObject experimentJson, JsonDeserializationContext context) {

core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,10 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
179179
cmab = parseCmab(cmabObject);
180180
}
181181

182+
String type = experimentObject.optString("type", null);
183+
182184
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations, userIdToVariationKeyMap,
183-
trafficAllocations, groupId, cmab));
185+
trafficAllocations, groupId, cmab, type));
184186
}
185187

186188
return experiments;

core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,17 @@ private List<Experiment> parseExperiments(JSONArray experimentJson, String group
189189
}
190190
}
191191

192-
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations,
193-
userIdToVariationKeyMap, trafficAllocations, groupId, cmab));
192+
// Parse type field
193+
String type = null;
194+
if (experimentObject.containsKey("type")) {
195+
Object typeObj = experimentObject.get("type");
196+
if (typeObj != null) {
197+
type = (String) typeObj;
198+
}
199+
}
200+
201+
experiments.add(new Experiment(id, key, status, layerId, audienceIds, conditions, variations,
202+
userIdToVariationKeyMap, trafficAllocations, groupId, cmab, type));
194203
}
195204

196205
return experiments;

0 commit comments

Comments
 (0)