Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 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
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
9f7ea59
fix to support android-sdk
jaeopt Oct 30, 2025
93cdaba
clean up
jaeopt Oct 30, 2025
e459321
update: refactor bucketing logic to remove CMAB handling from Decisio…
FarhanAnjum-opti Nov 5, 2025
8c96222
update: introduce CacheWithRemove interface and refactor DefaultCmabS…
FarhanAnjum-opti Nov 6, 2025
7c97734
update: implement CacheWithRemove interface in DefaultLRUCache class
FarhanAnjum-opti Nov 6, 2025
4feb857
update: refactor OptimizelyFactory to remove CMAB cache methods and a…
FarhanAnjum-opti Nov 6, 2025
4b35768
update: refactor DefaultCmabService to streamline logger initializati…
FarhanAnjum-opti Nov 6, 2025
c4c07a6
Merge branch 'farhan-anjum/FSSDK-11170-decision-service-methods-for-c…
FarhanAnjum-opti Nov 6, 2025
5e62fd6
Merge branch 'master' into farhan-anjum/FSSDK-11179-update-impression…
FarhanAnjum-opti Nov 17, 2025
fd75d42
cleanup
FarhanAnjum-opti Nov 17, 2025
e98ec64
triggering fsc with cmab flag on
FarhanAnjum-opti Nov 17, 2025
a1f69cd
testing fix
FarhanAnjum-opti Nov 17, 2025
fd959ed
Revert
FarhanAnjum-opti Nov 17, 2025
41e491a
Add support for CMAB traffic allocation in Bucketer class
FarhanAnjum-opti Nov 17, 2025
b5e691b
Add DecisionPath parameter to bucketing methods for CMAB support
FarhanAnjum-opti Nov 21, 2025
e3cfd5d
Remove unused imports from Optimizely.java
FarhanAnjum-opti Nov 21, 2025
cf4b7e2
Update core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
FarhanAnjum-opti Nov 21, 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
20 changes: 13 additions & 7 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
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 Down Expand Up @@ -306,7 +305,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 @@ -319,6 +318,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 @@ -327,7 +327,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 @@ -337,7 +338,8 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
filteredAttributes,
flagKey,
ruleType,
enabled);
enabled,
cmabUUID);

if (userEvent == null) {
return false;
Expand Down Expand Up @@ -499,7 +501,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 @@ -521,7 +523,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 @@ -1336,6 +1339,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 @@ -1345,7 +1350,8 @@ private OptimizelyDecision createOptimizelyDecision(
flagDecision.variation,
flagKey,
decisionSource.toString(),
flagEnabled);
flagEnabled,
cmabUUID);
}

DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()
Expand Down
46 changes: 40 additions & 6 deletions core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package com.optimizely.ab.bucketing;

import java.util.Collections;
import java.util.List;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -97,7 +98,8 @@ private Experiment bucketToExperiment(@Nonnull Group group,

@Nonnull
private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore experiment,
@Nonnull String bucketingId) {
@Nonnull String bucketingId,
@Nonnull DecisionPath decisionPath) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();

// "salt" the bucket id using the experiment id
Expand All @@ -111,8 +113,25 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore ex
int bucketValue = generateBucketValue(hashCode);
logger.debug("Assigned bucket {} to user with bucketingId \"{}\" when bucketing to a variation.", bucketValue, bucketingId);

// Only apply CMAB traffic allocation logic if decision path is WITH_CMAB
if (decisionPath == DecisionPath.WITH_CMAB && experiment instanceof Experiment && ((Experiment) experiment).getCmab() != null) {
// For CMAB experiments, the original trafficAllocation is kept empty for backward compatibility.
// Use the traffic allocation defined in the CMAB block for bucketing instead.
String message = reasons.addInfo("Using CMAB traffic allocation for experiment \"%s\"", experimentKey);
logger.info(message);
trafficAllocations = Collections.singletonList(
new TrafficAllocation("$", ((Experiment) experiment).getCmab().getTrafficAllocation())
);
}

String bucketedVariationId = bucketToEntity(bucketValue, trafficAllocations);
if (bucketedVariationId != null) {
if (decisionPath == DecisionPath.WITH_CMAB && "$".equals(bucketedVariationId)) {
// for cmab experiments
String message = reasons.addInfo("User with bucketingId \"%s\" is bucketed into CMAB for experiment \"%s\"", bucketingId, experimentKey);
logger.info(message);
return new DecisionResponse(new Variation("$", "$"), reasons);
}
else if (bucketedVariationId != null) {
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId);
String variationKey = bucketedVariation.getKey();
String message = reasons.addInfo("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey,
Expand All @@ -134,12 +153,14 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore ex
* @param experiment The Experiment in which the user is to be bucketed.
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
* @param projectConfig The current projectConfig
* @param decisionPath enum for decision making logic
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
*/
@Nonnull
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
@Nonnull String bucketingId,
@Nonnull ProjectConfig projectConfig) {
@Nonnull ProjectConfig projectConfig,
@Nonnull DecisionPath decisionPath) {
DecisionReasons reasons = DefaultDecisionReasons.newInstance();

// ---------- Bucket User ----------
Expand All @@ -154,8 +175,6 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
String message = reasons.addInfo("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId());
logger.info(message);
return new DecisionResponse(null, reasons);
} else {

}
// if the experiment a user is bucketed in within a group isn't the same as the experiment provided,
// don't perform further bucketing within the experiment
Expand All @@ -172,11 +191,26 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
}
}

DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId);
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId, decisionPath);
reasons.merge(decisionResponse.getReasons());
return new DecisionResponse<>(decisionResponse.getResult(), reasons);
}

/**
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
*
* @param experiment The Experiment in which the user is to be bucketed.
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
* @param projectConfig The current projectConfig
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
*/
@Nonnull
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
@Nonnull String bucketingId,
@Nonnull ProjectConfig projectConfig) {
return bucket(experiment, bucketingId, projectConfig, DecisionPath.WITHOUT_CMAB);
}

//======== Helper methods ========//

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
if (decisionMeetAudience.getResult()) {
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
String cmabUUID = null;
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment) && decisionVariation.getResult() != null) {
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig, decisionPath);
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment) && decisionVariation.getResult() != null) {
// group-allocation and traffic-allocation checking passed for cmab
// we need server decision overruling local bucketing for cmab
DecisionResponse<CmabDecision> cmabDecision = getDecisionForCmabExperiment(projectConfig, experiment, user, bucketingId, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig proje
@Nonnull Map<String, ?> attributes,
@Nonnull String flagKey,
@Nonnull String ruleType,
@Nonnull boolean enabled) {
@Nonnull boolean enabled,
@Nullable String cmabUUID) {

if ((FeatureDecision.DecisionSource.ROLLOUT.toString().equals(ruleType) || variation == null) && !projectConfig.getSendFlagDecisions())
{
Expand All @@ -68,13 +69,18 @@ public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig proje
.withProjectConfig(projectConfig)
.build();

DecisionMetadata metadata = new DecisionMetadata.Builder()
DecisionMetadata.Builder metadataBuilder = new DecisionMetadata.Builder()
.setFlagKey(flagKey)
.setRuleKey(experimentKey)
.setRuleType(ruleType)
.setVariationKey(variationKey)
.setEnabled(enabled)
.build();
.setEnabled(enabled);

if (cmabUUID != null) {
metadataBuilder.setCmabUUID(cmabUUID);
}

DecisionMetadata metadata = metadataBuilder.build();

return new ImpressionEvent.Builder()
.withUserContext(userContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,20 @@ public class DecisionMetadata {
String variationKey;
@JsonProperty("enabled")
boolean enabled;
@JsonProperty("cmab_uuid")
String cmabUUID;

@VisibleForTesting
public DecisionMetadata() {
}

public DecisionMetadata(String flagKey, String ruleKey, String ruleType, String variationKey, boolean enabled) {
public DecisionMetadata(String flagKey, String ruleKey, String ruleType, String variationKey, boolean enabled, String cmabUUID) {
this.flagKey = flagKey;
this.ruleKey = ruleKey;
this.ruleType = ruleType;
this.variationKey = variationKey;
this.enabled = enabled;
this.cmabUUID = cmabUUID;
}

public String getRuleType() {
Expand All @@ -66,6 +69,10 @@ public String getVariationKey() {
return variationKey;
}

public String getCmabUUID() {
return cmabUUID;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -77,6 +84,7 @@ public boolean equals(Object o) {
if (!ruleKey.equals(that.ruleKey)) return false;
if (!flagKey.equals(that.flagKey)) return false;
if (enabled != that.enabled) return false;
if (!java.util.Objects.equals(cmabUUID, that.cmabUUID)) return false;
return variationKey.equals(that.variationKey);
}

Expand All @@ -86,6 +94,7 @@ public int hashCode() {
result = 31 * result + flagKey.hashCode();
result = 31 * result + ruleKey.hashCode();
result = 31 * result + variationKey.hashCode();
result = 31 * result + (cmabUUID != null ? cmabUUID.hashCode() : 0);
return result;
}

Expand All @@ -97,6 +106,7 @@ public String toString() {
.add("ruleType='" + ruleType + "'")
.add("variationKey='" + variationKey + "'")
.add("enabled=" + enabled)
.add("cmabUUID='" + cmabUUID + "'")
.toString();
}

Expand All @@ -108,6 +118,7 @@ public static class Builder {
private String flagKey;
private String variationKey;
private boolean enabled;
private String cmabUUID;

public Builder setEnabled(boolean enabled) {
this.enabled = enabled;
Expand All @@ -134,8 +145,13 @@ public Builder setVariationKey(String variationKey) {
return this;
}

public Builder setCmabUUID(String cmabUUID){
this.cmabUUID = cmabUUID;
return this;
}

public DecisionMetadata build() {
return new DecisionMetadata(flagKey, ruleKey, ruleType, variationKey, enabled);
return new DecisionMetadata(flagKey, ruleKey, ruleType, variationKey, enabled, cmabUUID);
}
}
}
Loading
Loading