Skip to content

Commit a4c3f1c

Browse files
update: implement decision-making methods to skip CMAB logic in Optimizely and DecisionService
1 parent b2f270f commit a4c3f1c

File tree

3 files changed

+194
-17
lines changed

3 files changed

+194
-17
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,151 @@ Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
14981498
return decideForKeys(user, allFlagKeys, options);
14991499
}
15001500

1501+
/**
1502+
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context,
1503+
* skipping CMAB logic and using only traditional A/B testing.
1504+
*
1505+
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
1506+
* @param key A flag key for which a decision will be made.
1507+
* @param options A list of options for decision-making.
1508+
* @return A decision result using traditional A/B testing logic only.
1509+
*/
1510+
OptimizelyDecision decideWithoutCmab(@Nonnull OptimizelyUserContext user,
1511+
@Nonnull String key,
1512+
@Nonnull List<OptimizelyDecideOption> options) {
1513+
ProjectConfig projectConfig = getProjectConfig();
1514+
if (projectConfig == null) {
1515+
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
1516+
}
1517+
1518+
List<OptimizelyDecideOption> allOptions = getAllOptions(options);
1519+
allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);
1520+
1521+
return decideForKeysWithoutCmab(user, Arrays.asList(key), allOptions, true).get(key);
1522+
}
1523+
1524+
/**
1525+
* Returns decision results for multiple flag keys, skipping CMAB logic and using only traditional A/B testing.
1526+
*
1527+
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
1528+
* @param keys A list of flag keys for which decisions will be made.
1529+
* @param options A list of options for decision-making.
1530+
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
1531+
*/
1532+
Map<String, OptimizelyDecision> decideForKeysWithoutCmab(@Nonnull OptimizelyUserContext user,
1533+
@Nonnull List<String> keys,
1534+
@Nonnull List<OptimizelyDecideOption> options) {
1535+
return decideForKeysWithoutCmab(user, keys, options, false);
1536+
}
1537+
1538+
/**
1539+
* Returns decision results for all active flag keys, skipping CMAB logic and using only traditional A/B testing.
1540+
*
1541+
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
1542+
* @param options A list of options for decision-making.
1543+
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
1544+
*/
1545+
Map<String, OptimizelyDecision> decideAllWithoutCmab(@Nonnull OptimizelyUserContext user,
1546+
@Nonnull List<OptimizelyDecideOption> options) {
1547+
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
1548+
1549+
ProjectConfig projectConfig = getProjectConfig();
1550+
if (projectConfig == null) {
1551+
logger.error("Optimizely instance is not valid, failing decideAllWithoutCmab call.");
1552+
return decisionMap;
1553+
}
1554+
1555+
List<FeatureFlag> allFlags = projectConfig.getFeatureFlags();
1556+
List<String> allFlagKeys = new ArrayList<>();
1557+
for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey());
1558+
1559+
return decideForKeysWithoutCmab(user, allFlagKeys, options);
1560+
}
1561+
1562+
private Map<String, OptimizelyDecision> decideForKeysWithoutCmab(@Nonnull OptimizelyUserContext user,
1563+
@Nonnull List<String> keys,
1564+
@Nonnull List<OptimizelyDecideOption> options,
1565+
boolean ignoreDefaultOptions) {
1566+
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
1567+
1568+
ProjectConfig projectConfig = getProjectConfig();
1569+
if (projectConfig == null) {
1570+
logger.error("Optimizely instance is not valid, failing decideForKeysWithoutCmab call.");
1571+
return decisionMap;
1572+
}
1573+
1574+
if (keys.isEmpty()) return decisionMap;
1575+
1576+
List<OptimizelyDecideOption> allOptions = ignoreDefaultOptions ? options : getAllOptions(options);
1577+
1578+
Map<String, FeatureDecision> flagDecisions = new HashMap<>();
1579+
Map<String, DecisionReasons> decisionReasonsMap = new HashMap<>();
1580+
1581+
List<FeatureFlag> flagsWithoutForcedDecision = new ArrayList<>();
1582+
1583+
List<String> validKeys = new ArrayList<>();
1584+
1585+
for (String key : keys) {
1586+
FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
1587+
if (flag == null) {
1588+
decisionMap.put(key, OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key)));
1589+
continue;
1590+
}
1591+
1592+
validKeys.add(key);
1593+
1594+
DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
1595+
decisionReasonsMap.put(key, decisionReasons);
1596+
1597+
OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(key, null);
1598+
DecisionResponse<Variation> forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
1599+
decisionReasons.merge(forcedDecisionVariation.getReasons());
1600+
if (forcedDecisionVariation.getResult() != null) {
1601+
flagDecisions.put(key,
1602+
new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST));
1603+
} else {
1604+
flagsWithoutForcedDecision.add(flag);
1605+
}
1606+
}
1607+
1608+
// Use DecisionService method that skips CMAB logic
1609+
List<DecisionResponse<FeatureDecision>> decisionList =
1610+
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions, false);
1611+
1612+
for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) {
1613+
DecisionResponse<FeatureDecision> decision = decisionList.get(i);
1614+
boolean error = decision.isError();
1615+
String flagKey = flagsWithoutForcedDecision.get(i).getKey();
1616+
1617+
if (error) {
1618+
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.DECISION_ERROR.reason(flagKey));
1619+
decisionMap.put(flagKey, optimizelyDecision);
1620+
if (validKeys.contains(flagKey)) {
1621+
validKeys.remove(flagKey);
1622+
}
1623+
}
1624+
1625+
flagDecisions.put(flagKey, decision.getResult());
1626+
decisionReasonsMap.get(flagKey).merge(decision.getReasons());
1627+
}
1628+
1629+
for (String key : validKeys) {
1630+
FeatureDecision flagDecision = flagDecisions.get(key);
1631+
DecisionReasons decisionReasons = decisionReasonsMap.get((key));
1632+
1633+
OptimizelyDecision optimizelyDecision = createOptimizelyDecision(
1634+
user, key, flagDecision, decisionReasons, allOptions, projectConfig
1635+
);
1636+
1637+
if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || optimizelyDecision.getEnabled()) {
1638+
decisionMap.put(key, optimizelyDecision);
1639+
}
1640+
}
1641+
1642+
return decisionMap;
1643+
}
1644+
1645+
15011646
private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
15021647
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);
15031648
if (options != null) {

core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
113113
@Nonnull ProjectConfig projectConfig,
114114
@Nonnull List<OptimizelyDecideOption> options,
115115
@Nullable UserProfileTracker userProfileTracker,
116-
@Nullable DecisionReasons reasons) {
116+
@Nullable DecisionReasons reasons,
117+
@Nonnull boolean useCmab) {
117118
if (reasons == null) {
118119
reasons = DefaultDecisionReasons.newInstance();
119120
}
@@ -155,7 +156,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
155156
if (decisionMeetAudience.getResult()) {
156157
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
157158
String cmabUUID = null;
158-
if (isCmabExperiment(experiment)) {
159+
if (useCmab && isCmabExperiment(experiment)) {
159160
DecisionResponse<CmabDecision> cmabDecision = getDecisionForCmabExperiment(projectConfig, experiment, user, bucketingId, options);
160161
reasons.merge(cmabDecision.getReasons());
161162

@@ -205,7 +206,8 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
205206
public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
206207
@Nonnull OptimizelyUserContext user,
207208
@Nonnull ProjectConfig projectConfig,
208-
@Nonnull List<OptimizelyDecideOption> options) {
209+
@Nonnull List<OptimizelyDecideOption> options,
210+
@Nonnull boolean useCmab) {
209211
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
210212

211213
// fetch the user profile map from the user profile service
@@ -217,7 +219,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
217219
userProfileTracker.loadUserProfile(reasons, errorHandler);
218220
}
219221

220-
DecisionResponse<Variation> response = getVariation(experiment, user, projectConfig, options, userProfileTracker, reasons);
222+
DecisionResponse<Variation> response = getVariation(experiment, user, projectConfig, options, userProfileTracker, reasons, useCmab);
221223

222224
if(userProfileService != null && !ignoreUPS) {
223225
userProfileTracker.saveUserProfile(errorHandler);
@@ -229,7 +231,7 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
229231
public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
230232
@Nonnull OptimizelyUserContext user,
231233
@Nonnull ProjectConfig projectConfig) {
232-
return getVariation(experiment, user, projectConfig, Collections.emptyList());
234+
return getVariation(experiment, user, projectConfig, Collections.emptyList(), true);
233235
}
234236

235237
/**
@@ -256,13 +258,33 @@ public DecisionResponse<FeatureDecision> getVariationForFeature(@Nonnull Feature
256258
* @param user The current OptimizelyuserContext
257259
* @param projectConfig The current projectConfig
258260
* @param options An array of decision options
261+
* @param useCmab Boolean field that determines whether to use cmab service
259262
* @return A {@link DecisionResponse} including a {@link FeatureDecision} and the decision reasons
260263
*/
261264
@Nonnull
262265
public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Nonnull List<FeatureFlag> featureFlags,
263266
@Nonnull OptimizelyUserContext user,
264267
@Nonnull ProjectConfig projectConfig,
265268
@Nonnull List<OptimizelyDecideOption> options) {
269+
return getVariationsForFeatureList(featureFlags, user, projectConfig, options, true);
270+
}
271+
272+
/**
273+
* Get the variations the user is bucketed into for the list of feature flags
274+
*
275+
* @param featureFlags The feature flag list the user wants to access.
276+
* @param user The current OptimizelyuserContext
277+
* @param projectConfig The current projectConfig
278+
* @param options An array of decision options
279+
* @param useCmab Boolean field that determines whether to use cmab service
280+
* @return A {@link DecisionResponse} including a {@link FeatureDecision} and the decision reasons
281+
*/
282+
@Nonnull
283+
public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Nonnull List<FeatureFlag> featureFlags,
284+
@Nonnull OptimizelyUserContext user,
285+
@Nonnull ProjectConfig projectConfig,
286+
@Nonnull List<OptimizelyDecideOption> options,
287+
@Nonnull boolean useCmab) {
266288
DecisionReasons upsReasons = DefaultDecisionReasons.newInstance();
267289

268290
boolean ignoreUPS = options.contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE);
@@ -291,7 +313,7 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non
291313
}
292314
}
293315

294-
DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker);
316+
DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker, useCmab);
295317
reasons.merge(decisionVariationResponse.getReasons());
296318

297319
FeatureDecision decision = decisionVariationResponse.getResult();
@@ -346,14 +368,15 @@ DecisionResponse<FeatureDecision> getVariationFromExperiment(@Nonnull ProjectCon
346368
@Nonnull FeatureFlag featureFlag,
347369
@Nonnull OptimizelyUserContext user,
348370
@Nonnull List<OptimizelyDecideOption> options,
349-
@Nullable UserProfileTracker userProfileTracker) {
371+
@Nullable UserProfileTracker userProfileTracker,
372+
@Nonnull boolean useCmab) {
350373
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
351374
if (!featureFlag.getExperimentIds().isEmpty()) {
352375
for (String experimentId : featureFlag.getExperimentIds()) {
353376
Experiment experiment = projectConfig.getExperimentIdMapping().get(experimentId);
354377

355378
DecisionResponse<Variation> decisionVariation =
356-
getVariationFromExperimentRule(projectConfig, featureFlag.getKey(), experiment, user, options, userProfileTracker);
379+
getVariationFromExperimentRule(projectConfig, featureFlag.getKey(), experiment, user, options, userProfileTracker, useCmab);
357380
reasons.merge(decisionVariation.getReasons());
358381
Variation variation = decisionVariation.getResult();
359382
String cmabUUID = decisionVariation.getCmabUUID();
@@ -776,7 +799,8 @@ private DecisionResponse<Variation> getVariationFromExperimentRule(@Nonnull Proj
776799
@Nonnull Experiment rule,
777800
@Nonnull OptimizelyUserContext user,
778801
@Nonnull List<OptimizelyDecideOption> options,
779-
@Nullable UserProfileTracker userProfileTracker) {
802+
@Nullable UserProfileTracker userProfileTracker,
803+
@Nonnull boolean useCmab) {
780804
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
781805

782806
String ruleKey = rule != null ? rule.getKey() : null;
@@ -791,7 +815,7 @@ private DecisionResponse<Variation> getVariationFromExperimentRule(@Nonnull Proj
791815
return new DecisionResponse(variation, reasons);
792816
}
793817
//regular decision
794-
DecisionResponse<Variation> decisionResponse = getVariation(rule, user, projectConfig, options, userProfileTracker, null);
818+
DecisionResponse<Variation> decisionResponse = getVariation(rule, user, projectConfig, options, userProfileTracker, null, useCmab);
795819
reasons.merge(decisionResponse.getReasons());
796820

797821
variation = decisionResponse.getResult();

core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.junit.Rule;
3535
import org.junit.Test;
3636
import static org.mockito.Matchers.any;
37+
import static org.mockito.Matchers.anyBoolean;
3738
import static org.mockito.Matchers.anyMapOf;
3839
import static org.mockito.Matchers.anyObject;
3940
import static org.mockito.Matchers.anyString;
@@ -359,7 +360,8 @@ public void getVariationForFeatureReturnsNullWhenItGetsNoVariationsForExperiment
359360
any(ProjectConfig.class),
360361
anyObject(),
361362
anyObject(),
362-
any(DecisionReasons.class)
363+
any(DecisionReasons.class),
364+
anyBoolean()
363365
);
364366
// do not bucket to any rollouts
365367
doReturn(DecisionResponse.responseNoReasons(new FeatureDecision(null, null, null))).when(decisionService).getVariationForFeatureInRollout(
@@ -398,14 +400,16 @@ public void getVariationForFeatureReturnsVariationReturnedFromGetVariation() {
398400
eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_1),
399401
any(OptimizelyUserContext.class),
400402
any(ProjectConfig.class),
401-
anyObject()
403+
anyObject(),
404+
anyBoolean()
402405
);
403406

404407
doReturn(DecisionResponse.responseNoReasons(ValidProjectConfigV4.VARIATION_MUTEX_GROUP_EXP_2_VAR_1)).when(decisionService).getVariation(
405408
eq(ValidProjectConfigV4.EXPERIMENT_MUTEX_GROUP_EXPERIMENT_2),
406409
any(OptimizelyUserContext.class),
407410
any(ProjectConfig.class),
408-
anyObject()
411+
anyObject(),
412+
anyBoolean()
409413
);
410414

411415
FeatureDecision featureDecision = decisionService.getVariationForFeature(
@@ -445,7 +449,8 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout()
445449
any(ProjectConfig.class),
446450
anyObject(),
447451
anyObject(),
448-
any(DecisionReasons.class)
452+
any(DecisionReasons.class),
453+
anyBoolean()
449454
);
450455

451456
// return variation for rollout
@@ -479,7 +484,8 @@ public void getVariationForFeatureReturnsVariationFromExperimentBeforeRollout()
479484
any(ProjectConfig.class),
480485
anyObject(),
481486
anyObject(),
482-
any(DecisionReasons.class)
487+
any(DecisionReasons.class),
488+
anyBoolean()
483489
);
484490
}
485491

@@ -506,7 +512,8 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails
506512
any(ProjectConfig.class),
507513
anyObject(),
508514
anyObject(),
509-
any(DecisionReasons.class)
515+
any(DecisionReasons.class),
516+
anyBoolean()
510517
);
511518

512519
// return variation for rollout
@@ -540,7 +547,8 @@ public void getVariationForFeatureReturnsVariationFromRolloutWhenExperimentFails
540547
any(ProjectConfig.class),
541548
anyObject(),
542549
anyObject(),
543-
any(DecisionReasons.class)
550+
any(DecisionReasons.class),
551+
anyBoolean()
544552
);
545553

546554
logbackVerifier.expectMessage(

0 commit comments

Comments
 (0)