Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Top-level container class for Optimizely functionality.
Expand Down Expand Up @@ -562,7 +561,8 @@ else if (!variable.getType().equals(variableType)) {
else {
logger.info("User \"" + userId +
"\" was not bucketed into any variation for feature flag \"" + featureKey +
"\". The default value is being returned."
"\". The default value \"" + variableValue +
"\" for \"" + variableKey + "\" is being returned."
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import com.optimizely.ab.config.Experiment;
import com.optimizely.ab.config.FeatureFlag;
import com.optimizely.ab.config.ProjectConfig;
import com.optimizely.ab.config.Rollout;
import com.optimizely.ab.config.Variation;
import com.optimizely.ab.config.audience.Audience;
import com.optimizely.ab.error.ErrorHandler;
import com.optimizely.ab.internal.ExperimentUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -165,7 +167,80 @@ public DecisionService(@Nonnull Bucketer bucketer,
logger.info("The feature flag \"" + featureFlag.getKey() + "\" is not used in any experiments.");
}

return null;
Variation variation = getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes);
if (variation == null) {
logger.info("The user \"" + userId + "\" was not bucketed into a rollout for feature flag \"" +
featureFlag.getKey() + "\".");
}
else {
logger.info("The user \"" + userId + "\" was bucketed into a rollout for feature flag \"" +
featureFlag.getKey() + "\".");
}
return variation;
}

/**
* Try to bucket the user into a rollout rule.
* Evaluate the user for rules in priority order by seeing if the user satisfies the audience.
* Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation.
* @param featureFlag The feature flag the user wants to access.
* @param userId User Identifier
* @param filteredAttributes A map of filtered attributes.
* @return null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout.
* {@link Variation} the user is bucketed into fi the user is successfully bucketed.
*/
@Nullable Variation getVariationForFeatureInRollout(@Nonnull FeatureFlag featureFlag,
@Nonnull String userId,
@Nonnull Map<String, String> filteredAttributes) {
// use rollout to get variation for feature
if (featureFlag.getRolloutId().isEmpty()) {
logger.info("The feature flag \"" + featureFlag.getKey() + "\" is not used in a rollout.");
return null;
}
Rollout rollout = projectConfig.getRolloutIdMapping().get(featureFlag.getRolloutId());
if (rollout == null) {
logger.error("The rollout with id \"" + featureFlag.getRolloutId() +
"\" was not found in the datafile for feature flag \"" + featureFlag.getKey() +
"\".");
return null;
}
int rolloutRulesLength = rollout.getExperiments().size();
Variation variation;
// for all rules before the everyone else rule
for (int i = 0; i < rolloutRulesLength - 1; i++) {
Experiment rolloutRule= rollout.getExperiments().get(i);
Audience audience = projectConfig.getAudienceIdMapping().get(rolloutRule.getAudienceIds().get(0));
if (!rolloutRule.isActive()) {
logger.debug("Did not attempt to bucket user into rollout rule for audience \"" +
audience.getName() + "\" since the rule is not active.");
}
else if (ExperimentUtils.isUserInExperiment(projectConfig, rolloutRule, filteredAttributes)) {
logger.debug("Attempting to bucket user \"" + userId +
"\" into rollout rule for audience \"" + audience.getName() +
"\".");
variation = bucketer.bucket(rolloutRule, userId);
if (variation == null) {
logger.debug("User \"" + userId +
"\" was excluded due to traffic allocation.");
break;
}
return variation;
}
else {
logger.debug("User \"" + userId +
"\" did not meet the conditions to be in rollout rule for audience \"" + audience.getName() +
"\".");
}
}
// get last rule which is the everyone else rule
Experiment everyoneElseRule = rollout.getExperiments().get(rolloutRulesLength - 1);
variation = bucketer.bucket(everyoneElseRule, userId); // ignore audience
if (variation == null) {
logger.debug("User \"" + userId +
"\" was excluded from the \"Everyone Else\" rule for feature flag \"" + featureFlag.getKey() +
"\".");
}
return variation;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public String toString() {
private final Map<String, Audience> audienceIdMapping;
private final Map<String, Experiment> experimentIdMapping;
private final Map<String, Group> groupIdMapping;
private final Map<String, Rollout> rolloutIdMapping;

// other mappings
private final Map<String, List<Experiment>> liveVariableIdToExperimentsMapping;
Expand Down Expand Up @@ -192,6 +193,7 @@ public ProjectConfig(String accountId,
this.audienceIdMapping = ProjectConfigUtils.generateIdMapping(audiences);
this.experimentIdMapping = ProjectConfigUtils.generateIdMapping(this.experiments);
this.groupIdMapping = ProjectConfigUtils.generateIdMapping(groups);
this.rolloutIdMapping = ProjectConfigUtils.generateIdMapping(this.rollouts);

if (liveVariables == null) {
this.liveVariables = null;
Expand Down Expand Up @@ -318,6 +320,10 @@ public Map<String, Group> getGroupIdMapping() {
return groupIdMapping;
}

public Map<String, Rollout> getRolloutIdMapping() {
return rolloutIdMapping;
}

public Map<String, LiveVariable> getLiveVariableKeyMapping() {
return liveVariableKeyMapping;
}
Expand Down Expand Up @@ -488,6 +494,7 @@ public String toString() {
", audienceIdMapping=" + audienceIdMapping +
", experimentIdMapping=" + experimentIdMapping +
", groupIdMapping=" + groupIdMapping +
", rolloutIdMapping=" + rolloutIdMapping +
", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
", forcedVariationMapping=" + forcedVariationMapping +
Expand Down
67 changes: 46 additions & 21 deletions core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import com.optimizely.ab.event.LogEvent;
import com.optimizely.ab.event.internal.EventBuilder;
import com.optimizely.ab.event.internal.EventBuilderV2;
import com.optimizely.ab.event.internal.payload.Feature;
import com.optimizely.ab.internal.LogbackVerifier;
import com.optimizely.ab.notification.NotificationListener;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
Expand All @@ -53,13 +52,13 @@
import org.mockito.junit.MockitoRule;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Map;

import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigJsonV2;
import static com.optimizely.ab.config.ProjectConfigTestUtils.noAudienceProjectConfigJsonV3;
Expand All @@ -80,14 +79,17 @@
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_MULTIVARIATE_EXPERIMENT_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.EXPERIMENT_PAUSED_EXPERIMENT_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_SINGLE_VARIABLE_DOUBLE;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_MULTI_VARIATE_FEATURE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_STRING_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_BOOLEAN_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_DOUBLE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.MULTIVARIATE_EXPERIMENT_FORCED_VARIATION_USER_ID_GRED;
import static com.optimizely.ab.config.ValidProjectConfigV4.PAUSED_EXPERIMENT_FORCED_VARIATION_USER_ID_CONTROL;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_DEFAULT_VALUE;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_BOOLEAN_VARIABLE_DEFAULT_VALUE;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_BOOLEAN_VARIABLE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_DOUBLE_DEFAULT_VALUE;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_DOUBLE_VARIABLE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_DEFAULT_VALUE;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_MULTIVARIATE_EXPERIMENT_GRED_KEY;
import static com.optimizely.ab.event.LogEvent.RequestMethod;
Expand Down Expand Up @@ -2504,16 +2506,16 @@ public void getFeatureVariableValueReturnsNullWhenVariableTypeDoesNotMatch() thr
/**
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
* returns the String default value of a live variable
* when the feature is not attached to an experiment.
* when the feature is not attached to an experiment or a rollout.
* @throws ConfigParseException
*/
@Test
public void getFeatureVariableValueForTypeReturnsDefaultValueWhenFeatureIsNotAttached() throws ConfigParseException {
public void getFeatureVariableValueForTypeReturnsDefaultValueWhenFeatureIsNotAttachedToExperimentOrRollout() throws ConfigParseException {
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));

String validFeatureKey = FEATURE_SINGLE_VARIABLE_STRING_KEY;
String validVariableKey = VARIABLE_STRING_VARIABLE_KEY;
String defaultValue = VARIABLE_STRING_VARIABLE_DEFAULT_VALUE;
String validFeatureKey = FEATURE_SINGLE_VARIABLE_BOOLEAN_KEY;
String validVariableKey = VARIABLE_BOOLEAN_VARIABLE_KEY;
String defaultValue = VARIABLE_BOOLEAN_VARIABLE_DEFAULT_VALUE;
Map<String, String> attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE);

Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
Expand All @@ -2525,28 +2527,41 @@ public void getFeatureVariableValueForTypeReturnsDefaultValueWhenFeatureIsNotAtt
validVariableKey,
genericUserId,
attributes,
LiveVariable.VariableType.STRING);
LiveVariable.VariableType.BOOLEAN);
assertEquals(defaultValue, value);

logbackVerifier.expectMessage(
Level.INFO,
"The feature flag \"" + validFeatureKey + "\" is not used in any experiments."
);
logbackVerifier.expectMessage(
Level.INFO,
"The feature flag \"" + validFeatureKey + "\" is not used in a rollout."
);
logbackVerifier.expectMessage(
Level.INFO,
"User \"" + genericUserId + "\" was not bucketed into any variation for feature flag \"" +
validFeatureKey + "\". The default value \"" +
defaultValue + "\" for \"" +
validVariableKey + "\" is being returned."
);
}

/**
* Verify {@link Optimizely#getFeatureVariableValueForType(String, String, String, Map, LiveVariable.VariableType)}
* returns the String default value for a live variable
* when the feature is attached to an experiment, but the user is excluded from the experiment.
* when the feature is attached to an experiment and no rollout, but the user is excluded from the experiment.
* @throws ConfigParseException
*/
@Test
public void getFeatureVariableValueReturnsDefaultValueWhenFeatureIsAttachedToOneExperimentButFailsTargeting() throws ConfigParseException {
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));

String validFeatureKey = FEATURE_MULTI_VARIATE_FEATURE_KEY;
String validVariableKey = VARIABLE_FIRST_LETTER_KEY;
String expectedValue = VARIABLE_FIRST_LETTER_DEFAULT_VALUE;
String validFeatureKey = FEATURE_SINGLE_VARIABLE_DOUBLE_KEY;
String validVariableKey = VARIABLE_DOUBLE_VARIABLE_KEY;
String expectedValue = VARIABLE_DOUBLE_DEFAULT_VALUE;
FeatureFlag featureFlag = FEATURE_FLAG_SINGLE_VARIABLE_DOUBLE;
Experiment experiment = validProjectConfig.getExperimentIdMapping().get(featureFlag.getExperimentIds().get(0));

Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler)
.withConfig(validProjectConfig)
Expand All @@ -2556,16 +2571,26 @@ public void getFeatureVariableValueReturnsDefaultValueWhenFeatureIsAttachedToOne
validFeatureKey,
validVariableKey,
genericUserId,
Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, "Slytherin"),
LiveVariable.VariableType.STRING
Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, "Ravenclaw"),
LiveVariable.VariableType.DOUBLE
);
assertEquals(expectedValue, valueWithImproperAttributes);

logbackVerifier.expectMessage(
Level.INFO,
"User \"" + genericUserId + "\" does not meet conditions to be in experiment \"" +
experiment.getKey() + "\"."
);
logbackVerifier.expectMessage(
Level.INFO,
"The feature flag \"" + validFeatureKey + "\" is not used in a rollout."
);
logbackVerifier.expectMessage(
Level.INFO,
"User \"" + genericUserId +
"\" was not bucketed into any variation for feature flag \"" + validFeatureKey +
"\". The default value is being returned."
"\". The default value \"" + expectedValue +
"\" for \"" + validVariableKey + "\" is being returned."
);
}

Expand Down
Loading