Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1d6870f
implement bucketing for rollouts
wangjoshuah Aug 23, 2017
4536501
log variable name
wangjoshuah Aug 24, 2017
48df910
change bucketing to expect an everyone else audience for all feature …
wangjoshuah Aug 24, 2017
d439e4e
change and abstract rollout bucketing
wangjoshuah Aug 29, 2017
5da01a5
address PR from Mike
wangjoshuah Aug 29, 2017
6a8f8b8
fix existing unit tests
wangjoshuah Aug 29, 2017
8f4569a
add test to make sure getVariationForFeatuerInRolloutR returns null w…
wangjoshuah Aug 29, 2017
8b4bf73
add test to make sure getVariationForFeatureInRollout returns null wh…
wangjoshuah Aug 29, 2017
9c970f2
add test for making sure getVariationForFeatureInRollout returns null…
wangjoshuah Aug 30, 2017
0efaad7
add test to see that user gets bucketed into everyone else rule when …
wangjoshuah Aug 30, 2017
e250de2
add test to make sure user gets bucketed into everyone else rule when…
wangjoshuah Aug 30, 2017
f6d3e4a
add new attribute for nationality
wangjoshuah Aug 30, 2017
2e1166a
add english citizens audience
wangjoshuah Aug 30, 2017
0b10a33
add third rollout rule for english citizen audience that is not disjo…
wangjoshuah Aug 30, 2017
8c5e506
add test to ensure no other rules are bucketed when user fails first …
wangjoshuah Aug 30, 2017
4f60179
add test to make sure we continue evaluating more rules when we fail …
wangjoshuah Aug 30, 2017
bc2c95e
add test to make sure feature variable bucketing checks experiment first
wangjoshuah Aug 31, 2017
0fe34d7
add test to make sure we return rollout variation when we do not get …
wangjoshuah Sep 1, 2017
a8f748e
fix dead store jmh bug
wangjoshuah Sep 1, 2017
2e2aa5b
fix rebase issue
wangjoshuah Sep 1, 2017
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
2 changes: 1 addition & 1 deletion core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ 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 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,71 @@ public DecisionService(@Nonnull Bucketer bucketer,
logger.info("The feature flag \"" + featureFlag.getKey() + "\" is not used in any experiments.");
}

return null;
return getVariationForFeatureInRollout(featureFlag, userId, filteredAttributes);
}

/**
* 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 able to be 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
54 changes: 41 additions & 13 deletions core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,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_BOOLEAN_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_DOUBLE_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_SINGLE_VARIABLE_STRING_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_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_DEFAULT_VALUE;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_FIRST_LETTER_KEY;
import static com.optimizely.ab.config.ValidProjectConfigV4.VARIABLE_STRING_VARIABLE_DEFAULT_VALUE;
Expand Down Expand Up @@ -2504,16 +2511,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 +2532,40 @@ 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 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 +2575,25 @@ 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 for \"" + validVariableKey + "\" is being returned."
);
}

Expand Down
Loading