Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
a80c0d3
update: enhance CMAB handling in bucketing and decision services, add…
FarhanAnjum-opti Oct 15, 2025
a9ae805
update: add backward compatibility support for Android sync and async…
FarhanAnjum-opti Oct 15, 2025
f25f824
update: add empty list parameter to decision methods in OptimizelyUse…
FarhanAnjum-opti Oct 15, 2025
7363a2f
update: replace null with empty list parameter in async decision meth…
FarhanAnjum-opti Oct 15, 2025
1c52366
update: add useCmab parameter to decideForKeys methods for enhanced d…
FarhanAnjum-opti Oct 15, 2025
b2dcf9e
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 17, 2025
89771bc
update: refactor decision-making logic to use DecisionPath enum for c…
FarhanAnjum-opti Oct 23, 2025
73d5673
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 23, 2025
690379c
Update core-api/src/main/java/com/optimizely/ab/Optimizely.java
FarhanAnjum-opti Oct 23, 2025
9c8dd8f
update: modify OptimizelyUserContext to change optimizely field to pa…
FarhanAnjum-opti Oct 27, 2025
a17becd
update: implement asynchronous decision-making methods in Optimizely …
FarhanAnjum-opti Oct 27, 2025
ba575ce
update: refactor DefaultCmabService to remove CmabServiceOptions depe…
FarhanAnjum-opti Oct 27, 2025
ad474c8
update: refactor DefaultCmabService to use a generic Cache interface …
FarhanAnjum-opti Oct 27, 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
187 changes: 171 additions & 16 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
package com.optimizely.ab;

import com.optimizely.ab.annotations.VisibleForTesting;
import com.optimizely.ab.bucketing.Bucketer;
import com.optimizely.ab.bucketing.DecisionService;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.bucketing.UserProfileService;
import com.optimizely.ab.bucketing.*;
import com.optimizely.ab.cmab.service.CmabCacheValue;
import com.optimizely.ab.cmab.service.CmabService;
import com.optimizely.ab.cmab.service.DefaultCmabService;
import com.optimizely.ab.config.AtomicProjectConfigManager;
import com.optimizely.ab.config.DatafileProjectConfig;
import com.optimizely.ab.config.EventType;
Expand All @@ -45,6 +45,7 @@
import com.optimizely.ab.event.internal.UserEvent;
import com.optimizely.ab.event.internal.UserEventFactory;
import com.optimizely.ab.event.internal.payload.EventBatch;
import com.optimizely.ab.internal.DefaultLRUCache;
import com.optimizely.ab.internal.NotificationRegistry;
import com.optimizely.ab.notification.ActivateNotification;
import com.optimizely.ab.notification.DecisionNotification;
Expand All @@ -62,19 +63,16 @@
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
import com.optimizely.ab.optimizelydecision.DecisionMessage;
import com.optimizely.ab.optimizelydecision.DecisionReasons;
import com.optimizely.ab.optimizelydecision.DecisionResponse;
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
import com.optimizely.ab.optimizelydecision.*;
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -84,6 +82,7 @@
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import com.optimizely.ab.cmab.client.CmabClient;
import static com.optimizely.ab.internal.SafetyUtils.tryClose;

/**
Expand Down Expand Up @@ -141,8 +140,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 +154,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
) {
this.eventHandler = eventHandler;
this.eventProcessor = eventProcessor;
this.errorHandler = errorHandler;
Expand All @@ -164,6 +167,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 @@ -1395,7 +1399,8 @@ Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext use
private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
boolean ignoreDefaultOptions,
DecisionPath decisionPath) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
Expand Down Expand Up @@ -1440,11 +1445,25 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon
}

List<DecisionResponse<FeatureDecision>> decisionList =
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions);
decisionService.getVariationsForFeatureList(flagsWithoutForcedDecision, user, projectConfig, allOptions, decisionPath);

for (int i = 0; i < flagsWithoutForcedDecision.size(); i++) {
DecisionResponse<FeatureDecision> decision = decisionList.get(i);
boolean error = decision.isError();
String experimentKey = null;
if (decision.getResult() != null && decision.getResult().experiment != null) {
experimentKey = decision.getResult().experiment.getKey();
}
String flagKey = flagsWithoutForcedDecision.get(i).getKey();

if (error) {
OptimizelyDecision optimizelyDecision = OptimizelyDecision.newErrorDecision(flagKey, user, DecisionMessage.CMAB_ERROR.reason(experimentKey));
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we always report CMAB error for any decision errors? Is this safe?

Copy link
Contributor Author

@FarhanAnjum-opti FarhanAnjum-opti Oct 15, 2025

Choose a reason for hiding this comment

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

As far as I understand, we get error from decision service only when cmab fails. So this error flag is only true for cmab errors. @raju-opti can verify.

decisionMap.put(flagKey, optimizelyDecision);
if (validKeys.contains(flagKey)) {
validKeys.remove(flagKey);
}
}

flagDecisions.put(flagKey, decision.getResult());
decisionReasonsMap.get(flagKey).merge(decision.getReasons());
}
Expand All @@ -1465,6 +1484,13 @@ private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserCon
return decisionMap;
}

private Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
return decideForKeys(user, keys, options, ignoreDefaultOptions, DecisionPath.WITH_CMAB);
}

Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
@Nonnull List<OptimizelyDecideOption> options) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
Expand All @@ -1482,6 +1508,125 @@ 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.
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
*
* @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 decideSync(@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 decideForKeysSync(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.
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
*
* @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> decideForKeysSync(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options) {
return decideForKeysSync(user, keys, options, false);
}

/**
* Returns decision results for all active flag keys, skipping CMAB logic and using only traditional A/B testing.
* This will be called by mobile apps which will make synchronous decisions only (for backward compatibility with android-sdk)
*
* @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> decideAllSync(@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 decideAllSync 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 decideForKeysSync(user, allFlagKeys, options);
}

private Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull OptimizelyUserContext user,
@Nonnull List<String> keys,
@Nonnull List<OptimizelyDecideOption> options,
boolean ignoreDefaultOptions) {
return decideForKeys(user, keys, options, ignoreDefaultOptions, DecisionPath.WITHOUT_CMAB);
}

//============ decide async ============//

/**
* Returns a decision result asynchronously for a given flag key and a user context.
*
* @param userContext The user context to make decisions for
* @param key A flag key for which a decision will be made
* @param callback A callback to invoke when the decision is available
* @param options A list of options for decision-making
*/
public void decideAsync(@Nonnull OptimizelyUserContext userContext,
@Nonnull String key,
@Nonnull OptimizelyDecisionCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, key, options, callback);
fetcher.start();
}

/**
* Returns decision results asynchronously for multiple flag keys.
*
* @param userContext The user context to make decisions for
* @param keys A list of flag keys for which decisions will be made
* @param callback A callback to invoke when decisions are available
* @param options A list of options for decision-making
*/
public void decideForKeysAsync(@Nonnull OptimizelyUserContext userContext,
@Nonnull List<String> keys,
@Nonnull OptimizelyDecisionsCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, keys, options, callback);
fetcher.start();
}

/**
* Returns decision results asynchronously for all active flag keys.
*
* @param userContext The user context to make decisions for
* @param callback A callback to invoke when decisions are available
* @param options A list of options for decision-making
*/
public void decideAllAsync(@Nonnull OptimizelyUserContext userContext,
@Nonnull OptimizelyDecisionsCallback callback,
@Nonnull List<OptimizelyDecideOption> options) {
AsyncDecisionFetcher fetcher = new AsyncDecisionFetcher(userContext, options, callback);
fetcher.start();
}

private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);
if (options != null) {
Expand Down Expand Up @@ -1731,6 +1876,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 +1988,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 @@ -1872,8 +2023,12 @@ public Optimizely build() {
bucketer = new Bucketer();
}

if (cmabService == null) {
logger.warn("CMAB service is not initiated. CMAB functionality will not be available.");
}

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 +2071,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);
}
}
}
25 changes: 15 additions & 10 deletions core-api/src/main/java/com/optimizely/ab/OptimizelyUserContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@
*/
package com.optimizely.ab;

import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.odp.ODPManager;
import com.optimizely.ab.odp.ODPSegmentCallback;
import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelydecision.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.optimizely.ab.odp.ODPSegmentCallback;
import com.optimizely.ab.odp.ODPSegmentOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;

public class OptimizelyUserContext {
// OptimizelyForcedDecisionsKey mapped to variationKeys
Expand All @@ -42,7 +47,7 @@ public class OptimizelyUserContext {
private List<String> qualifiedSegments;

@Nonnull
private final Optimizely optimizely;
final Optimizely optimizely;

private static final Logger logger = LoggerFactory.getLogger(OptimizelyUserContext.class);

Expand Down
Loading
Loading