Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0edcfe6
added force bucketing
thomaszurkan-optimizely Aug 15, 2017
8c0ffc9
get test working
thomaszurkan-optimizely Aug 15, 2017
b487cb6
fix broken test and fix code errors
thomaszurkan-optimizely Aug 16, 2017
d0909e3
unit test forced variations
thomaszurkan-optimizely Aug 16, 2017
b9e741a
fix EventBuilderV2Test
thomaszurkan-optimizely Aug 16, 2017
ba2ea7f
move force bucketing logic to decision service. enhance decision tes…
thomaszurkan-optimizely Aug 16, 2017
7661164
move methods to project config. decision logic in decision service
thomaszurkan-optimizely Aug 17, 2017
6ecb1e1
update to reflect spacing issues with my cut and paste
thomaszurkan-optimizely Aug 17, 2017
70f6e05
more line cleanup
thomaszurkan-optimizely Aug 17, 2017
bf0efd9
use variation and experiment ids instead of key for storage. use put…
thomaszurkan-optimizely Aug 17, 2017
a5bae5d
added all unit tests and code coverage report from jacocoTestReport
thomaszurkan-optimizely Aug 18, 2017
e6e2446
added logging for projectConfig, better testing, no import star, test…
thomaszurkan-optimizely Aug 21, 2017
884c77e
added missing decisionService test for non-running experiement with f…
thomaszurkan-optimizely Aug 21, 2017
b40368b
add getForceVariation check
thomaszurkan-optimizely Aug 21, 2017
1ffc359
fix comments for test of non-running experiment
thomaszurkan-optimizely Aug 22, 2017
951f161
update comments to be more consistent with php sdk
thomaszurkan-optimizely Aug 22, 2017
7db9cc8
resolve conflicts
thomaszurkan-optimizely Aug 22, 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
37 changes: 37 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
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 @@ -615,6 +616,42 @@ Variation getVariation(@Nonnull String experimentKey,
return decisionService.getVariation(experiment,userId,filteredAttributes);
}

/**
* Force a user into a variation for a given experiment.
* The forced variation value does not persist across application launches.
* If the experiment key is not in the project file, this call fails and returns false.
* If the variationKey is not in the experiment, this call fails.
* @param experimentKey The key for the experiment.
* @param userId The user ID to be used for bucketing.
* @param variationKey The variation key to force the user into. If the variation key is null
* then the forcedVariation for that experiment is removed.
*
* @return boolean A boolean value that indicates if the set completed successfully.
*/
public boolean setForcedVariation(@Nonnull String experimentKey,
@Nonnull String userId,
@Nullable String variationKey) {


return projectConfig.setForcedVariation(experimentKey, userId, variationKey);
}

/**
* Gets the forced variation for a given user and experiment.
* This method just calls into the {@link com.optimizely.ab.config.ProjectConfig#getForcedVariation(String, String)}
* method of the same signature.
*
* @param experimentKey The key for the experiment.
* @param userId The user ID to be used for bucketing.
*
* @return The variation the user was bucketed into. This value can be null if the
* forced variation fails.
*/
public @Nullable Variation getForcedVariation(@Nonnull String experimentKey,
@Nonnull String userId) {
return projectConfig.getForcedVariation(experimentKey, userId);
}

/**
* @return the current {@link ProjectConfig} instance.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ public DecisionService(@Nonnull Bucketer bucketer,
return null;
}

// look for forced bucketing first.
Variation variation = projectConfig.getForcedVariation(experiment.getKey(), userId);

// check for whitelisting
Variation variation = getWhitelistedVariation(experiment, userId);
if (variation == null) {
variation = getWhitelistedVariation(experiment, userId);
}

if (variation != null) {
return variation;
}
Expand Down
147 changes: 147 additions & 0 deletions core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.optimizely.ab.config.audience.Audience;
import com.optimizely.ab.config.audience.Condition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Represents the Optimizely Project configuration.
Expand Down Expand Up @@ -54,6 +58,10 @@ public String toString() {
}
}

// logger
private static final Logger logger = LoggerFactory.getLogger(ProjectConfig.class);

// ProjectConfig properties
private final String accountId;
private final String projectId;
private final String revision;
Expand Down Expand Up @@ -85,6 +93,14 @@ public String toString() {
private final Map<String, Map<String, LiveVariableUsageInstance>> variationToLiveVariableUsageInstanceMapping;
private final Map<String, Experiment> variationIdToExperimentMapping;

/**
* Forced variations supersede any other mappings. They are transient and are not persistent or part of
* the actual datafile. This contains all the forced variations
* set by the user by calling {@link ProjectConfig#setForcedVariation(String, String, String)} (it is not the same as the
* whitelisting forcedVariations data structure in the Experiments class).
*/
private transient ConcurrentHashMap<String, ConcurrentHashMap<String, String>> forcedVariationMapping = new ConcurrentHashMap<String, ConcurrentHashMap<String, String>>();

// v2 constructor
public ProjectConfig(String accountId, String projectId, String version, String revision, List<Group> groups,
List<Experiment> experiments, List<Attribute> attributes, List<EventType> eventType,
Expand Down Expand Up @@ -318,6 +334,136 @@ public Map<String, FeatureFlag> getFeatureKeyMapping() {
return featureKeyMapping;
}

public ConcurrentHashMap<String, ConcurrentHashMap<String, String>> getForcedVariationMapping() { return forcedVariationMapping; }

/**
* Force a user into a variation for a given experiment.
* The forced variation value does not persist across application launches.
* If the experiment key is not in the project file, this call fails and returns false.
*
* @param experimentKey The key for the experiment.
* @param userId The user ID to be used for bucketing.
* @param variationKey The variation key to force the user into. If the variation key is null
* then the forcedVariation for that experiment is removed.
*
* @return boolean A boolean value that indicates if the set completed successfully.
*/
public boolean setForcedVariation(@Nonnull String experimentKey,
@Nonnull String userId,
@Nullable String variationKey) {

// if the experiment is not a valid experiment key, don't set it.
Experiment experiment = getExperimentKeyMapping().get(experimentKey);
if (experiment == null){
logger.error("Experiment {} does not exist in ProjectConfig for project {}", experimentKey, projectId);
return false;
}

Variation variation = null;

// keep in mind that you can pass in a variationKey that is null if you want to
// remove the variation.
if (variationKey != null) {
variation = experiment.getVariationKeyToVariationMap().get(variationKey);
// if the variation is not part of the experiment, return false.
if (variation == null) {
logger.error("Variation {} does not exist for experiment {}", variationKey, experimentKey);
return false;
}
}

// if the user id is invalid, return false.
if (userId == null || userId.trim().isEmpty()) {
logger.error("User ID is invalid");
return false;
}

ConcurrentHashMap<String, String> experimentToVariation;
if (!forcedVariationMapping.containsKey(userId)) {
forcedVariationMapping.putIfAbsent(userId, new ConcurrentHashMap<String, String>());
}
experimentToVariation = forcedVariationMapping.get(userId);

boolean retVal = true;
// if it is null remove the variation if it exists.
if (variationKey == null) {
String removedVariationId = experimentToVariation.remove(experiment.getId());
if (removedVariationId != null) {
Variation removedVariation = experiment.getVariationIdToVariationMap().get(removedVariationId);
if (removedVariation != null) {
logger.debug("Variation mapped to experiment \"%s\" has been removed for user \"%s\"", experiment.getKey(), userId);
}
else {
logger.debug("Removed forced variation that did not exist in experiment");
}
}
else {
logger.debug("No variation for experiment {}", experimentKey);
retVal = false;
}
}
else {
String previous = experimentToVariation.put(experiment.getId(), variation.getId());
logger.debug("Set variation \"%s\" for experiment \"%s\" and user \"%s\" in the forced variation map.",
variation.getKey(), experiment.getKey(), userId);
if (previous != null) {
Variation previousVariation = experiment.getVariationIdToVariationMap().get(previous);
if (previousVariation != null) {
logger.debug("forced variation {} replaced forced variation {} in forced variation map.",
variation.getKey(), previousVariation.getKey());
}
}
}

return retVal;
}

/**
* Gets the forced variation for a given user and experiment.
*
* @param experimentKey The key for the experiment.
* @param userId The user ID to be used for bucketing.
*
* @return The variation the user was bucketed into. This value can be null if the
* forced variation fails.
*/
public @Nullable Variation getForcedVariation(@Nonnull String experimentKey,
@Nonnull String userId) {

// if the user id is invalid, return false.
if (userId == null || userId.trim().isEmpty()) {
logger.error("User ID is invalid");
return null;
}

if (experimentKey == null || experimentKey.isEmpty()) {
logger.error("experiment key is invalid");
return null;
}

Map<String, String> experimentToVariation = getForcedVariationMapping().get(userId);
if (experimentToVariation != null) {
Experiment experiment = getExperimentKeyMapping().get(experimentKey);
if (experiment == null) {
logger.debug("No experiment \"%s\" mapped to user \"%s\" in the forced variation map ", experimentKey, userId);
return null;
}
String variationId = experimentToVariation.get(experiment.getId());
if (variationId != null) {
Variation variation = experiment.getVariationIdToVariationMap().get(variationId);
if (variation != null) {
logger.debug("Variation \"%s\" is mapped to experiment \"%s\" and user \"%s\" in the forced variation map",
variation.getKey(), experimentKey, userId);
return variation;
}
}
else {
logger.debug("No variation for experiment \"%s\" mapped to user \"%s\" in the forced variation map ", experimentKey, userId);
}
}
return null;
}

@Override
public String toString() {
return "ProjectConfig{" +
Expand All @@ -344,6 +490,7 @@ public String toString() {
", groupIdMapping=" + groupIdMapping +
", liveVariableIdToExperimentsMapping=" + liveVariableIdToExperimentsMapping +
", variationToLiveVariableUsageInstanceMapping=" + variationToLiveVariableUsageInstanceMapping +
", forcedVariationMapping=" + forcedVariationMapping +
", variationIdToExperimentMapping=" + variationIdToExperimentMapping +
'}';
}
Expand Down
Loading