34
34
import com .optimizely .ab .optimizelyconfig .OptimizelyConfig ;
35
35
import com .optimizely .ab .optimizelyconfig .OptimizelyConfigManager ;
36
36
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 ;
37
42
import com .optimizely .ab .optimizelyjson .OptimizelyJSON ;
38
43
import org .slf4j .Logger ;
39
44
import org .slf4j .LoggerFactory ;
@@ -76,7 +81,6 @@ public class Optimizely implements AutoCloseable {
76
81
77
82
private static final Logger logger = LoggerFactory .getLogger (Optimizely .class );
78
83
79
- @ VisibleForTesting
80
84
final DecisionService decisionService ;
81
85
@ VisibleForTesting
82
86
@ Deprecated
@@ -86,6 +90,8 @@ public class Optimizely implements AutoCloseable {
86
90
@ VisibleForTesting
87
91
final ErrorHandler errorHandler ;
88
92
93
+ public final List <OptimizelyDecideOption > defaultDecideOptions ;
94
+
89
95
private final ProjectConfigManager projectConfigManager ;
90
96
91
97
@ Nullable
@@ -104,7 +110,8 @@ private Optimizely(@Nonnull EventHandler eventHandler,
104
110
@ Nullable UserProfileService userProfileService ,
105
111
@ Nonnull ProjectConfigManager projectConfigManager ,
106
112
@ Nullable OptimizelyConfigManager optimizelyConfigManager ,
107
- @ Nonnull NotificationCenter notificationCenter
113
+ @ Nonnull NotificationCenter notificationCenter ,
114
+ @ Nonnull List <OptimizelyDecideOption > defaultDecideOptions
108
115
) {
109
116
this .eventHandler = eventHandler ;
110
117
this .eventProcessor = eventProcessor ;
@@ -114,6 +121,7 @@ private Optimizely(@Nonnull EventHandler eventHandler,
114
121
this .projectConfigManager = projectConfigManager ;
115
122
this .optimizelyConfigManager = optimizelyConfigManager ;
116
123
this .notificationCenter = notificationCenter ;
124
+ this .defaultDecideOptions = defaultDecideOptions ;
117
125
}
118
126
119
127
/**
@@ -779,7 +787,6 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
779
787
}
780
788
781
789
// Helper method which takes type and variable value and convert it to object to use in Listener DecisionInfo object variable value
782
- @ VisibleForTesting
783
790
Object convertStringToType (String variableValue , String type ) {
784
791
if (variableValue != null ) {
785
792
switch (type ) {
@@ -1129,6 +1136,202 @@ public OptimizelyConfig getOptimizelyConfig() {
1129
1136
return new OptimizelyConfigService (projectConfig ).getConfig ();
1130
1137
}
1131
1138
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
+
1132
1335
/**
1133
1336
* Helper method which makes separate copy of attributesMap variable and returns it
1134
1337
*
@@ -1233,6 +1436,7 @@ public static class Builder {
1233
1436
private OptimizelyConfigManager optimizelyConfigManager ;
1234
1437
private UserProfileService userProfileService ;
1235
1438
private NotificationCenter notificationCenter ;
1439
+ private List <OptimizelyDecideOption > defaultDecideOptions ;
1236
1440
1237
1441
// For backwards compatibility
1238
1442
private AtomicProjectConfigManager fallbackConfigManager = new AtomicProjectConfigManager ();
@@ -1304,6 +1508,11 @@ public Builder withDatafile(String datafile) {
1304
1508
return this ;
1305
1509
}
1306
1510
1511
+ public Builder withDefaultDecideOptions (List <OptimizelyDecideOption > defaultDecideOtions ) {
1512
+ this .defaultDecideOptions = defaultDecideOtions ;
1513
+ return this ;
1514
+ }
1515
+
1307
1516
// Helper functions for making testing easier
1308
1517
protected Builder withBucketing (Bucketer bucketer ) {
1309
1518
this .bucketer = bucketer ;
@@ -1372,7 +1581,13 @@ public Optimizely build() {
1372
1581
eventProcessor = new ForwardingEventProcessor (eventHandler , notificationCenter );
1373
1582
}
1374
1583
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 );
1376
1591
}
1377
1592
}
1378
1593
}
0 commit comments