Skip to content

feat: add getFeatureVariableJSON and getAllFeatureVariables apis #375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
May 7, 2020
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
157 changes: 150 additions & 7 deletions core-api/src/main/java/com/optimizely/ab/Optimizely.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@
import com.optimizely.ab.error.ErrorHandler;
import com.optimizely.ab.error.NoOpErrorHandler;
import com.optimizely.ab.event.*;
import com.optimizely.ab.event.internal.*;
import com.optimizely.ab.event.internal.ClientEngineInfo;
import com.optimizely.ab.event.internal.EventFactory;
import com.optimizely.ab.event.internal.UserEvent;
import com.optimizely.ab.event.internal.UserEventFactory;
import com.optimizely.ab.event.internal.payload.EventBatch;
import com.optimizely.ab.notification.*;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

import static com.optimizely.ab.internal.SafetyUtils.tryClose;

Expand Down Expand Up @@ -601,6 +601,46 @@ public String getFeatureVariableString(@Nonnull String featureKey,
FeatureVariable.STRING_TYPE);
}

/**
* Get the JSON value of the specified variable in the feature.
*
* @param featureKey The unique key of the feature.
* @param variableKey The unique key of the variable.
* @param userId The ID of the user.
* @return An OptimizelyJSON instance for the JSON variable value.
* Null if the feature or variable could not be found.
*/
@Nullable
public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
@Nonnull String variableKey,
@Nonnull String userId) {
return getFeatureVariableJSON(featureKey, variableKey, userId, Collections.<String, String>emptyMap());
}

/**
* Get the JSON value of the specified variable in the feature.
*
* @param featureKey The unique key of the feature.
* @param variableKey The unique key of the variable.
* @param userId The ID of the user.
* @param attributes The user's attributes.
* @return An OptimizelyJSON instance for the JSON variable value.
* Null if the feature or variable could not be found.
*/
@Nullable
public OptimizelyJSON getFeatureVariableJSON(@Nonnull String featureKey,
@Nonnull String variableKey,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes) {

return getFeatureVariableValueForType(
featureKey,
variableKey,
userId,
attributes,
FeatureVariable.JSON_TYPE);
}

@VisibleForTesting
<T> T getFeatureVariableValueForType(@Nonnull String featureKey,
@Nonnull String variableKey,
Expand Down Expand Up @@ -671,6 +711,10 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
}

Object convertedValue = convertStringToType(variableValue, variableType);
Object notificationValue = convertedValue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be done in convertStringToType() like the others ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need notification map type different from the return OptimizelyJSON type.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to the comment, convertStringToType() is used only for notification listener. You already have a case there for json type, can you return a map there ?

if (convertedValue instanceof OptimizelyJSON) {
notificationValue = ((OptimizelyJSON) convertedValue).toMap();
}

DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder()
.withUserId(userId)
Expand All @@ -679,7 +723,7 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
.withFeatureEnabled(featureEnabled)
.withVariableKey(variableKey)
.withVariableType(variableType)
.withVariableValue(convertedValue)
.withVariableValue(notificationValue)
.withFeatureDecision(featureDecision)
.build();

Expand Down Expand Up @@ -714,6 +758,8 @@ Object convertStringToType(String variableValue, String type) {
"\" as Integer. " + exception.toString());
}
break;
case FeatureVariable.JSON_TYPE:
return new OptimizelyJSON(variableValue);
default:
return variableValue;
}
Expand All @@ -722,6 +768,103 @@ Object convertStringToType(String variableValue, String type) {
return null;
}

/**
* Get the values of all variables in the feature.
*
* @param featureKey The unique key of the feature.
* @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,
@Nonnull String userId) {
return getAllFeatureVariables(featureKey, userId, Collections.<String, String>emptyMap());
}

/**
* Get the values of all variables in the feature.
*
* @param featureKey The unique key of the feature.
* @param userId The ID of the user.
* @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,
@Nonnull String userId,
@Nonnull Map<String, ?> attributes) {

if (featureKey == null) {
logger.warn("The featureKey parameter must be nonnull.");
return null;
} else if (userId == null) {
logger.warn("The userId parameter must be nonnull.");
return null;
}

ProjectConfig projectConfig = getProjectConfig();
if (projectConfig == null) {
logger.error("Optimizely instance is not valid, failing getAllFeatureVariableValues call. type");
return null;
}

FeatureFlag featureFlag = projectConfig.getFeatureKeyMapping().get(featureKey);
if (featureFlag == null) {
logger.info("No feature flag was found for key \"{}\".", featureKey);
return null;
}

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

if (variation != null) {
if (!variation.getFeatureEnabled()) {
logger.info("Feature \"{}\" for variation \"{}\" was not enabled. " +
"The default value is being returned.", featureKey, featureDecision.variation.getKey());
}

featureEnabled = variation.getFeatureEnabled();
} else {
logger.info("User \"{}\" was not bucketed into any variation for feature flag \"{}\". " +
"The default values are being returned.", userId, featureKey);
}

Map<String, Object> valuesMap = new HashMap<String, Object>();
for (FeatureVariable variable : featureFlag.getVariables()) {
String value = variable.getDefaultValue();
if (featureEnabled) {
FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId());
if (instance != null) {
value = instance.getValue();
}
}

Object convertedValue = convertStringToType(value, variable.getType());
if (convertedValue instanceof OptimizelyJSON) {
convertedValue = ((OptimizelyJSON) convertedValue).toMap();
}

valuesMap.put(variable.getKey(), convertedValue);
}

DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableDecisionNotificationBuilder()
.withUserId(userId)
.withAttributes(copiedAttributes)
.withFeatureKey(featureKey)
.withFeatureEnabled(featureEnabled)
.withVariableValues(valuesMap)
.withFeatureDecision(featureDecision)
.build();

notificationCenter.send(decisionNotification);

return new OptimizelyJSON(valuesMap);
}

/**
* Get the list of features that are enabled for the user.
* TODO revisit this method. Calling this as-is can dramatically increase visitor impression counts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import com.optimizely.ab.OptimizelyRuntimeException;
import com.optimizely.ab.bucketing.FeatureDecision;
import com.optimizely.ab.config.FeatureVariable;
import com.optimizely.ab.config.Variation;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -239,13 +238,16 @@ public static class FeatureVariableDecisionNotificationBuilder {
public static final String VARIABLE_KEY = "variableKey";
public static final String VARIABLE_TYPE = "variableType";
public static final String VARIABLE_VALUE = "variableValue";
public static final String VARIABLE_VALUES = "variableValues";

private NotificationCenter.DecisionNotificationType notificationType;
private String featureKey;
private Boolean featureEnabled;
private FeatureDecision featureDecision;
private String variableKey;
private String variableType;
private Object variableValue;
private Object variableValues;
private String userId;
private Map<String, ?> attributes;
private Map<String, Object> decisionInfo;
Expand Down Expand Up @@ -293,6 +295,11 @@ public FeatureVariableDecisionNotificationBuilder withVariableValue(Object varia
return this;
}

public FeatureVariableDecisionNotificationBuilder withVariableValues(Object variableValues) {
this.variableValues = variableValues;
return this;
}

public DecisionNotification build() {
if (featureKey == null) {
throw new OptimizelyRuntimeException("featureKey not set");
Expand All @@ -302,20 +309,30 @@ public DecisionNotification build() {
throw new OptimizelyRuntimeException("featureEnabled not set");
}

if (variableKey == null) {
throw new OptimizelyRuntimeException("variableKey not set");
}

if (variableType == null) {
throw new OptimizelyRuntimeException("variableType not set");
}

decisionInfo = new HashMap<>();
decisionInfo.put(FEATURE_KEY, featureKey);
decisionInfo.put(FEATURE_ENABLED, featureEnabled);
decisionInfo.put(VARIABLE_KEY, variableKey);
decisionInfo.put(VARIABLE_TYPE, variableType.toString());
decisionInfo.put(VARIABLE_VALUE, variableValue);

if (variableValues != null) {
notificationType = NotificationCenter.DecisionNotificationType.ALL_FEATURE_VARIABLES;
decisionInfo.put(VARIABLE_VALUES, variableValues);
} else {
notificationType = NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE;

if (variableKey == null) {
throw new OptimizelyRuntimeException("variableKey not set");
}

if (variableType == null) {
throw new OptimizelyRuntimeException("variableType not set");
}

decisionInfo.put(VARIABLE_KEY, variableKey);
decisionInfo.put(VARIABLE_TYPE, variableType.toString());
decisionInfo.put(VARIABLE_VALUE, variableValue);
}

SourceInfo sourceInfo = new RolloutSourceInfo();

if (featureDecision != null && FeatureDecision.DecisionSource.FEATURE_TEST.equals(featureDecision.decisionSource)) {
Expand All @@ -327,7 +344,7 @@ public DecisionNotification build() {
decisionInfo.put(SOURCE_INFO, sourceInfo.get());

return new DecisionNotification(
NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(),
notificationType.toString(),
userId,
attributes,
decisionInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public enum DecisionNotificationType {
AB_TEST("ab-test"),
FEATURE("feature"),
FEATURE_TEST("feature-test"),
FEATURE_VARIABLE("feature-variable");
FEATURE_VARIABLE("feature-variable"),
ALL_FEATURE_VARIABLES("all-feature-variables");

private final String key;

Expand Down
Loading