Skip to content

Commit

Permalink
feat(ForcedDecisions): add forced-decisions APIs to OptimizelyUserCon…
Browse files Browse the repository at this point in the history
…text (#451)

## Summary
Add a set of new APIs for forced-decisions to OptimizelyUserContext:
- setForcedDecision
- getForcedDecision
- removeForcedDecision
- removeAllForcedDecisions

## Test plan
- unit tests for the new APIs
- FSC tests with new test cases
  • Loading branch information
The-inside-man authored Nov 4, 2021
1 parent e25985f commit 04d379a
Show file tree
Hide file tree
Showing 14 changed files with 1,287 additions and 257 deletions.
66 changes: 49 additions & 17 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ public void track(@Nonnull String eventName,
@Nonnull
public Boolean isFeatureEnabled(@Nonnull String featureKey,
@Nonnull String userId) {
return isFeatureEnabled(featureKey, userId, Collections.<String, String>emptyMap());
return isFeatureEnabled(featureKey, userId, Collections.emptyMap());
}

/**
Expand Down Expand Up @@ -424,7 +424,7 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,

Map<String, ?> copiedAttributes = copyAttributes(attributes);
FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
Boolean featureEnabled = false;
SourceInfo sourceInfo = new RolloutSourceInfo();
if (featureDecision.decisionSource != null) {
Expand Down Expand Up @@ -733,7 +733,7 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,

String variableValue = variable.getDefaultValue();
Map<String, ?> copiedAttributes = copyAttributes(attributes);
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
Boolean featureEnabled = false;
if (featureDecision.variation != null) {
if (featureDecision.variation.getFeatureEnabled()) {
Expand Down Expand Up @@ -824,6 +824,7 @@ Object convertStringToType(String variableValue, String type) {
* @param userId The ID of the user.
* @return An OptimizelyJSON instance for all variable values.
* Null if the feature could not be found.
*
*/
@Nullable
public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
Expand All @@ -839,6 +840,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
* @param attributes The user's attributes.
* @return An OptimizelyJSON instance for all variable values.
* Null if the feature could not be found.
*
*/
@Nullable
public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
Expand Down Expand Up @@ -866,7 +868,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
}

Map<String, ?> copiedAttributes = copyAttributes(attributes);
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig, Collections.emptyList()).getResult();
Boolean featureEnabled = false;
Variation variation = featureDecision.variation;

Expand Down Expand Up @@ -922,9 +924,10 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
* @param attributes The user's attributes.
* @return List of the feature keys that are enabled for the user if the userId is empty it will
* return Empty List.
*
*/
public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<String, ?> attributes) {
List<String> enabledFeaturesList = new ArrayList<String>();
List<String> enabledFeaturesList = new ArrayList();
if (!validateUserId(userId)) {
return enabledFeaturesList;
}
Expand All @@ -951,7 +954,7 @@ public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<Stri
public Variation getVariation(@Nonnull Experiment experiment,
@Nonnull String userId) throws UnknownExperimentException {

return getVariation(experiment, userId, Collections.<String, String>emptyMap());
return getVariation(experiment, userId, Collections.emptyMap());
}

@Nullable
Expand All @@ -967,8 +970,7 @@ private Variation getVariation(@Nonnull ProjectConfig projectConfig,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
Map<String, ?> copiedAttributes = copyAttributes(attributes);
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes, projectConfig).getResult();

Variation variation = decisionService.getVariation(experiment, createUserContext(userId, copiedAttributes), projectConfig).getResult();
String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();

if (projectConfig.getExperimentFeatureKeyMapping().get(experiment.getId()) != null) {
Expand Down Expand Up @@ -1145,7 +1147,7 @@ public OptimizelyConfig getOptimizelyConfig() {
* @return An OptimizelyUserContext associated with this OptimizelyClient.
*/
public OptimizelyUserContext createUserContext(@Nonnull String userId,
@Nonnull Map<String, Object> attributes) {
@Nonnull Map<String, ?> attributes) {
if (userId == null) {
logger.warn("The userId parameter must be nonnull.");
return null;
Expand Down Expand Up @@ -1179,14 +1181,24 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);

Map<String, ?> copiedAttributes = new HashMap<>(attributes);
DecisionResponse<FeatureDecision> decisionVariation = decisionService.getVariationForFeature(
flag,
userId,
copiedAttributes,
projectConfig,
allOptions);
FeatureDecision flagDecision = decisionVariation.getResult();
decisionReasons.merge(decisionVariation.getReasons());
FeatureDecision flagDecision;

// Check Forced Decision
OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flag.getKey(), null);
DecisionResponse<Variation> forcedDecisionVariation = user.findValidatedForcedDecision(optimizelyDecisionContext);
decisionReasons.merge(forcedDecisionVariation.getReasons());
if (forcedDecisionVariation.getResult() != null) {
flagDecision = new FeatureDecision(null, forcedDecisionVariation.getResult(), FeatureDecision.DecisionSource.FEATURE_TEST);
} else {
// Regular decision
DecisionResponse<FeatureDecision> decisionVariation = decisionService.getVariationForFeature(
flag,
user,
projectConfig,
allOptions);
flagDecision = decisionVariation.getResult();
decisionReasons.merge(decisionVariation.getReasons());
}

Boolean flagEnabled = false;
if (flagDecision.variation != null) {
Expand Down Expand Up @@ -1332,6 +1344,26 @@ private DecisionResponse<Map<String, Object>> getDecisionVariableMap(@Nonnull Fe
return new DecisionResponse(valuesMap, reasons);
}

/**
* Gets a variation based on flagKey and variationKey
*
* @param flagKey The flag key for the variation
* @param variationKey The variation key for the variation
* @return Returns a variation based on flagKey and variationKey, otherwise null
*/
public Variation getFlagVariationByKey(String flagKey, String variationKey) {
Map<String, List<Variation>> flagVariationsMap = getProjectConfig().getFlagVariationsMap();
if (flagVariationsMap.containsKey(flagKey)) {
List<Variation> variations = flagVariationsMap.get(flagKey);
for (Variation variation : variations) {
if (variation.getKey().equals(variationKey)) {
return variation;
}
}
}
return null;
}

/**
* Helper method which makes separate copy of attributesMap variable and returns it
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
*
* Copyright 2021, Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class OptimizelyDecisionContext {
public static final String OPTI_NULL_RULE_KEY = "$opt-null-rule-key";
public static final String OPTI_KEY_DIVIDER = "-$opt$-";

private String flagKey;
private String ruleKey;

public OptimizelyDecisionContext(@Nonnull String flagKey, @Nullable String ruleKey) {
this.flagKey = flagKey;
this.ruleKey = ruleKey;
}

public String getFlagKey() {
return flagKey;
}

public String getRuleKey() {
return ruleKey != null ? ruleKey : OPTI_NULL_RULE_KEY;
}

public String getKey() {
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(flagKey);
keyBuilder.append(OPTI_KEY_DIVIDER);
keyBuilder.append(getRuleKey());
return keyBuilder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
*
* Copyright 2021, Optimizely and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.optimizely.ab;

import javax.annotation.Nonnull;

public class OptimizelyForcedDecision {
private String variationKey;

public OptimizelyForcedDecision(@Nonnull String variationKey) {
this.variationKey = variationKey;
}

public String getVariationKey() {
return variationKey;
}
}
Loading

0 comments on commit 04d379a

Please sign in to comment.