Skip to content

Commit cdaa2e0

Browse files
Addition of ForcedDecisions
1 parent 674cade commit cdaa2e0

File tree

9 files changed

+633
-195
lines changed

9 files changed

+633
-195
lines changed

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ public void track(@Nonnull String eventName,
376376
@Nonnull
377377
public Boolean isFeatureEnabled(@Nonnull String featureKey,
378378
@Nonnull String userId) {
379-
return isFeatureEnabled(featureKey, userId, Collections.<String, String>emptyMap());
379+
return isFeatureEnabled(featureKey, userId, Collections.emptyMap());
380380
}
381381

382382
/**
@@ -422,9 +422,9 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
422422
return false;
423423
}
424424

425-
Map<String, ?> copiedAttributes = copyAttributes(attributes);
425+
Map<String, Object> copiedAttributes = copyAttributes(attributes);
426426
FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
427-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
427+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
428428
Boolean featureEnabled = false;
429429
SourceInfo sourceInfo = new RolloutSourceInfo();
430430
if (featureDecision.decisionSource != null) {
@@ -732,8 +732,8 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
732732
}
733733

734734
String variableValue = variable.getDefaultValue();
735-
Map<String, ?> copiedAttributes = copyAttributes(attributes);
736-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
735+
Map<String, Object> copiedAttributes = copyAttributes(attributes);
736+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig).getResult();
737737
Boolean featureEnabled = false;
738738
if (featureDecision.variation != null) {
739739
if (featureDecision.variation.getFeatureEnabled()) {
@@ -865,8 +865,8 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
865865
return null;
866866
}
867867

868-
Map<String, ?> copiedAttributes = copyAttributes(attributes);
869-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
868+
Map<String, Object> copiedAttributes = copyAttributes(attributes);
869+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, createUserContext(userId, copiedAttributes), projectConfig, Collections.emptyList()).getResult();
870870
Boolean featureEnabled = false;
871871
Variation variation = featureDecision.variation;
872872

@@ -924,7 +924,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
924924
* return Empty List.
925925
*/
926926
public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<String, ?> attributes) {
927-
List<String> enabledFeaturesList = new ArrayList<String>();
927+
List<String> enabledFeaturesList = new ArrayList();
928928
if (!validateUserId(userId)) {
929929
return enabledFeaturesList;
930930
}
@@ -935,7 +935,7 @@ public List<String> getEnabledFeatures(@Nonnull String userId, @Nonnull Map<Stri
935935
return enabledFeaturesList;
936936
}
937937

938-
Map<String, ?> copiedAttributes = copyAttributes(attributes);
938+
Map<String, Object> copiedAttributes = copyAttributes(attributes);
939939
for (FeatureFlag featureFlag : projectConfig.getFeatureFlags()) {
940940
String featureKey = featureFlag.getKey();
941941
if (isFeatureEnabled(projectConfig, featureKey, userId, copiedAttributes))
@@ -966,9 +966,8 @@ private Variation getVariation(@Nonnull ProjectConfig projectConfig,
966966
@Nonnull Experiment experiment,
967967
@Nonnull String userId,
968968
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
969-
Map<String, ?> copiedAttributes = copyAttributes(attributes);
970-
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes, projectConfig).getResult();
971-
969+
Map<String, Object> copiedAttributes = copyAttributes(attributes);
970+
Variation variation = decisionService.getVariation(experiment, createUserContext(userId, copiedAttributes), projectConfig).getResult();
972971
String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();
973972

974973
if (projectConfig.getExperimentFeatureKeyMapping().get(experiment.getId()) != null) {
@@ -1034,7 +1033,10 @@ public Variation getVariation(@Nonnull String experimentKey,
10341033
* @param variationKey The variation key to force the user into. If the variation key is null
10351034
* then the forcedVariation for that experiment is removed.
10361035
* @return boolean A boolean value that indicates if the set completed successfully.
1036+
*
1037+
* @deprecated use {@link OptimizelyUserContext#setForcedDecision(String, String, String)} instead
10371038
*/
1039+
@Deprecated
10381040
public boolean setForcedVariation(@Nonnull String experimentKey,
10391041
@Nonnull String userId,
10401042
@Nullable String variationKey) {
@@ -1065,7 +1067,10 @@ public boolean setForcedVariation(@Nonnull String experimentKey,
10651067
* @param userId The user ID to be used for bucketing.
10661068
* @return The variation the user was bucketed into. This value can be null if the
10671069
* forced variation fails.
1070+
*
1071+
* @deprecated use {@link OptimizelyUserContext#getForcedDecision(String, String)} instead
10681072
*/
1073+
@Deprecated
10691074
@Nullable
10701075
public Variation getForcedVariation(@Nonnull String experimentKey,
10711076
@Nonnull String userId) {
@@ -1181,8 +1186,7 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
11811186
Map<String, ?> copiedAttributes = new HashMap<>(attributes);
11821187
DecisionResponse<FeatureDecision> decisionVariation = decisionService.getVariationForFeature(
11831188
flag,
1184-
userId,
1185-
copiedAttributes,
1189+
user,
11861190
projectConfig,
11871191
allOptions);
11881192
FeatureDecision flagDecision = decisionVariation.getResult();
@@ -1332,14 +1336,32 @@ private DecisionResponse<Map<String, Object>> getDecisionVariableMap(@Nonnull Fe
13321336
return new DecisionResponse(valuesMap, reasons);
13331337
}
13341338

1339+
/**
1340+
* Gets a variation based on flagKey and variationKey
1341+
*
1342+
* @param flagKey The flag key for the variation
1343+
* @param variationKey The variation key for the variation
1344+
* @return Returns a variation based on flagKey and variationKey, otherwise null
1345+
*/
1346+
public Variation getFlagVariationByKey(String flagKey, String variationKey) {
1347+
Map<String, List<Variation>> flagVariationsMap = getProjectConfig().getFlagVariationsMap();
1348+
List<Variation> variations = flagVariationsMap.get(flagKey);
1349+
for (Variation variation : variations) {
1350+
if (variation.getKey().equals(variationKey)) {
1351+
return variation;
1352+
}
1353+
}
1354+
return null;
1355+
}
1356+
13351357
/**
13361358
* Helper method which makes separate copy of attributesMap variable and returns it
13371359
*
13381360
* @param attributes map to copy
13391361
* @return copy of attributes
13401362
*/
1341-
private Map<String, ?> copyAttributes(Map<String, ?> attributes) {
1342-
Map<String, ?> copiedAttributes = null;
1363+
private Map<String, Object> copyAttributes(Map<String, ?> attributes) {
1364+
Map<String, Object> copiedAttributes = null;
13431365
if (attributes != null) {
13441366
copiedAttributes = new HashMap<>(attributes);
13451367
}

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

Lines changed: 223 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,36 @@
1616
*/
1717
package com.optimizely.ab;
1818

19-
import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption;
20-
import com.optimizely.ab.optimizelydecision.OptimizelyDecision;
19+
import com.optimizely.ab.config.Variation;
20+
import com.optimizely.ab.optimizelydecision.*;
2121
import org.slf4j.Logger;
2222
import org.slf4j.LoggerFactory;
2323

2424
import javax.annotation.Nonnull;
2525
import javax.annotation.Nullable;
26-
import java.util.Collections;
27-
import java.util.HashMap;
28-
import java.util.List;
29-
import java.util.Map;
26+
import java.util.*;
3027

3128
public class OptimizelyUserContext {
29+
static class ForcedDecision {
30+
private String flagKey;
31+
private String ruleKey;
32+
private String variationKey;
33+
34+
ForcedDecision(@Nonnull String flagKey, String ruleKey, @Nonnull String variationKey) {
35+
this.flagKey = flagKey;
36+
this.ruleKey = ruleKey;
37+
this.variationKey = variationKey;
38+
}
39+
40+
public String getFlagKey() { return flagKey; }
41+
public String getRuleKey() { return ruleKey; }
42+
public String getVariationKey() { return variationKey; }
43+
}
44+
45+
// flagKeys mapped to ruleKeys mapped to forcedDecisions
46+
Map<String, Map<String, ForcedDecision>> forcedDecisionsMap = new HashMap<>();
47+
Map<String, ForcedDecision> forcedDecisionsMapWithNoRuleKey = new HashMap<>();
48+
3249
@Nonnull
3350
private final String userId;
3451

@@ -172,6 +189,206 @@ public void trackEvent(@Nonnull String eventName) throws UnknownEventTypeExcepti
172189
trackEvent(eventName, Collections.emptyMap());
173190
}
174191

192+
/**
193+
*
194+
* @param flagKey The flag key for the forced decision
195+
* @param variationKey The variation key for the forced decision
196+
* @return Returns a boolean, True if successfully set, otherwise false
197+
*/
198+
public Boolean setForcedDecision(@Nonnull String flagKey, @Nonnull String variationKey) {
199+
return setForcedDecision(flagKey, null, variationKey);
200+
}
201+
202+
/**
203+
* Set a forced decision
204+
*
205+
* @param flagKey The flag key for the forced decision
206+
* @param ruleKey The rule key for the forced decision
207+
* @param variationKey The variation key for the forced decision
208+
* @return Returns a boolean, Ture if successfully set, otherwise false
209+
*/
210+
public Boolean setForcedDecision(@Nonnull String flagKey, String ruleKey, @Nonnull String variationKey) {
211+
if (optimizely.getOptimizelyConfig() == null) {
212+
logger.error("Optimizely SDK not ready.");
213+
return false;
214+
}
215+
216+
if (ruleKey == null) {
217+
// If the ruleKey is null, we will populate/update the appropriate map
218+
if (forcedDecisionsMapWithNoRuleKey.get(flagKey) != null) {
219+
forcedDecisionsMapWithNoRuleKey.get(flagKey).variationKey = variationKey;
220+
} else {
221+
forcedDecisionsMapWithNoRuleKey.put(flagKey, new ForcedDecision(flagKey, null, variationKey));
222+
}
223+
} else {
224+
// If the flagKey and ruleKey are already present, set the updated variationKey
225+
if (forcedDecisionsMap.containsKey(flagKey)) {
226+
if (forcedDecisionsMap.get(flagKey).containsKey(ruleKey)) {
227+
forcedDecisionsMap.get(flagKey).get(ruleKey).variationKey = variationKey;
228+
} else {
229+
forcedDecisionsMap.get(flagKey).put(ruleKey, new ForcedDecision(flagKey, ruleKey, variationKey));
230+
}
231+
} else {
232+
Map<String, ForcedDecision> forcedDecision = new HashMap<>();
233+
forcedDecision.put(ruleKey, new ForcedDecision(flagKey, ruleKey, variationKey));
234+
forcedDecisionsMap.put(flagKey, forcedDecision);
235+
}
236+
}
237+
238+
return true;
239+
}
240+
241+
/**
242+
*
243+
* @param flagKey The flag key for the forced decision
244+
* @return Returns a variationKey for a given forced decision
245+
*/
246+
public String getForcedDecision(@Nonnull String flagKey) {
247+
return getForcedDecision(flagKey, null);
248+
}
249+
250+
/**
251+
* Get a forced decision
252+
*
253+
* @param flagKey The flag key for the forced decision
254+
* @param ruleKey The rule key for the forced decision
255+
* @return Returns a variationKey for a given forced decision
256+
*/
257+
public String getForcedDecision(@Nonnull String flagKey, String ruleKey) {
258+
if (optimizely.getOptimizelyConfig() == null) {
259+
logger.error("Optimizely SDK not ready.");
260+
return null;
261+
}
262+
return findForcedDecision(flagKey, ruleKey);
263+
}
264+
265+
/**
266+
* Finds a forced decision
267+
*
268+
* @param flagKey The flag key for the forced decision
269+
* @param ruleKey The rule key for the forced decision
270+
* @return Returns a variationKey relating to the found forced decision, otherwise null
271+
*/
272+
public String findForcedDecision(@Nonnull String flagKey, String ruleKey) {
273+
String variationKey = null;
274+
if (ruleKey != null) {
275+
if (forcedDecisionsMap.size() > 0 && forcedDecisionsMap.containsKey(flagKey)) {
276+
if (forcedDecisionsMap.get(flagKey).containsKey(ruleKey)) {
277+
variationKey = forcedDecisionsMap.get(flagKey).get(ruleKey).getVariationKey();
278+
}
279+
}
280+
} else {
281+
if (forcedDecisionsMapWithNoRuleKey.size() > 0 && forcedDecisionsMapWithNoRuleKey.containsKey(flagKey)) {
282+
variationKey = forcedDecisionsMapWithNoRuleKey.get(flagKey).getVariationKey();
283+
}
284+
}
285+
return variationKey;
286+
}
287+
288+
/**
289+
*
290+
* @param flagKey The flag key in the forced decision
291+
* @return Returns a boolean of true if successful, otherwise false
292+
*/
293+
public boolean removeForcedDecision(@Nonnull String flagKey) {
294+
return removeForcedDecision(flagKey, null);
295+
}
296+
297+
/**
298+
* Remove a forced decision
299+
*
300+
* @param flagKey The flag key for the forced decision
301+
* @param ruleKey The rule key for the forced decision
302+
* @return Returns a boolean, true if successfully removed, otherwise false
303+
*/
304+
public boolean removeForcedDecision(@Nonnull String flagKey, String ruleKey) {
305+
if (optimizely.getOptimizelyConfig() == null) {
306+
logger.error("Optimizely SDK not ready.");
307+
return false;
308+
}
309+
if (ruleKey != null) {
310+
try {
311+
forcedDecisionsMap.get(flagKey).remove(ruleKey);
312+
if (forcedDecisionsMap.get(flagKey).size() == 0) {
313+
forcedDecisionsMap.remove(flagKey);
314+
}
315+
return true;
316+
} catch (Exception e) {
317+
logger.error("Forced Decision does not exist to remove - " + e);
318+
}
319+
} else {
320+
try {
321+
forcedDecisionsMapWithNoRuleKey.remove(flagKey);
322+
return true;
323+
} catch (Exception e) {
324+
logger.error("Forced Decision does not exist to remove - " + e);
325+
}
326+
}
327+
328+
return false;
329+
}
330+
331+
/**
332+
* Remove all forced decisions
333+
*
334+
* @return Returns a boolean, True if successfully, otherwise false
335+
*/
336+
public boolean removeAllForcedDecisions() {
337+
if (optimizely.getProjectConfig() == null) {
338+
logger.error("Optimizely SDK not ready.");
339+
return false;
340+
}
341+
// Clear both maps for with and without ruleKey
342+
forcedDecisionsMap.clear();
343+
forcedDecisionsMapWithNoRuleKey.clear();
344+
return true;
345+
}
346+
347+
/**
348+
* Find a validated forced decision
349+
*
350+
* @param flagKey The flag key for the forced decision
351+
* @return Returns a DecisionResponse structure of type Variation, otherwise null with reasons
352+
*/
353+
public DecisionResponse<Variation> findValidatedForcedDecision(@Nonnull String flagKey) {
354+
return findValidatedForcedDecision(flagKey, null);
355+
}
356+
357+
/**
358+
* Find a validated forced decision
359+
*
360+
* @param flagKey The flag key for a forced decision
361+
* @param ruleKey The rule key for a forced decision
362+
* @return Returns a DecisionResponse structure of type Variation, otherwise null result with reasons
363+
*/
364+
public DecisionResponse<Variation> findValidatedForcedDecision(@Nonnull String flagKey, String ruleKey) {
365+
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
366+
// TODO - Move all info strings to a single class to be called rather than hardcoded in functions
367+
String variationKey = findForcedDecision(flagKey, ruleKey);
368+
if (variationKey != null) {
369+
Variation variation = optimizely.getFlagVariationByKey(flagKey, variationKey);
370+
String strRuleKey = ruleKey != null ? ruleKey : "null";
371+
if (variation != null) {
372+
String info = "Variation " + variationKey
373+
+ " is mapped to flag: " + flagKey
374+
+ " and rule: " + strRuleKey
375+
+ " and user: " + userId
376+
+ " in the forced decision map.";
377+
logger.debug(info);
378+
reasons.addInfo(info);
379+
return new DecisionResponse(variation, reasons);
380+
} else {
381+
String info = "Invalid variation is mapped to flag: " + flagKey
382+
+ " and rule: " + strRuleKey
383+
+ " and user: " + userId
384+
+ " forced decision map.";
385+
logger.debug(info);
386+
reasons.addInfo(info);
387+
}
388+
}
389+
return new DecisionResponse<>(null, reasons);
390+
}
391+
175392
// Utils
176393

177394
@Override

0 commit comments

Comments
 (0)