Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
a4419a4
update: add cmabUUID parameter to impression event methods and relate…
FarhanAnjum-opti Sep 26, 2025
416bcbd
update: return cmab error decision whenever found
FarhanAnjum-opti Sep 30, 2025
0213b60
Merge branch 'farhan-anjum/FSSDK-11170-decision-service-methods-for-c…
FarhanAnjum-opti Sep 30, 2025
64f378f
update: enhance error handling by specifying CMAB error messages in d…
FarhanAnjum-opti Oct 1, 2025
d0a2b59
Merge branch 'farhan-anjum/FSSDK-11170-decision-service-methods-for-c…
FarhanAnjum-opti Oct 1, 2025
8539166
update: improve error handling by checking for null values in experim…
FarhanAnjum-opti Oct 1, 2025
16a70cc
Merge branch 'farhan-anjum/FSSDK-11170-decision-service-methods-for-c…
FarhanAnjum-opti Oct 1, 2025
3cee65c
update: fix CMAB error handling by providing a valid Experiment in Fe…
FarhanAnjum-opti Oct 1, 2025
59b90d7
Merge branch 'farhan-anjum/FSSDK-11170-decision-service-methods-for-c…
FarhanAnjum-opti Oct 1, 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
188 changes: 178 additions & 10 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
) {
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 @@ -305,7 +311,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
@Nonnull Map<String, ?> filteredAttributes,
@Nonnull Variation variation,
@Nonnull String ruleType) {
sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType, true);
sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType, true, null);
}

/**
Expand All @@ -318,6 +324,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
* @param variation the variation that was returned from activate.
* @param flagKey It can either be empty if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
* @param cmabUUID The cmabUUID if the experiment is a cmab experiment.
*/
private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
@Nullable ExperimentCore experiment,
Expand All @@ -326,7 +333,8 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
@Nullable Variation variation,
@Nonnull String flagKey,
@Nonnull String ruleType,
@Nonnull boolean enabled) {
@Nonnull boolean enabled,
@Nullable String cmabUUID) {

UserEvent userEvent = UserEventFactory.createImpressionEvent(
projectConfig,
Expand All @@ -336,7 +344,8 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
filteredAttributes,
flagKey,
ruleType,
enabled);
enabled,
cmabUUID);

if (userEvent == null) {
return false;
Expand Down Expand Up @@ -498,7 +507,7 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
if (featureDecision.decisionSource != null) {
decisionSource = featureDecision.decisionSource;
}

String cmabUUID = featureDecision.cmabUUID;
if (featureDecision.variation != null) {
// This information is only necessary for feature tests.
// For rollouts experiments and variations are an implementation detail only.
Expand All @@ -520,7 +529,8 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
featureDecision.variation,
featureKey,
decisionSource.toString(),
featureEnabled);
featureEnabled,
cmabUUID);

DecisionNotification decisionNotification = DecisionNotification.newFeatureDecisionNotificationBuilder()
.withUserId(userId)
Expand Down Expand Up @@ -1349,6 +1359,8 @@ private OptimizelyDecision createOptimizelyDecision(
Map<String, Object> attributes = user.getAttributes();
Map<String, ?> copiedAttributes = new HashMap<>(attributes);

String cmabUUID = flagDecision.cmabUUID;

if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) {
decisionEventDispatched = sendImpression(
projectConfig,
Expand All @@ -1358,7 +1370,8 @@ private OptimizelyDecision createOptimizelyDecision(
flagDecision.variation,
flagKey,
decisionSource.toString(),
flagEnabled);
flagEnabled,
cmabUUID);
}

DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()
Expand Down Expand Up @@ -1444,7 +1457,21 @@ 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 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));
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 +1509,141 @@ 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 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.
*
* @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.
*
* @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) {
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
logger.error("Optimizely instance is not valid, failing decideForKeysSync 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);
String flagKey = flagsWithoutForcedDecision.get(i).getKey();
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 +1893,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 +2005,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 +2041,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 +2084,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