Skip to content

Commit b60ddec

Browse files
[FSSDK-11179] Update: Send CMAB uuid in impression events (#584)
* update: add CmabService to Optimizely class and builder * update: integrate CMAB service into OptimizelyFactory * update: change CmabService field to non-nullable in Optimizely class * update: add CmabService to DecisionService and its tests * update: implement CMAB traffic allocation in Bucketer and DecisionService * update: enhance DecisionService, FeatureDecision, and DecisionResponse to support CMAB UUID handling * update: enhance DecisionService and DecisionMessage to handle errors and include CMAB UUIDs in responses * update: add validConfigJsonCMAB method to DatafileProjectConfigTestUtils for CMAB configuration * update: add tests to verify precedence of whitelisted and forced variations over CMAB service decisions in DecisionService * update: add test to verify error handling in getVariation for CMAB service failures * update: modify DecisionResponse to include additional error handling information * update: fix error handling assertion in DecisionServiceTest to correctly verify error state * update: add tests for CMAB experiment variations in DecisionService * update: implement decision-making methods to skip CMAB logic in Optimizely and DecisionService * update: add methods to OptimizelyUserContext for decision-making without CMAB logic * update: add asynchronous decision-making methods in OptimizelyUserContext and related fetcher classes * update: add decision-making methods without CMAB logic in OptimizelyUserContextTest * update: remove unused parameter 'useCmab' from DecisionService method documentation * update: rename methods to use 'Sync' suffix for clarity in decision-making logic * update: add cmabUUID parameter to impression event methods and related classes * update: return cmab error decision whenever found * update: enhance error handling by specifying CMAB error messages in decision responses * update: improve error handling by checking for null values in experiment key retrieval * update: fix CMAB error handling by providing a valid Experiment in FeatureDecision * update: add Javadoc comments for async decision methods and config creation in CMAB client * update: refactor build to use cmabClient instead of default service * update: refactor DefaultCmabClient to utilize CmabClientHelper * update: refactor AsyncDecisionsFetcher to AsyncDecisionFetcher and enhance decision handling * update: add missing copyright notice and license information to CmabClientHelper * update: enhance CMAB handling in bucketing and decision services, add backward compatibility for mobile apps * update: add backward compatibility support for Android sync and async decisions in OptimizelyUserContext * update: add empty list parameter to decision methods in OptimizelyUserContextTest for consistency * update: replace null with empty list parameter in async decision method for consistency * update: add useCmab parameter to decideForKeys methods for enhanced decision handling * Update core-api/src/main/java/com/optimizely/ab/Optimizely.java Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * update: refactor decision-making logic to use DecisionPath enum for clarity and maintainability * Update core-api/src/main/java/com/optimizely/ab/Optimizely.java Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * Update core-api/src/main/java/com/optimizely/ab/Optimizely.java Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> * update: modify OptimizelyUserContext to change optimizely field to package-private and add copyright notice to DecisionPath * update: implement asynchronous decision-making methods in Optimizely and OptimizelyUserContext with corresponding tests * update: refactor DefaultCmabService to remove CmabServiceOptions dependency and adjust related tests * update: refactor DefaultCmabService to use a generic Cache interface and enhance builder methods for cache configuration * fix to support android-sdk * clean up * update: refactor bucketing logic to remove CMAB handling from DecisionService and adjust tests accordingly * update: introduce CacheWithRemove interface and refactor DefaultCmabService to utilize it * update: implement CacheWithRemove interface in DefaultLRUCache class * update: refactor OptimizelyFactory to remove CMAB cache methods and adjust instance creation logic * update: refactor DefaultCmabService to streamline logger initialization and enhance cache handling logging * cleanup * triggering fsc with cmab flag on * testing fix * Revert * Add support for CMAB traffic allocation in Bucketer class * Add DecisionPath parameter to bucketing methods for CMAB support * Remove unused imports from Optimizely.java * Update core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java comment update Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> --------- Co-authored-by: Jae Kim <45045038+jaeopt@users.noreply.github.com> Co-authored-by: Jae Kim <jae.kim@optimizely.com>
1 parent efbda89 commit b60ddec

File tree

9 files changed

+208
-43
lines changed

9 files changed

+208
-43
lines changed

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import com.optimizely.ab.event.internal.UserEvent;
4444
import com.optimizely.ab.event.internal.UserEventFactory;
4545
import com.optimizely.ab.event.internal.payload.EventBatch;
46-
import com.optimizely.ab.internal.DefaultLRUCache;
4746
import com.optimizely.ab.internal.NotificationRegistry;
4847
import com.optimizely.ab.notification.ActivateNotification;
4948
import com.optimizely.ab.notification.DecisionNotification;
@@ -306,7 +305,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
306305
@Nonnull Map<String, ?> filteredAttributes,
307306
@Nonnull Variation variation,
308307
@Nonnull String ruleType) {
309-
sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType, true);
308+
sendImpression(projectConfig, experiment, userId, filteredAttributes, variation, "", ruleType, true, null);
310309
}
311310

312311
/**
@@ -319,6 +318,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
319318
* @param variation the variation that was returned from activate.
320319
* @param flagKey It can either be empty if ruleType is experiment or it's feature key in case ruleType is feature-test or rollout
321320
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
321+
* @param cmabUUID The cmabUUID if the experiment is a cmab experiment.
322322
*/
323323
private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
324324
@Nullable ExperimentCore experiment,
@@ -327,7 +327,8 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
327327
@Nullable Variation variation,
328328
@Nonnull String flagKey,
329329
@Nonnull String ruleType,
330-
@Nonnull boolean enabled) {
330+
@Nonnull boolean enabled,
331+
@Nullable String cmabUUID) {
331332

332333
UserEvent userEvent = UserEventFactory.createImpressionEvent(
333334
projectConfig,
@@ -337,7 +338,8 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
337338
filteredAttributes,
338339
flagKey,
339340
ruleType,
340-
enabled);
341+
enabled,
342+
cmabUUID);
341343

342344
if (userEvent == null) {
343345
return false;
@@ -499,7 +501,7 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
499501
if (featureDecision.decisionSource != null) {
500502
decisionSource = featureDecision.decisionSource;
501503
}
502-
504+
String cmabUUID = featureDecision.cmabUUID;
503505
if (featureDecision.variation != null) {
504506
// This information is only necessary for feature tests.
505507
// For rollouts experiments and variations are an implementation detail only.
@@ -521,7 +523,8 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
521523
featureDecision.variation,
522524
featureKey,
523525
decisionSource.toString(),
524-
featureEnabled);
526+
featureEnabled,
527+
cmabUUID);
525528

526529
DecisionNotification decisionNotification = DecisionNotification.newFeatureDecisionNotificationBuilder()
527530
.withUserId(userId)
@@ -1336,6 +1339,8 @@ private OptimizelyDecision createOptimizelyDecision(
13361339
Map<String, Object> attributes = user.getAttributes();
13371340
Map<String, ?> copiedAttributes = new HashMap<>(attributes);
13381341

1342+
String cmabUUID = flagDecision.cmabUUID;
1343+
13391344
if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) {
13401345
decisionEventDispatched = sendImpression(
13411346
projectConfig,
@@ -1345,7 +1350,8 @@ private OptimizelyDecision createOptimizelyDecision(
13451350
flagDecision.variation,
13461351
flagKey,
13471352
decisionSource.toString(),
1348-
flagEnabled);
1353+
flagEnabled,
1354+
cmabUUID);
13491355
}
13501356

13511357
DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()

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

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.optimizely.ab.bucketing;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021

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

9899
@Nonnull
99100
private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore experiment,
100-
@Nonnull String bucketingId) {
101+
@Nonnull String bucketingId,
102+
@Nonnull DecisionPath decisionPath) {
101103
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
102104

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

116+
// Only apply CMAB traffic allocation logic if decision path is WITH_CMAB
117+
if (decisionPath == DecisionPath.WITH_CMAB && experiment instanceof Experiment && ((Experiment) experiment).getCmab() != null) {
118+
// For CMAB experiments, the original trafficAllocation is kept empty for backward compatibility.
119+
// Use the traffic allocation defined in the CMAB block for bucketing instead.
120+
String message = reasons.addInfo("Using CMAB traffic allocation for experiment \"%s\"", experimentKey);
121+
logger.info(message);
122+
trafficAllocations = Collections.singletonList(
123+
new TrafficAllocation("$", ((Experiment) experiment).getCmab().getTrafficAllocation())
124+
);
125+
}
126+
114127
String bucketedVariationId = bucketToEntity(bucketValue, trafficAllocations);
115-
if (bucketedVariationId != null) {
128+
if (decisionPath == DecisionPath.WITH_CMAB && "$".equals(bucketedVariationId)) {
129+
// for cmab experiments
130+
String message = reasons.addInfo("User with bucketingId \"%s\" is bucketed into CMAB for experiment \"%s\"", bucketingId, experimentKey);
131+
logger.info(message);
132+
return new DecisionResponse(new Variation("$", "$"), reasons);
133+
}
134+
else if (bucketedVariationId != null) {
116135
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId);
117136
String variationKey = bucketedVariation.getKey();
118137
String message = reasons.addInfo("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey,
@@ -134,12 +153,14 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore ex
134153
* @param experiment The Experiment in which the user is to be bucketed.
135154
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
136155
* @param projectConfig The current projectConfig
156+
* @param decisionPath enum for decision making logic
137157
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
138158
*/
139159
@Nonnull
140160
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
141161
@Nonnull String bucketingId,
142-
@Nonnull ProjectConfig projectConfig) {
162+
@Nonnull ProjectConfig projectConfig,
163+
@Nonnull DecisionPath decisionPath) {
143164
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
144165

145166
// ---------- Bucket User ----------
@@ -154,8 +175,6 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
154175
String message = reasons.addInfo("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId());
155176
logger.info(message);
156177
return new DecisionResponse(null, reasons);
157-
} else {
158-
159178
}
160179
// if the experiment a user is bucketed in within a group isn't the same as the experiment provided,
161180
// don't perform further bucketing within the experiment
@@ -172,11 +191,26 @@ public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
172191
}
173192
}
174193

175-
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId);
194+
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId, decisionPath);
176195
reasons.merge(decisionResponse.getReasons());
177196
return new DecisionResponse<>(decisionResponse.getResult(), reasons);
178197
}
179198

199+
/**
200+
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3.
201+
*
202+
* @param experiment The Experiment in which the user is to be bucketed.
203+
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
204+
* @param projectConfig The current projectConfig
205+
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
206+
*/
207+
@Nonnull
208+
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
209+
@Nonnull String bucketingId,
210+
@Nonnull ProjectConfig projectConfig) {
211+
return bucket(experiment, bucketingId, projectConfig, DecisionPath.WITHOUT_CMAB);
212+
}
213+
180214
//======== Helper methods ========//
181215

182216
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ public DecisionResponse<Variation> getVariation(@Nonnull Experiment experiment,
170170
if (decisionMeetAudience.getResult()) {
171171
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
172172
String cmabUUID = null;
173-
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig);
174-
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment) && decisionVariation.getResult() != null) {
173+
decisionVariation = bucketer.bucket(experiment, bucketingId, projectConfig, decisionPath);
174+
if (decisionPath == DecisionPath.WITH_CMAB && isCmabExperiment(experiment) && decisionVariation.getResult() != null) {
175175
// group-allocation and traffic-allocation checking passed for cmab
176176
// we need server decision overruling local bucketing for cmab
177177
DecisionResponse<CmabDecision> cmabDecision = getDecisionForCmabExperiment(projectConfig, experiment, user, bucketingId, options);

core-api/src/main/java/com/optimizely/ab/event/internal/UserEventFactory.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig proje
4141
@Nonnull Map<String, ?> attributes,
4242
@Nonnull String flagKey,
4343
@Nonnull String ruleType,
44-
@Nonnull boolean enabled) {
44+
@Nonnull boolean enabled,
45+
@Nullable String cmabUUID) {
4546

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

71-
DecisionMetadata metadata = new DecisionMetadata.Builder()
72+
DecisionMetadata.Builder metadataBuilder = new DecisionMetadata.Builder()
7273
.setFlagKey(flagKey)
7374
.setRuleKey(experimentKey)
7475
.setRuleType(ruleType)
7576
.setVariationKey(variationKey)
76-
.setEnabled(enabled)
77-
.build();
77+
.setEnabled(enabled);
78+
79+
if (cmabUUID != null) {
80+
metadataBuilder.setCmabUUID(cmabUUID);
81+
}
82+
83+
DecisionMetadata metadata = metadataBuilder.build();
7884

7985
return new ImpressionEvent.Builder()
8086
.withUserContext(userContext)

core-api/src/main/java/com/optimizely/ab/event/internal/payload/DecisionMetadata.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ public class DecisionMetadata {
3333
String variationKey;
3434
@JsonProperty("enabled")
3535
boolean enabled;
36+
@JsonProperty("cmab_uuid")
37+
String cmabUUID;
3638

3739
@VisibleForTesting
3840
public DecisionMetadata() {
3941
}
4042

41-
public DecisionMetadata(String flagKey, String ruleKey, String ruleType, String variationKey, boolean enabled) {
43+
public DecisionMetadata(String flagKey, String ruleKey, String ruleType, String variationKey, boolean enabled, String cmabUUID) {
4244
this.flagKey = flagKey;
4345
this.ruleKey = ruleKey;
4446
this.ruleType = ruleType;
4547
this.variationKey = variationKey;
4648
this.enabled = enabled;
49+
this.cmabUUID = cmabUUID;
4750
}
4851

4952
public String getRuleType() {
@@ -66,6 +69,10 @@ public String getVariationKey() {
6669
return variationKey;
6770
}
6871

72+
public String getCmabUUID() {
73+
return cmabUUID;
74+
}
75+
6976
@Override
7077
public boolean equals(Object o) {
7178
if (this == o) return true;
@@ -77,6 +84,7 @@ public boolean equals(Object o) {
7784
if (!ruleKey.equals(that.ruleKey)) return false;
7885
if (!flagKey.equals(that.flagKey)) return false;
7986
if (enabled != that.enabled) return false;
87+
if (!java.util.Objects.equals(cmabUUID, that.cmabUUID)) return false;
8088
return variationKey.equals(that.variationKey);
8189
}
8290

@@ -86,6 +94,7 @@ public int hashCode() {
8694
result = 31 * result + flagKey.hashCode();
8795
result = 31 * result + ruleKey.hashCode();
8896
result = 31 * result + variationKey.hashCode();
97+
result = 31 * result + (cmabUUID != null ? cmabUUID.hashCode() : 0);
8998
return result;
9099
}
91100

@@ -97,6 +106,7 @@ public String toString() {
97106
.add("ruleType='" + ruleType + "'")
98107
.add("variationKey='" + variationKey + "'")
99108
.add("enabled=" + enabled)
109+
.add("cmabUUID='" + cmabUUID + "'")
100110
.toString();
101111
}
102112

@@ -108,6 +118,7 @@ public static class Builder {
108118
private String flagKey;
109119
private String variationKey;
110120
private boolean enabled;
121+
private String cmabUUID;
111122

112123
public Builder setEnabled(boolean enabled) {
113124
this.enabled = enabled;
@@ -134,8 +145,13 @@ public Builder setVariationKey(String variationKey) {
134145
return this;
135146
}
136147

148+
public Builder setCmabUUID(String cmabUUID){
149+
this.cmabUUID = cmabUUID;
150+
return this;
151+
}
152+
137153
public DecisionMetadata build() {
138-
return new DecisionMetadata(flagKey, ruleKey, ruleType, variationKey, enabled);
154+
return new DecisionMetadata(flagKey, ruleKey, ruleType, variationKey, enabled, cmabUUID);
139155
}
140156
}
141157
}

0 commit comments

Comments
 (0)