Skip to content

Commit 87c553c

Browse files
Merge branch 'master' into farhan-anjum/FSSDK-11134-cmab-datafile-parsing
2 parents 50334f1 + bc39669 commit 87c553c

15 files changed

+545
-114
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.optimizely.ab.config.DatafileProjectConfig;
2525
import com.optimizely.ab.config.EventType;
2626
import com.optimizely.ab.config.Experiment;
27+
import com.optimizely.ab.config.ExperimentCore;
2728
import com.optimizely.ab.config.FeatureFlag;
2829
import com.optimizely.ab.config.FeatureVariable;
2930
import com.optimizely.ab.config.FeatureVariableUsageInstance;
@@ -319,7 +320,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
319320
* @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout
320321
*/
321322
private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
322-
@Nullable Experiment experiment,
323+
@Nullable ExperimentCore experiment,
323324
@Nonnull String userId,
324325
@Nonnull Map<String, ?> filteredAttributes,
325326
@Nullable Variation variation,
@@ -344,13 +345,17 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig,
344345
if (experiment != null) {
345346
logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey());
346347
}
348+
349+
// Legacy API methods only apply to the Experiment type and not to Holdout.
350+
boolean isExperimentType = experiment instanceof Experiment;
351+
347352
// Kept For backwards compatibility.
348353
// This notification is deprecated and the new DecisionNotifications
349354
// are sent via their respective method calls.
350-
if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0) {
355+
if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0 && isExperimentType) {
351356
LogEvent impressionEvent = EventFactory.createLogEvent(userEvent);
352357
ActivateNotification activateNotification = new ActivateNotification(
353-
experiment, userId, filteredAttributes, variation, impressionEvent);
358+
(Experiment)experiment, userId, filteredAttributes, variation, impressionEvent);
354359
notificationCenter.send(activateNotification);
355360
}
356361
return true;

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

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

19+
import java.util.List;
20+
21+
import javax.annotation.Nonnull;
22+
import javax.annotation.concurrent.Immutable;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
1927
import com.optimizely.ab.annotations.VisibleForTesting;
2028
import com.optimizely.ab.bucketing.internal.MurmurHash3;
21-
import com.optimizely.ab.config.*;
29+
import com.optimizely.ab.config.Experiment;
30+
import com.optimizely.ab.config.ExperimentCore;
31+
import com.optimizely.ab.config.Group;
32+
import com.optimizely.ab.config.ProjectConfig;
33+
import com.optimizely.ab.config.TrafficAllocation;
34+
import com.optimizely.ab.config.Variation;
2235
import com.optimizely.ab.optimizelydecision.DecisionReasons;
2336
import com.optimizely.ab.optimizelydecision.DecisionResponse;
2437
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
27-
28-
import javax.annotation.Nonnull;
29-
import javax.annotation.concurrent.Immutable;
30-
import java.util.List;
3138

3239
/**
3340
* Default Optimizely bucketing algorithm that evenly distributes users using the Murmur3 hash of some provided
3441
* identifier.
3542
* <p>
3643
* The user identifier <i>must</i> be provided in the first data argument passed to
37-
* {@link #bucket(Experiment, String, ProjectConfig)} and <i>must</i> be non-null and non-empty.
44+
* {@link #bucket(ExperimentCore, String, ProjectConfig)} and <i>must</i> be non-null and non-empty.
3845
*
3946
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash</a>
4047
*/
@@ -89,7 +96,7 @@ private Experiment bucketToExperiment(@Nonnull Group group,
8996
}
9097

9198
@Nonnull
92-
private DecisionResponse<Variation> bucketToVariation(@Nonnull Experiment experiment,
99+
private DecisionResponse<Variation> bucketToVariation(@Nonnull ExperimentCore experiment,
93100
@Nonnull String bucketingId) {
94101
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
95102

@@ -130,7 +137,7 @@ private DecisionResponse<Variation> bucketToVariation(@Nonnull Experiment experi
130137
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
131138
*/
132139
@Nonnull
133-
public DecisionResponse<Variation> bucket(@Nonnull Experiment experiment,
140+
public DecisionResponse<Variation> bucket(@Nonnull ExperimentCore experiment,
134141
@Nonnull String bucketingId,
135142
@Nonnull ProjectConfig projectConfig) {
136143
DecisionReasons reasons = DefaultDecisionReasons.newInstance();

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

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,39 @@
1515
***************************************************************************/
1616
package com.optimizely.ab.bucketing;
1717

18+
import java.util.AbstractMap;
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.concurrent.ConcurrentHashMap;
25+
26+
import javax.annotation.Nonnull;
27+
import javax.annotation.Nullable;
28+
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
1832
import com.optimizely.ab.OptimizelyDecisionContext;
1933
import com.optimizely.ab.OptimizelyForcedDecision;
2034
import com.optimizely.ab.OptimizelyRuntimeException;
2135
import com.optimizely.ab.OptimizelyUserContext;
22-
import com.optimizely.ab.config.*;
36+
import com.optimizely.ab.config.Experiment;
37+
import com.optimizely.ab.config.FeatureFlag;
38+
import com.optimizely.ab.config.Holdout;
39+
import com.optimizely.ab.config.ProjectConfig;
40+
import com.optimizely.ab.config.Rollout;
41+
import com.optimizely.ab.config.Variation;
2342
import com.optimizely.ab.error.ErrorHandler;
2443
import com.optimizely.ab.internal.ControlAttribute;
2544
import com.optimizely.ab.internal.ExperimentUtils;
45+
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT;
46+
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE;
2647
import com.optimizely.ab.optimizelydecision.DecisionReasons;
2748
import com.optimizely.ab.optimizelydecision.DecisionResponse;
2849
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
2950
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
30-
import org.slf4j.Logger;
31-
import org.slf4j.LoggerFactory;
32-
import javax.annotation.Nonnull;
33-
import javax.annotation.Nullable;
34-
import java.util.*;
35-
import java.util.concurrent.ConcurrentHashMap;
36-
37-
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT;
38-
import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE;
3951

4052
/**
4153
* Optimizely's decision service that determines which variation of an experiment the user will be allocated to.
@@ -240,10 +252,22 @@ public List<DecisionResponse<FeatureDecision>> getVariationsForFeatureList(@Non
240252

241253
List<DecisionResponse<FeatureDecision>> decisions = new ArrayList<>();
242254

243-
for (FeatureFlag featureFlag: featureFlags) {
255+
flagLoop: for (FeatureFlag featureFlag: featureFlags) {
244256
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
245257
reasons.merge(upsReasons);
246258

259+
List<Holdout> holdouts = projectConfig.getHoldoutForFlag(featureFlag.getId());
260+
if (!holdouts.isEmpty()) {
261+
for (Holdout holdout : holdouts) {
262+
DecisionResponse<Variation> holdoutDecision = getVariationForHoldout(holdout, user, projectConfig);
263+
reasons.merge(holdoutDecision.getReasons());
264+
if (holdoutDecision.getResult() != null) {
265+
decisions.add(new DecisionResponse<>(new FeatureDecision(holdout, holdoutDecision.getResult(), FeatureDecision.DecisionSource.HOLDOUT), reasons));
266+
continue flagLoop;
267+
}
268+
}
269+
}
270+
247271
DecisionResponse<FeatureDecision> decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker);
248272
reasons.merge(decisionVariationResponse.getReasons());
249273

@@ -419,6 +443,54 @@ DecisionResponse<Variation> getWhitelistedVariation(@Nonnull Experiment experime
419443
return new DecisionResponse(null, reasons);
420444
}
421445

446+
/**
447+
* Determines the variation for a holdout rule.
448+
*
449+
* @param holdout The holdout rule to evaluate.
450+
* @param user The user context.
451+
* @param projectConfig The current project configuration.
452+
* @return A {@link DecisionResponse} with the variation (if any) and reasons.
453+
*/
454+
@Nonnull
455+
DecisionResponse<Variation> getVariationForHoldout(@Nonnull Holdout holdout,
456+
@Nonnull OptimizelyUserContext user,
457+
@Nonnull ProjectConfig projectConfig) {
458+
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
459+
460+
if (!holdout.isActive()) {
461+
String message = reasons.addInfo("Holdout (%s) is not running.", holdout.getKey());
462+
logger.info(message);
463+
return new DecisionResponse<>(null, reasons);
464+
}
465+
466+
DecisionResponse<Boolean> decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, holdout, user, EXPERIMENT, holdout.getKey());
467+
reasons.merge(decisionMeetAudience.getReasons());
468+
469+
if (decisionMeetAudience.getResult()) {
470+
// User meets audience conditions for holdout
471+
String audienceMatchMessage = reasons.addInfo("User (%s) meets audience conditions for holdout (%s).", user.getUserId(), holdout.getKey());
472+
logger.info(audienceMatchMessage);
473+
474+
String bucketingId = getBucketingId(user.getUserId(), user.getAttributes());
475+
DecisionResponse<Variation> decisionVariation = bucketer.bucket(holdout, bucketingId, projectConfig);
476+
reasons.merge(decisionVariation.getReasons());
477+
Variation variation = decisionVariation.getResult();
478+
479+
if (variation != null) {
480+
String message = reasons.addInfo("User (%s) is in variation (%s) of holdout (%s).", user.getUserId(), variation.getKey(), holdout.getKey());
481+
logger.info(message);
482+
} else {
483+
String message = reasons.addInfo("User (%s) is in no holdout variation.", user.getUserId());
484+
logger.info(message);
485+
}
486+
return new DecisionResponse<>(variation, reasons);
487+
}
488+
489+
String message = reasons.addInfo("User (%s) does not meet conditions for holdout (%s).", user.getUserId(), holdout.getKey());
490+
logger.info(message);
491+
return new DecisionResponse<>(null, reasons);
492+
}
493+
422494

423495
// TODO: Logically, it makes sense to move this method to UserProfileTracker. But some tests are also calling this
424496
// method, requiring us to refactor those tests as well. We'll look to refactor this later.

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
***************************************************************************/
1616
package com.optimizely.ab.bucketing;
1717

18-
import com.optimizely.ab.config.Experiment;
19-
import com.optimizely.ab.config.Variation;
20-
2118
import javax.annotation.Nullable;
2219

20+
import com.optimizely.ab.config.ExperimentCore;
21+
import com.optimizely.ab.config.Variation;
22+
2323
public class FeatureDecision {
2424
/**
25-
* The {@link Experiment} the Feature is associated with.
25+
* The {@link ExperimentCore} the Feature is associated with.
2626
*/
2727
@Nullable
28-
public Experiment experiment;
28+
public ExperimentCore experiment;
2929

3030
/**
3131
* The {@link Variation} the user was bucketed into.
@@ -41,7 +41,8 @@ public class FeatureDecision {
4141

4242
public enum DecisionSource {
4343
FEATURE_TEST("feature-test"),
44-
ROLLOUT("rollout");
44+
ROLLOUT("rollout"),
45+
HOLDOUT("holdout");
4546

4647
private final String key;
4748

@@ -58,11 +59,11 @@ public String toString() {
5859
/**
5960
* Initialize a FeatureDecision object.
6061
*
61-
* @param experiment The {@link Experiment} the Feature is associated with.
62+
* @param experiment The {@link ExperimentCore} the Feature is associated with.
6263
* @param variation The {@link Variation} the user was bucketed into.
6364
* @param decisionSource The source of the variation.
6465
*/
65-
public FeatureDecision(@Nullable Experiment experiment, @Nullable Variation variation,
66+
public FeatureDecision(@Nullable ExperimentCore experiment, @Nullable Variation variation,
6667
@Nullable DecisionSource decisionSource) {
6768
this.experiment = experiment;
6869
this.variation = variation;

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

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

19+
import java.util.Map;
20+
21+
import javax.annotation.Nonnull;
22+
import javax.annotation.Nullable;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
1927
import com.optimizely.ab.bucketing.FeatureDecision;
20-
import com.optimizely.ab.config.Experiment;
28+
import com.optimizely.ab.config.ExperimentCore;
2129
import com.optimizely.ab.config.ProjectConfig;
2230
import com.optimizely.ab.config.Variation;
2331
import com.optimizely.ab.event.internal.payload.DecisionMetadata;
2432
import com.optimizely.ab.internal.EventTagUtils;
25-
import org.slf4j.Logger;
26-
import org.slf4j.LoggerFactory;
27-
import javax.annotation.Nonnull;
28-
import javax.annotation.Nullable;
29-
import java.util.Map;
3033

3134
public class UserEventFactory {
3235
private static final Logger logger = LoggerFactory.getLogger(UserEventFactory.class);
3336

3437
public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig projectConfig,
35-
@Nullable Experiment activatedExperiment,
38+
@Nullable ExperimentCore activatedExperiment,
3639
@Nullable Variation variation,
3740
@Nonnull String userId,
3841
@Nonnull Map<String, ?> attributes,

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

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

19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import javax.annotation.Nonnull;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
1927
import com.optimizely.ab.OptimizelyUserContext;
2028
import com.optimizely.ab.config.Experiment;
29+
import com.optimizely.ab.config.ExperimentCore;
2130
import com.optimizely.ab.config.ProjectConfig;
2231
import com.optimizely.ab.config.audience.AudienceIdCondition;
2332
import com.optimizely.ab.config.audience.Condition;
2433
import com.optimizely.ab.config.audience.OrCondition;
2534
import com.optimizely.ab.optimizelydecision.DecisionReasons;
2635
import com.optimizely.ab.optimizelydecision.DecisionResponse;
2736
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
28-
import org.slf4j.Logger;
29-
import org.slf4j.LoggerFactory;
30-
31-
import javax.annotation.Nonnull;
32-
import java.util.ArrayList;
33-
import java.util.List;
34-
import java.util.Map;
3537

3638
public final class ExperimentUtils {
3739

@@ -62,7 +64,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) {
6264
*/
6365
@Nonnull
6466
public static DecisionResponse<Boolean> doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig,
65-
@Nonnull Experiment experiment,
67+
@Nonnull ExperimentCore experiment,
6668
@Nonnull OptimizelyUserContext user,
6769
@Nonnull String loggingEntityType,
6870
@Nonnull String loggingKey) {
@@ -86,7 +88,7 @@ public static DecisionResponse<Boolean> doesUserMeetAudienceConditions(@Nonnull
8688

8789
@Nonnull
8890
public static DecisionResponse<Boolean> evaluateAudience(@Nonnull ProjectConfig projectConfig,
89-
@Nonnull Experiment experiment,
91+
@Nonnull ExperimentCore experiment,
9092
@Nonnull OptimizelyUserContext user,
9193
@Nonnull String loggingEntityType,
9294
@Nonnull String loggingKey) {
@@ -118,7 +120,7 @@ public static DecisionResponse<Boolean> evaluateAudience(@Nonnull ProjectConfig
118120

119121
@Nonnull
120122
public static DecisionResponse<Boolean> evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig,
121-
@Nonnull Experiment experiment,
123+
@Nonnull ExperimentCore experiment,
122124
@Nonnull OptimizelyUserContext user,
123125
@Nonnull String loggingEntityType,
124126
@Nonnull String loggingKey) {

core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
*/
1717
package com.optimizely.ab.notification;
1818

19+
import java.util.Map;
20+
1921
import com.optimizely.ab.annotations.VisibleForTesting;
2022
import com.optimizely.ab.config.Experiment;
2123
import com.optimizely.ab.config.Variation;
2224
import com.optimizely.ab.event.LogEvent;
2325

24-
import java.util.Map;
25-
2626
/**
2727
* ActivateNotification supplies notification for AB activatation.
2828
*

core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717

1818
package com.optimizely.ab.notification;
1919

20+
import java.util.Map;
21+
22+
import javax.annotation.Nonnull;
23+
2024
import com.optimizely.ab.config.Experiment;
2125
import com.optimizely.ab.config.Variation;
2226
import com.optimizely.ab.event.LogEvent;
2327

24-
import javax.annotation.Nonnull;
25-
import java.util.Map;
26-
2728
/**
2829
* ActivateNotificationListener handles the activate event notification.
2930
*

0 commit comments

Comments
 (0)