Skip to content

Commit 373bed5

Browse files
committed
Merge branch 'master' into jae/FSSDK-11986-jackson-2
2 parents abcf41a + 28d6d5e commit 373bed5

33 files changed

+1991
-406
lines changed

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

Lines changed: 183 additions & 45 deletions
Large diffs are not rendered by default.

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

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@
1616
*/
1717
package com.optimizely.ab;
1818

19-
import com.optimizely.ab.config.ProjectConfig;
20-
import com.optimizely.ab.odp.ODPManager;
21-
import com.optimizely.ab.odp.ODPSegmentCallback;
22-
import com.optimizely.ab.odp.ODPSegmentOption;
23-
import com.optimizely.ab.optimizelydecision.*;
24-
import org.slf4j.Logger;
25-
import org.slf4j.LoggerFactory;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.LinkedList;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
2625

2726
import javax.annotation.Nonnull;
2827
import javax.annotation.Nullable;
29-
import java.util.*;
30-
import java.util.concurrent.ConcurrentHashMap;
28+
29+
import com.optimizely.ab.annotations.VisibleForTesting;
30+
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionCallback;
31+
import com.optimizely.ab.optimizelydecision.OptimizelyDecisionsCallback;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
import com.optimizely.ab.odp.ODPSegmentCallback;
36+
import com.optimizely.ab.odp.ODPSegmentOption;
37+
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
38+
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
3139

3240
public class OptimizelyUserContext {
3341
// OptimizelyForcedDecisionsKey mapped to variationKeys
@@ -42,7 +50,7 @@ public class OptimizelyUserContext {
4250
private List<String> qualifiedSegments;
4351

4452
@Nonnull
45-
private final Optimizely optimizely;
53+
final Optimizely optimizely;
4654

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

@@ -390,4 +398,44 @@ public String toString() {
390398
", attributes='" + attributes + '\'' +
391399
'}';
392400
}
401+
402+
// sync decision support for android-sdk backward compatibility only
403+
404+
@VisibleForTesting // protected, open for testing only
405+
public OptimizelyDecision decideSync(@Nonnull String key,
406+
@Nonnull List<OptimizelyDecideOption> options) {
407+
return optimizely.decideSync(copy(), key, options);
408+
}
409+
410+
@VisibleForTesting // protected, open for testing only
411+
public Map<String, OptimizelyDecision> decideForKeysSync(@Nonnull List<String> keys,
412+
@Nonnull List<OptimizelyDecideOption> options) {
413+
return optimizely.decideForKeysSync(copy(), keys, options);
414+
}
415+
416+
@VisibleForTesting // protected, open for testing only
417+
public Map<String, OptimizelyDecision> decideAllSync(@Nonnull List<OptimizelyDecideOption> options) {
418+
return optimizely.decideAllSync(copy(), options);
419+
}
420+
421+
@VisibleForTesting // protected, open for testing only
422+
public void decideAsync(@Nonnull String key,
423+
@Nonnull List<OptimizelyDecideOption> options,
424+
@Nonnull OptimizelyDecisionCallback callback) {
425+
optimizely.decideAsync(copy(), key, options, callback);
426+
}
427+
428+
@VisibleForTesting // protected, open for testing only
429+
public void decideForKeysAsync(@Nonnull List<String> keys,
430+
@Nonnull List<OptimizelyDecideOption> options,
431+
@Nonnull OptimizelyDecisionsCallback callback) {
432+
optimizely.decideForKeysAsync(copy(), keys, options, callback);
433+
}
434+
435+
@VisibleForTesting // protected, open for testing only
436+
public void decideAllAsync(@Nonnull List<OptimizelyDecideOption> options,
437+
@Nonnull OptimizelyDecisionsCallback callback) {
438+
optimizely.decideAllAsync(copy(), options, callback);
439+
}
440+
393441
}

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
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/****************************************************************************
2+
* Copyright 2025 Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
package com.optimizely.ab.bucketing;
17+
18+
public enum DecisionPath {
19+
WITH_CMAB, // Use CMAB logic
20+
WITHOUT_CMAB // Skip CMAB logic (traditional A/B testing)
21+
}

0 commit comments

Comments
 (0)