Skip to content

Commit 5cffdd2

Browse files
authored
feat(decide): add a new set of decide apis (#406)
Add a new set of Decide APIs: - define OptimizelyUserContext/OptimizelyDecision/OptimizelyDecideOption - add createUserContext API to Optimizely - add defaultDecideOption to Optimizely constructor and builder - decide/decideAll/decideForKeys/trackEvent
1 parent db5b0e0 commit 5cffdd2

31 files changed

+3198
-468
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 219 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
3535
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigManager;
3636
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
37+
import com.optimizely.ab.optimizelydecision.DecisionMessage;
38+
import com.optimizely.ab.optimizelydecision.DecisionReasons;
39+
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
40+
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
41+
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
3742
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
3843
import org.slf4j.Logger;
3944
import org.slf4j.LoggerFactory;
@@ -76,7 +81,6 @@ public class Optimizely implements AutoCloseable {
7681

7782
private static final Logger logger = LoggerFactory.getLogger(Optimizely.class);
7883

79-
@VisibleForTesting
8084
final DecisionService decisionService;
8185
@VisibleForTesting
8286
@Deprecated
@@ -86,6 +90,8 @@ public class Optimizely implements AutoCloseable {
8690
@VisibleForTesting
8791
final ErrorHandler errorHandler;
8892

93+
public final List<OptimizelyDecideOption> defaultDecideOptions;
94+
8995
private final ProjectConfigManager projectConfigManager;
9096

9197
@Nullable
@@ -104,7 +110,8 @@ private Optimizely(@Nonnull EventHandler eventHandler,
104110
@Nullable UserProfileService userProfileService,
105111
@Nonnull ProjectConfigManager projectConfigManager,
106112
@Nullable OptimizelyConfigManager optimizelyConfigManager,
107-
@Nonnull NotificationCenter notificationCenter
113+
@Nonnull NotificationCenter notificationCenter,
114+
@Nonnull List<OptimizelyDecideOption> defaultDecideOptions
108115
) {
109116
this.eventHandler = eventHandler;
110117
this.eventProcessor = eventProcessor;
@@ -114,6 +121,7 @@ private Optimizely(@Nonnull EventHandler eventHandler,
114121
this.projectConfigManager = projectConfigManager;
115122
this.optimizelyConfigManager = optimizelyConfigManager;
116123
this.notificationCenter = notificationCenter;
124+
this.defaultDecideOptions = defaultDecideOptions;
117125
}
118126

119127
/**
@@ -779,7 +787,6 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
779787
}
780788

781789
// Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value
782-
@VisibleForTesting
783790
Object convertStringToType(String variableValue, String type) {
784791
if (variableValue != null) {
785792
switch (type) {
@@ -1129,6 +1136,202 @@ public OptimizelyConfig getOptimizelyConfig() {
11291136
return new OptimizelyConfigService(projectConfig).getConfig();
11301137
}
11311138

1139+
//============ decide ============//
1140+
1141+
/**
1142+
* Create a context of the user for which decision APIs will be called.
1143+
*
1144+
* A user context will be created successfully even when the SDK is not fully configured yet.
1145+
*
1146+
* @param userId The user ID to be used for bucketing.
1147+
* @param attributes: A map of attribute names to current user attribute values.
1148+
* @return An OptimizelyUserContext associated with this OptimizelyClient.
1149+
*/
1150+
public OptimizelyUserContext createUserContext(@Nonnull String userId,
1151+
@Nonnull Map<String, Object> attributes) {
1152+
if (userId == null) {
1153+
logger.warn("The userId parameter must be nonnull.");
1154+
return null;
1155+
}
1156+
1157+
return new OptimizelyUserContext(this, userId, attributes);
1158+
}
1159+
1160+
public OptimizelyUserContext createUserContext(@Nonnull String userId) {
1161+
return new OptimizelyUserContext(this, userId);
1162+
}
1163+
1164+
OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
1165+
@Nonnull String key,
1166+
@Nonnull List<OptimizelyDecideOption> options) {
1167+
1168+
ProjectConfig projectConfig = getProjectConfig();
1169+
if (projectConfig == null) {
1170+
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.SDK_NOT_READY.reason());
1171+
}
1172+
1173+
FeatureFlag flag = projectConfig.getFeatureKeyMapping().get(key);
1174+
if (flag == null) {
1175+
return OptimizelyDecision.newErrorDecision(key, user, DecisionMessage.FLAG_KEY_INVALID.reason(key));
1176+
}
1177+
1178+
String userId = user.getUserId();
1179+
Map<String, Object> attributes = user.getAttributes();
1180+
Boolean decisionEventDispatched = false;
1181+
List<OptimizelyDecideOption> allOptions = getAllOptions(options);
1182+
DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
1183+
1184+
Map<String, ?> copiedAttributes = new HashMap<>(attributes);
1185+
FeatureDecision flagDecision = decisionService.getVariationForFeature(
1186+
flag,
1187+
userId,
1188+
copiedAttributes,
1189+
projectConfig,
1190+
allOptions,
1191+
decisionReasons);
1192+
1193+
Boolean flagEnabled = false;
1194+
if (flagDecision.variation != null) {
1195+
if (flagDecision.variation.getFeatureEnabled()) {
1196+
flagEnabled = true;
1197+
}
1198+
}
1199+
logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled);
1200+
1201+
Map<String, Object> variableMap = new HashMap<>();
1202+
if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
1203+
variableMap = getDecisionVariableMap(
1204+
flag,
1205+
flagDecision.variation,
1206+
flagEnabled,
1207+
decisionReasons);
1208+
}
1209+
OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap);
1210+
1211+
FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
1212+
if (flagDecision.decisionSource != null) {
1213+
decisionSource = flagDecision.decisionSource;
1214+
}
1215+
1216+
List<String> reasonsToReport = decisionReasons.toReport();
1217+
String variationKey = flagDecision.variation != null ? flagDecision.variation.getKey() : null;
1218+
// TODO: add ruleKey values when available later. use a copy of experimentKey until then.
1219+
// add to event metadata as well (currently set to experimentKey)
1220+
String ruleKey = flagDecision.experiment != null ? flagDecision.experiment.getKey() : null;
1221+
1222+
if (!allOptions.contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) {
1223+
sendImpression(
1224+
projectConfig,
1225+
flagDecision.experiment,
1226+
userId,
1227+
copiedAttributes,
1228+
flagDecision.variation,
1229+
key,
1230+
decisionSource.toString(),
1231+
flagEnabled);
1232+
decisionEventDispatched = true;
1233+
}
1234+
1235+
DecisionNotification decisionNotification = DecisionNotification.newFlagDecisionNotificationBuilder()
1236+
.withUserId(userId)
1237+
.withAttributes(copiedAttributes)
1238+
.withFlagKey(key)
1239+
.withEnabled(flagEnabled)
1240+
.withVariables(variableMap)
1241+
.withVariationKey(variationKey)
1242+
.withRuleKey(ruleKey)
1243+
.withReasons(reasonsToReport)
1244+
.withDecisionEventDispatched(decisionEventDispatched)
1245+
.build();
1246+
notificationCenter.send(decisionNotification);
1247+
1248+
return new OptimizelyDecision(
1249+
variationKey,
1250+
flagEnabled,
1251+
optimizelyJSON,
1252+
ruleKey,
1253+
key,
1254+
user,
1255+
reasonsToReport);
1256+
}
1257+
1258+
Map<String, OptimizelyDecision> decideForKeys(@Nonnull OptimizelyUserContext user,
1259+
@Nonnull List<String> keys,
1260+
@Nonnull List<OptimizelyDecideOption> options) {
1261+
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
1262+
1263+
ProjectConfig projectConfig = getProjectConfig();
1264+
if (projectConfig == null) {
1265+
logger.error("Optimizely instance is not valid, failing isFeatureEnabled call.");
1266+
return decisionMap;
1267+
}
1268+
1269+
if (keys.isEmpty()) return decisionMap;
1270+
1271+
List<OptimizelyDecideOption> allOptions = getAllOptions(options);
1272+
1273+
for (String key : keys) {
1274+
OptimizelyDecision decision = decide(user, key, options);
1275+
if (!allOptions.contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.getEnabled()) {
1276+
decisionMap.put(key, decision);
1277+
}
1278+
}
1279+
1280+
return decisionMap;
1281+
}
1282+
1283+
Map<String, OptimizelyDecision> decideAll(@Nonnull OptimizelyUserContext user,
1284+
@Nonnull List<OptimizelyDecideOption> options) {
1285+
Map<String, OptimizelyDecision> decisionMap = new HashMap<>();
1286+
1287+
ProjectConfig projectConfig = getProjectConfig();
1288+
if (projectConfig == null) {
1289+
logger.error("Optimizely instance is not valid, failing isFeatureEnabled call.");
1290+
return decisionMap;
1291+
}
1292+
1293+
List<FeatureFlag> allFlags = projectConfig.getFeatureFlags();
1294+
List<String> allFlagKeys = new ArrayList<>();
1295+
for (int i = 0; i < allFlags.size(); i++) allFlagKeys.add(allFlags.get(i).getKey());
1296+
1297+
return decideForKeys(user, allFlagKeys, options);
1298+
}
1299+
1300+
private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption> options) {
1301+
List<OptimizelyDecideOption> copiedOptions = new ArrayList(defaultDecideOptions);
1302+
if (options != null) {
1303+
copiedOptions.addAll(options);
1304+
}
1305+
return copiedOptions;
1306+
}
1307+
1308+
private Map<String, Object> getDecisionVariableMap(@Nonnull FeatureFlag flag,
1309+
@Nonnull Variation variation,
1310+
@Nonnull Boolean featureEnabled,
1311+
@Nonnull DecisionReasons decisionReasons) {
1312+
Map<String, Object> valuesMap = new HashMap<String, Object>();
1313+
for (FeatureVariable variable : flag.getVariables()) {
1314+
String value = variable.getDefaultValue();
1315+
if (featureEnabled) {
1316+
FeatureVariableUsageInstance instance = variation.getVariableIdToFeatureVariableUsageInstanceMap().get(variable.getId());
1317+
if (instance != null) {
1318+
value = instance.getValue();
1319+
}
1320+
}
1321+
1322+
Object convertedValue = convertStringToType(value, variable.getType());
1323+
if (convertedValue == null) {
1324+
decisionReasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey()));
1325+
} else if (convertedValue instanceof OptimizelyJSON) {
1326+
convertedValue = ((OptimizelyJSON) convertedValue).toMap();
1327+
}
1328+
1329+
valuesMap.put(variable.getKey(), convertedValue);
1330+
}
1331+
1332+
return valuesMap;
1333+
}
1334+
11321335
/**
11331336
* Helper method which makes separate copy of attributesMap variable and returns it
11341337
*
@@ -1233,6 +1436,7 @@ public static class Builder {
12331436
private OptimizelyConfigManager optimizelyConfigManager;
12341437
private UserProfileService userProfileService;
12351438
private NotificationCenter notificationCenter;
1439+
private List<OptimizelyDecideOption> defaultDecideOptions;
12361440

12371441
// For backwards compatibility
12381442
private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager();
@@ -1304,6 +1508,11 @@ public Builder withDatafile(String datafile) {
13041508
return this;
13051509
}
13061510

1511+
public Builder withDefaultDecideOptions(List<OptimizelyDecideOption> defaultDecideOtions) {
1512+
this.defaultDecideOptions = defaultDecideOtions;
1513+
return this;
1514+
}
1515+
13071516
// Helper functions for making testing easier
13081517
protected Builder withBucketing(Bucketer bucketer) {
13091518
this.bucketer = bucketer;
@@ -1372,7 +1581,13 @@ public Optimizely build() {
13721581
eventProcessor = new ForwardingEventProcessor(eventHandler, notificationCenter);
13731582
}
13741583

1375-
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter);
1584+
if (defaultDecideOptions != null) {
1585+
defaultDecideOptions = Collections.unmodifiableList(defaultDecideOptions);
1586+
} else {
1587+
defaultDecideOptions = Collections.emptyList();
1588+
}
1589+
1590+
return new Optimizely(eventHandler, eventProcessor, errorHandler, decisionService, userProfileService, projectConfigManager, optimizelyConfigManager, notificationCenter, defaultDecideOptions);
13761591
}
13771592
}
13781593
}

0 commit comments

Comments
 (0)