Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
26e6393
update: add CmabService to Optimizely class and builder
FarhanAnjum-opti Sep 19, 2025
ad63201
update: integrate CMAB service into OptimizelyFactory
FarhanAnjum-opti Sep 19, 2025
fbed362
update: change CmabService field to non-nullable in Optimizely class
FarhanAnjum-opti Sep 19, 2025
53d754a
update: add CmabService to DecisionService and its tests
FarhanAnjum-opti Sep 19, 2025
9905026
update: implement CMAB traffic allocation in Bucketer and DecisionSer…
FarhanAnjum-opti Sep 23, 2025
78f45bf
update: enhance DecisionService, FeatureDecision, and DecisionRespons…
FarhanAnjum-opti Sep 24, 2025
9757d49
update: enhance DecisionService and DecisionMessage to handle errors …
FarhanAnjum-opti Sep 24, 2025
ecf9199
update: add validConfigJsonCMAB method to DatafileProjectConfigTestUt…
FarhanAnjum-opti Sep 24, 2025
5e0808f
update: add tests to verify precedence of whitelisted and forced vari…
FarhanAnjum-opti Sep 24, 2025
36d2b4c
update: add test to verify error handling in getVariation for CMAB se…
FarhanAnjum-opti Sep 24, 2025
5796cb7
update: modify DecisionResponse to include additional error handling …
FarhanAnjum-opti Sep 24, 2025
d8b0134
update: fix error handling assertion in DecisionServiceTest to correc…
FarhanAnjum-opti Sep 24, 2025
b2f270f
update: add tests for CMAB experiment variations in DecisionService
FarhanAnjum-opti Sep 24, 2025
a4c3f1c
update: implement decision-making methods to skip CMAB logic in Optim…
FarhanAnjum-opti Sep 24, 2025
e4fe788
update: add methods to OptimizelyUserContext for decision-making with…
FarhanAnjum-opti Sep 24, 2025
e75693d
update: add asynchronous decision-making methods in OptimizelyUserCon…
FarhanAnjum-opti Sep 24, 2025
af210d8
update: add decision-making methods without CMAB logic in OptimizelyU…
FarhanAnjum-opti Sep 24, 2025
42053e4
update: remove unused parameter 'useCmab' from DecisionService method…
FarhanAnjum-opti Sep 24, 2025
9a12d72
update: rename methods to use 'Sync' suffix for clarity in decision-m…
FarhanAnjum-opti Sep 25, 2025
416bcbd
update: return cmab error decision whenever found
FarhanAnjum-opti Sep 30, 2025
64f378f
update: enhance error handling by specifying CMAB error messages in d…
FarhanAnjum-opti Oct 1, 2025
8539166
update: improve error handling by checking for null values in experim…
FarhanAnjum-opti Oct 1, 2025
3cee65c
update: fix CMAB error handling by providing a valid Experiment in Fe…
FarhanAnjum-opti Oct 1, 2025
47c65b5
update: add Javadoc comments for async decision methods and config cr…
FarhanAnjum-opti Oct 1, 2025
fe75a85
update: refactor build to use cmabClient instead of default service
FarhanAnjum-opti Oct 3, 2025
b0d5090
update: refactor DefaultCmabClient to utilize CmabClientHelper
FarhanAnjum-opti Oct 3, 2025
6db2e88
update: refactor AsyncDecisionsFetcher to AsyncDecisionFetcher and en…
FarhanAnjum-opti Oct 3, 2025
6fc6446
update: add missing copyright notice and license information to CmabC…
FarhanAnjum-opti Oct 3, 2025
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
175 changes: 171 additions & 4 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.optimizely.ab.bucketing.DecisionService;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.bucketing.UserProfileService;
import com.optimizely.ab.cmab.service.CmabService;
import com.optimizely.ab.config.AtomicProjectConfigManager;
import com.optimizely.ab.config.DatafileProjectConfig;
import com.optimizely.ab.config.EventType;
Expand Down Expand Up @@ -141,8 +142,11 @@ public class Optimizely implements AutoCloseable {
@Nullable
private final ODPManager odpManager;

private final CmabService cmabService;

private final ReentrantLock lock = new ReentrantLock();


private Optimizely(@Nonnull EventHandler eventHandler,
@Nonnull EventProcessor eventProcessor,
@Nonnull ErrorHandler errorHandler,
Expand All @@ -152,8 +156,9 @@ private Optimizely(@Nonnull EventHandler eventHandler,
@Nullable OptimizelyConfigManager optimizelyConfigManager,
@Nonnull NotificationCenter notificationCenter,
@Nonnull List<OptimizelyDecideOption> defaultDecideOptions,
@Nullable ODPManager odpManager
) {
@Nullable ODPManager odpManager,
@Nonnull CmabService cmabService
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like breaking change, can we make nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javascript and all other server side implementations have cmabService as a mandatory field (non null). It shouldn't be breaking.

) {
this.eventHandler = eventHandler;
this.eventProcessor = eventProcessor;
this.errorHandler = errorHandler;
Expand All @@ -164,6 +169,7 @@ private Optimizely(@Nonnull EventHandler eventHandler,
this.notificationCenter = notificationCenter;
this.defaultDecideOptions = defaultDecideOptions;
this.odpManager = odpManager;
this.cmabService = cmabService;

if (odpManager != null) {
odpManager.getEventManager().start();
Expand Down Expand Up @@ -1444,7 +1450,17 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon

for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) {
DecisionResponse<FeatureDecision> decision = decisionList.get(i);
boolean error = decision.isError();
String flagKey = flagsWithoutForcedDecision.get(i).getKey();

if (error) {
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.DECISION_ERROR.reason(flagKey));
decisionMap.put(flagKey, optimizelyDecision);
if (validKeys.contains(flagKey)) {
validKeys.remove(flagKey);
}
}

flagDecisions.put(flagKey, decision.getResult());
decisionReasonsMap.get(flagKey).merge(decision.getReasons());
}
Expand Down Expand Up @@ -1482,6 +1498,151 @@ Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
return decideForKeys(user, allFlagKeys, options);
}

/**
* Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context,
* skipping CMAB logic and using only traditional A/B testing.
*
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
* @param key A flag key for which a decision will be made.
* @param options A list of options for decision-making.
* @return A decision result using traditional A/B testing logic only.
*/
OptimizelyDecision decideWithoutCmab(@Nonnull OptimizelyUserContext user,
@Nonnull String key,
@Nonnull List<OptimizelyDecideOption> options) {
ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
}

List<OptimizelyDecideOption> allOptions = getAllOptions(options);
allOptions.remove(OptimizelyDecideOption.ENABLED_FLAGS_ONLY);

return decideForKeysWithoutCmab(user, Arrays.asList(key), allOptions, true).get(key);
}

/**
* Returns decision results for multiple flag keys, skipping CMAB logic and using only traditional A/B testing.
*
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
* @param keys A list of flag keys for which decisions will be made.
* @param options A list of options for decision-making.
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
*/
Map<String, OptimizelyDecision> decideForKeysWithoutCmab(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options) {
return decideForKeysWithoutCmab(user, keys, options, false);
}

/**
* Returns decision results for all active flag keys, skipping CMAB logic and using only traditional A/B testing.
*
* @param user An OptimizelyUserContext associated with this OptimizelyClient.
* @param options A list of options for decision-making.
* @return All decision results mapped by flag keys, using traditional A/B testing logic only.
*/
Map<String, OptimizelyDecision> decideAllWithoutCmab(@Nonnull OptimizelyUserContext user,
@Nonnull List<OptimizelyDecideOption> options) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
logger.error("Optimizely instance is not valid, failing decideAllWithoutCmab call.");
return decisionMap;
}

List<FeatureFlag> allFlags = projectConfig.getFeatureFlags();
List<String> allFlagKeys = new ArrayList<>();
for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey());

return decideForKeysWithoutCmab(user, allFlagKeys, options);
}

private Map<String, OptimizelyDecision> decideForKeysWithoutCmab(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
logger.error("Optimizely instance is not valid, failing decideForKeysWithoutCmab call.");
return decisionMap;
}

if (keys.isEmpty()) return decisionMap;

List<OptimizelyDecideOption> allOptions = ignoreDefaultOptions ? options : getAllOptions(options);

Map<String, FeatureDecision> flagDecisions = new HashMap<>();
Map<String, DecisionReasons> decisionReasonsMap = new HashMap<>();

List<FeatureFlag> flagsWithoutForcedDecision = new ArrayList<>();

List<String> validKeys = new ArrayList<>();

for (String key : keys) {
FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
if (flag == null) {
decisionMap.put(key, OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key)));
continue;
}

validKeys.add(key);

DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
decisionReasonsMap.put(key, decisionReasons);

OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(key, null);
DecisionResponse<Variation> forcedDecisionVariation = decisionService.validatedForcedDecision(optimizelyDecisionContext, projectConfig, user);
decisionReasons.merge(forcedDecisionVariation.getReasons());
if (forcedDecisionVariation.getResult() != null) {
flagDecisions.put(key,
new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST));
} else {
flagsWithoutForcedDecision.add(flag);
}
}

// Use DecisionService method that skips CMAB logic
List<DecisionResponse<FeatureDecision>> decisionList =
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions, false);

for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) {
DecisionResponse<FeatureDecision> decision = decisionList.get(i);
boolean error = decision.isError();
String flagKey = flagsWithoutForcedDecision.get(i).getKey();

if (error) {
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.DECISION_ERROR.reason(flagKey));
decisionMap.put(flagKey, optimizelyDecision);
if (validKeys.contains(flagKey)) {
validKeys.remove(flagKey);
}
}

flagDecisions.put(flagKey, decision.getResult());
decisionReasonsMap.get(flagKey).merge(decision.getReasons());
}

for (String key : validKeys) {
FeatureDecision flagDecision = flagDecisions.get(key);
DecisionReasons decisionReasons = decisionReasonsMap.get((key));

OptimizelyDecision optimizelyDecision = createOptimizelyDecision(
user, key, flagDecision, decisionReasons, allOptions, projectConfig
);

if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || optimizelyDecision.getEnabled()) {
decisionMap.put(key, optimizelyDecision);
}
}

return decisionMap;
}


private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);
if (options != null) {
Expand Down Expand Up @@ -1731,6 +1892,7 @@ public static class Builder {
private NotificationCenter notificationCenter;
private List<OptimizelyDecideOption> defaultDecideOptions;
private ODPManager odpManager;
private CmabService cmabService;

// For backwards compatibility
private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager();
Expand Down Expand Up @@ -1842,6 +2004,11 @@ public Builder withODPManager(ODPManager odpManager) {
return this;
}

public Builder withCmabService(CmabService cmabService) {
this.cmabService = cmabService;
return this;
}

// Helper functions for making testing easier
protected Builder withBucketing(Bucketer bucketer) {
this.bucketer = bucketer;
Expand Down Expand Up @@ -1873,7 +2040,7 @@ public Optimizely build() {
}

if (decisionService == null) {
decisionService = new DecisionService(bucketer, errorHandler, userProfileService);
decisionService = new DecisionService(bucketer, errorHandler, userProfileService, cmabService);
}

if (projectConfig == null && datafile != null && !datafile.isEmpty()) {
Expand Down Expand Up @@ -1916,7 +2083,7 @@ public Optimizely build() {
defaultDecideOptions = Collections.emptyList();
}

return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions, odpManager);
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions, odpManager, cmabService);
}
}
}
Loading
Loading