Skip to content

Commit 9625677

Browse files
committed
Merge branch 'master' into 2.0.x
2 parents da0f1f3 + c43046f commit 9625677

File tree

8 files changed

+265
-8
lines changed

8 files changed

+265
-8
lines changed

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

+27-4
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ else if (userId == null) {
347347
Map<String, String> filteredAttributes = filterAttributes(projectConfig, attributes);
348348

349349
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, filteredAttributes);
350-
if (featureDecision.variation != null) {
350+
if (featureDecision.variation == null || !featureDecision.variation.getFeatureEnabled()) {
351+
logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId);
352+
return false;
353+
} else {
351354
if (featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) {
352355
sendImpression(
353356
projectConfig,
@@ -361,9 +364,6 @@ else if (userId == null) {
361364
}
362365
logger.info("Feature \"{}\" is enabled for user \"{}\".", featureKey, userId);
363366
return true;
364-
} else {
365-
logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId);
366-
return false;
367367
}
368368
}
369369

@@ -590,6 +590,29 @@ else if (userId == null) {
590590
return variableValue;
591591
}
592592

593+
/**
594+
* Get the list of features that are enabled for the user.
595+
* @param userId The ID of the user.
596+
* @param attributes The user's attributes.
597+
* @return List of the feature keys that are enabled for the user if the userId is empty it will
598+
* return Empty List.
599+
*/
600+
public List<String> getEnabledFeatures(@Nonnull String userId,@Nonnull Map<String, String> attributes) {
601+
List<String> enabledFeaturesList = new ArrayList<String>();
602+
603+
if (!validateUserId(userId)){
604+
return enabledFeaturesList;
605+
}
606+
607+
for (FeatureFlag featureFlag : projectConfig.getFeatureFlags()){
608+
String featureKey = featureFlag.getKey();
609+
if(isFeatureEnabled(featureKey, userId, attributes))
610+
enabledFeaturesList.add(featureKey);
611+
}
612+
613+
return enabledFeaturesList;
614+
}
615+
593616
//======== getVariation calls ========//
594617

595618
public @Nullable

Diff for: core-api/src/main/java/com/optimizely/ab/config/Variation.java

+16
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,31 @@ public class Variation implements IdKeyMapped {
3737

3838
private final String id;
3939
private final String key;
40+
private final Boolean featureEnabled;
4041
private final List<LiveVariableUsageInstance> liveVariableUsageInstances;
4142
private final Map<String, LiveVariableUsageInstance> variableIdToLiveVariableUsageInstanceMap;
4243

4344
public Variation(String id, String key) {
4445
this(id, key, null);
4546
}
4647

48+
public Variation(String id,
49+
String key,
50+
List<LiveVariableUsageInstance> liveVariableUsageInstances) {
51+
this(id, key,false, liveVariableUsageInstances);
52+
}
53+
4754
@JsonCreator
4855
public Variation(@JsonProperty("id") String id,
4956
@JsonProperty("key") String key,
57+
@JsonProperty("featureEnabled") Boolean featureEnabled,
5058
@JsonProperty("variables") List<LiveVariableUsageInstance> liveVariableUsageInstances) {
5159
this.id = id;
5260
this.key = key;
61+
if(featureEnabled != null)
62+
this.featureEnabled = featureEnabled;
63+
else
64+
this.featureEnabled = false;
5365
if (liveVariableUsageInstances == null) {
5466
this.liveVariableUsageInstances = Collections.emptyList();
5567
}
@@ -67,6 +79,10 @@ public Variation(@JsonProperty("id") String id,
6779
return key;
6880
}
6981

82+
public @Nonnull Boolean getFeatureEnabled() {
83+
return featureEnabled;
84+
}
85+
7086
public @Nullable List<LiveVariableUsageInstance> getLiveVariableUsageInstances() {
7187
return liveVariableUsageInstances;
7288
}

Diff for: core-api/src/main/java/com/optimizely/ab/config/parser/GsonHelpers.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ private static List<Variation> parseVariations(JsonArray variationJson, JsonDese
5050
JsonObject variationObject = (JsonObject)obj;
5151
String id = variationObject.get("id").getAsString();
5252
String key = variationObject.get("key").getAsString();
53+
Boolean featureEnabled = false;
54+
if (variationObject.has("featureEnabled"))
55+
featureEnabled = variationObject.get("featureEnabled").getAsBoolean();
5356

5457
List<LiveVariableUsageInstance> variableUsageInstances = null;
5558
// this is an existence check rather than a version check since it's difficult to pass data
@@ -61,7 +64,7 @@ private static List<Variation> parseVariations(JsonArray variationJson, JsonDese
6164
liveVariableUsageInstancesType);
6265
}
6366

64-
variations.add(new Variation(id, key, variableUsageInstances));
67+
variations.add(new Variation(id, key, featureEnabled, variableUsageInstances));
6568
}
6669

6770
return variations;

Diff for: core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,18 @@ private List<Variation> parseVariations(JSONArray variationJson) {
186186
JSONObject variationObject = (JSONObject)obj;
187187
String id = variationObject.getString("id");
188188
String key = variationObject.getString("key");
189+
Boolean featureEnabled = false;
190+
191+
if(variationObject.has("featureEnabled"))
192+
featureEnabled = variationObject.getBoolean("featureEnabled");
189193

190194
List<LiveVariableUsageInstance> liveVariableUsageInstances = null;
191195
if (variationObject.has("variables")) {
192196
liveVariableUsageInstances =
193197
parseLiveVariableInstances(variationObject.getJSONArray("variables"));
194198
}
195199

196-
variations.add(new Variation(id, key, liveVariableUsageInstances));
200+
variations.add(new Variation(id, key, featureEnabled, liveVariableUsageInstances));
197201
}
198202

199203
return variations;

Diff for: core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,17 @@ private List<Variation> parseVariations(JSONArray variationJson) {
191191
JSONObject variationObject = (JSONObject)obj;
192192
String id = (String)variationObject.get("id");
193193
String key = (String)variationObject.get("key");
194+
Boolean featureEnabled = false;
195+
196+
if(variationObject.containsKey("featureEnabled"))
197+
featureEnabled = (Boolean)variationObject.get("featureEnabled");
194198

195199
List<LiveVariableUsageInstance> liveVariableUsageInstances = null;
196200
if (variationObject.containsKey("variables")) {
197201
liveVariableUsageInstances = parseLiveVariableInstances((JSONArray)variationObject.get("variables"));
198202
}
199203

200-
variations.add(new Variation(id, key, liveVariableUsageInstances));
204+
variations.add(new Variation(id, key, featureEnabled, liveVariableUsageInstances));
201205
}
202206

203207
return variations;

Diff for: core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java

+176-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_INTEGER_VARIABLE_KEY;
100100
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED;
101101
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY;
102+
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_DOUBLE_FEATURE_EXPERIMENT_KEY;
102103
import static com.optimizely.ab.event.LogEvent.RequestMethod;
103104
import static com.optimizely.ab.event.internal.EventBuilderTest.createExperimentVariationMap;
104105
import static java.util.Arrays.asList;
@@ -3439,7 +3440,7 @@ public void isFeatureEnabledReturnsTrueButDoesNotSendWhenUserIsBucketedIntoVaria
34393440
// Should be an experiment from the rollout associated with the feature, but for this test
34403441
// it doesn't matter. Just use any valid experiment.
34413442
Experiment experiment = validProjectConfig.getRolloutIdMapping().get(ROLLOUT_2_ID).getExperiments().get(0);
3442-
Variation variation = new Variation("variationId", "variationKey");
3443+
Variation variation = new Variation("variationId", "variationKey", true, null);
34433444
FeatureDecision featureDecision = new FeatureDecision(experiment, variation, FeatureDecision.DecisionSource.ROLLOUT);
34443445
doReturn(featureDecision).when(mockDecisionService).getVariationForFeature(
34453446
eq(FEATURE_FLAG_MULTI_VARIATE_FEATURE),
@@ -3472,6 +3473,117 @@ public void isFeatureEnabledReturnsTrueButDoesNotSendWhenUserIsBucketedIntoVaria
34723473
verify(mockEventHandler, never()).dispatchEvent(any(LogEvent.class));
34733474
}
34743475

3476+
/**
3477+
* Verify that the {@link Optimizely#activate(String, String, Map<String, String>)} call
3478+
* uses forced variation to force the user into the third variation in which FeatureEnabled is set to
3479+
* false so feature enabled will return false
3480+
*/
3481+
@Test
3482+
public void isFeatureEnabledWithExperimentKeyForcedOfFeatureEnabledFalse() throws Exception {
3483+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3484+
Experiment activatedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY);
3485+
Variation forcedVariation = activatedExperiment.getVariations().get(2);
3486+
EventBuilder mockEventBuilder = mock(EventBuilder.class);
3487+
3488+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
3489+
.withBucketing(mockBucketer)
3490+
.withEventBuilder(mockEventBuilder)
3491+
.withConfig(validProjectConfig)
3492+
.withErrorHandler(mockErrorHandler)
3493+
.build();
3494+
3495+
optimizely.setForcedVariation(activatedExperiment.getKey(), testUserId, forcedVariation.getKey() );
3496+
assertFalse(optimizely.isFeatureEnabled(FEATURE_FLAG_MULTI_VARIATE_FEATURE.getKey(), testUserId));
3497+
}
3498+
3499+
/**
3500+
* Verify that the {@link Optimizely#activate(String, String, Map<String, String>)} call
3501+
* uses forced variation to force the user into the second variation in which FeatureEnabled is not set
3502+
* feature enabled will return false by default
3503+
*/
3504+
@Test
3505+
public void isFeatureEnabledWithExperimentKeyForcedWithNoFeatureEnabledSet() throws Exception {
3506+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3507+
Experiment activatedExperiment = validProjectConfig.getExperimentKeyMapping().get(EXPERIMENT_DOUBLE_FEATURE_EXPERIMENT_KEY);
3508+
Variation forcedVariation = activatedExperiment.getVariations().get(1);
3509+
EventBuilder mockEventBuilder = mock(EventBuilder.class);
3510+
3511+
Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
3512+
.withBucketing(mockBucketer)
3513+
.withEventBuilder(mockEventBuilder)
3514+
.withConfig(validProjectConfig)
3515+
.withErrorHandler(mockErrorHandler)
3516+
.build();
3517+
3518+
optimizely.setForcedVariation(activatedExperiment.getKey(), testUserId, forcedVariation.getKey() );
3519+
assertFalse(optimizely.isFeatureEnabled(FEATURE_SINGLE_VARIABLE_DOUBLE_KEY, testUserId));
3520+
}
3521+
3522+
/**
3523+
* Verify {@link Optimizely#isFeatureEnabled(String, String)} calls into
3524+
* {@link Optimizely#isFeatureEnabled(String, String, Map)} sending FeatureEnabled true and they both
3525+
* return True when the user is bucketed into a variation for the feature.
3526+
* An impression event should not be dispatched since the user was not bucketed into an Experiment.
3527+
* @throws Exception
3528+
*/
3529+
@Test
3530+
public void isFeatureEnabledTrueWhenFeatureEnabledOfVariationIsTrue() throws Exception{
3531+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3532+
3533+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
3534+
3535+
Optimizely spyOptimizely = spy(Optimizely.builder(validDatafile, mockEventHandler)
3536+
.withConfig(validProjectConfig)
3537+
.withDecisionService(mockDecisionService)
3538+
.build());
3539+
// Should be an experiment from the rollout associated with the feature, but for this test
3540+
// it doesn't matter. Just use any valid experiment.
3541+
Experiment experiment = validProjectConfig.getRolloutIdMapping().get(ROLLOUT_2_ID).getExperiments().get(0);
3542+
Variation variation = new Variation("variationId", "variationKey", true, null);
3543+
FeatureDecision featureDecision = new FeatureDecision(experiment, variation, FeatureDecision.DecisionSource.ROLLOUT);
3544+
doReturn(featureDecision).when(mockDecisionService).getVariationForFeature(
3545+
eq(FEATURE_FLAG_MULTI_VARIATE_FEATURE),
3546+
eq(genericUserId),
3547+
eq(Collections.<String, String>emptyMap())
3548+
);
3549+
3550+
assertTrue(spyOptimizely.isFeatureEnabled(validFeatureKey, genericUserId));
3551+
3552+
}
3553+
3554+
3555+
/**
3556+
* Verify {@link Optimizely#isFeatureEnabled(String, String)} calls into
3557+
* {@link Optimizely#isFeatureEnabled(String, String, Map)} sending FeatureEnabled false because of which and they both
3558+
* return false even when the user is bucketed into a variation for the feature.
3559+
* An impression event should not be dispatched since the user was not bucketed into an Experiment.
3560+
* @throws Exception
3561+
*/
3562+
@Test
3563+
public void isFeatureEnabledFalseWhenFeatureEnabledOfVariationIsFalse() throws Exception{
3564+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3565+
3566+
String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
3567+
3568+
Optimizely spyOptimizely = spy(Optimizely.builder(validDatafile, mockEventHandler)
3569+
.withConfig(validProjectConfig)
3570+
.withDecisionService(mockDecisionService)
3571+
.build());
3572+
// Should be an experiment from the rollout associated with the feature, but for this test
3573+
// it doesn't matter. Just use any valid experiment.
3574+
Experiment experiment = validProjectConfig.getRolloutIdMapping().get(ROLLOUT_2_ID).getExperiments().get(0);
3575+
Variation variation = new Variation("variationId", "variationKey", false, null);
3576+
FeatureDecision featureDecision = new FeatureDecision(experiment, variation, FeatureDecision.DecisionSource.ROLLOUT);
3577+
doReturn(featureDecision).when(mockDecisionService).getVariationForFeature(
3578+
eq(FEATURE_FLAG_MULTI_VARIATE_FEATURE),
3579+
eq(genericUserId),
3580+
eq(Collections.<String, String>emptyMap())
3581+
);
3582+
3583+
assertFalse(spyOptimizely.isFeatureEnabled(validFeatureKey, genericUserId));
3584+
3585+
}
3586+
34753587
/** Integration Test
34763588
* Verify {@link Optimizely#isFeatureEnabled(String, String, Map)}
34773589
* returns True
@@ -3503,6 +3615,69 @@ public void isFeatureEnabledReturnsTrueAndDispatchesEventWhenUserIsBucketedIntoA
35033615
verify(mockEventHandler, times(1)).dispatchEvent(any(LogEvent.class));
35043616
}
35053617

3618+
/**
3619+
* Verify {@link Optimizely#getEnabledFeatures(String, Map)} calls into
3620+
* {@link Optimizely#isFeatureEnabled(String, String, Map)} for each featureFlag
3621+
* return List of FeatureFlags that are enabled
3622+
*/
3623+
@Test
3624+
public void getEnabledFeatureWithValidUserId() throws ConfigParseException{
3625+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3626+
3627+
Optimizely spyOptimizely = spy(Optimizely.builder(validDatafile, mockEventHandler)
3628+
.withConfig(validProjectConfig)
3629+
.build());
3630+
ArrayList<String> featureFlags = (ArrayList<String>) spyOptimizely.getEnabledFeatures(genericUserId,
3631+
new HashMap<String, String>());
3632+
assertFalse(featureFlags.isEmpty());
3633+
3634+
}
3635+
3636+
3637+
/**
3638+
* Verify {@link Optimizely#getEnabledFeatures(String, Map)} calls into
3639+
* {@link Optimizely#isFeatureEnabled(String, String, Map)} for each featureFlag sending
3640+
* userId as empty string
3641+
* return empty List of FeatureFlags without checking further.
3642+
*/
3643+
@Test
3644+
public void getEnabledFeatureWithEmptyUserId() throws ConfigParseException{
3645+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3646+
3647+
Optimizely spyOptimizely = spy(Optimizely.builder(validDatafile, mockEventHandler)
3648+
.withConfig(validProjectConfig)
3649+
.build());
3650+
ArrayList<String> featureFlags = (ArrayList<String>) spyOptimizely.getEnabledFeatures("",
3651+
new HashMap<String, String>());
3652+
logbackVerifier.expectMessage(Level.ERROR, "Non-empty user ID required");
3653+
assertTrue(featureFlags.isEmpty());
3654+
3655+
}
3656+
3657+
/**
3658+
* Verify {@link Optimizely#getEnabledFeatures(String, Map)} calls into
3659+
* {@link Optimizely#isFeatureEnabled(String, String, Map)} for each featureFlag sending
3660+
* userId and emptyMap and Mocked {@link Optimizely#isFeatureEnabled(String, String, Map)}
3661+
* to return false so {@link Optimizely#getEnabledFeatures(String, Map)} will
3662+
* return empty List of FeatureFlags.
3663+
*/
3664+
@Test
3665+
public void getEnabledFeatureWithMockIsFeatureEnabledToReturnFalse() throws ConfigParseException{
3666+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
3667+
3668+
Optimizely spyOptimizely = spy(Optimizely.builder(validDatafile, mockEventHandler)
3669+
.withConfig(validProjectConfig)
3670+
.build());
3671+
doReturn(false).when(spyOptimizely).isFeatureEnabled(
3672+
any(String.class),
3673+
eq(genericUserId),
3674+
eq(Collections.<String, String>emptyMap())
3675+
);
3676+
ArrayList<String> featureFlags = (ArrayList<String>) spyOptimizely.getEnabledFeatures(genericUserId,
3677+
Collections.<String, String>emptyMap());
3678+
assertTrue(featureFlags.isEmpty());
3679+
}
3680+
35063681
/**
35073682
* Verify {@link Optimizely#getFeatureVariableString(String, String, String)}
35083683
* calls through to {@link Optimizely#getFeatureVariableString(String, String, String, Map)}

0 commit comments

Comments
 (0)