Skip to content

Commit 5bb2588

Browse files
authored
fix(decide): change to return reasons as a part of tuple in decision hierarchy (#415)
1 parent 651ccdc commit 5bb2588

23 files changed

+837
-891
lines changed

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

+23-23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2016-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2016-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -34,11 +34,7 @@
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;
37+
import com.optimizely.ab.optimizelydecision.*;
4238
import com.optimizely.ab.optimizelyjson.OptimizelyJSON;
4339
import org.slf4j.Logger;
4440
import org.slf4j.LoggerFactory;
@@ -427,7 +423,7 @@ private Boolean isFeatureEnabled(@Nonnull ProjectConfig projectConfig,
427423

428424
Map<String, ?> copiedAttributes = copyAttributes(attributes);
429425
FeatureDecision.DecisionSource decisionSource = FeatureDecision.DecisionSource.ROLLOUT;
430-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
426+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
431427
Boolean featureEnabled = false;
432428
SourceInfo sourceInfo = new RolloutSourceInfo();
433429
if (featureDecision.decisionSource != null) {
@@ -736,7 +732,7 @@ <T> T getFeatureVariableValueForType(@Nonnull String featureKey,
736732

737733
String variableValue = variable.getDefaultValue();
738734
Map<String, ?> copiedAttributes = copyAttributes(attributes);
739-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
735+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
740736
Boolean featureEnabled = false;
741737
if (featureDecision.variation != null) {
742738
if (featureDecision.variation.getFeatureEnabled()) {
@@ -869,7 +865,7 @@ public OptimizelyJSON getAllFeatureVariables(@Nonnull String featureKey,
869865
}
870866

871867
Map<String, ?> copiedAttributes = copyAttributes(attributes);
872-
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig);
868+
FeatureDecision featureDecision = decisionService.getVariationForFeature(featureFlag, userId, copiedAttributes, projectConfig).getResult();
873869
Boolean featureEnabled = false;
874870
Variation variation = featureDecision.variation;
875871

@@ -970,7 +966,7 @@ private Variation getVariation(@Nonnull ProjectConfig projectConfig,
970966
@Nonnull String userId,
971967
@Nonnull Map<String, ?> attributes) throws UnknownExperimentException {
972968
Map<String, ?> copiedAttributes = copyAttributes(attributes);
973-
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes, projectConfig);
969+
Variation variation = decisionService.getVariation(experiment, userId, copiedAttributes, projectConfig).getResult();
974970

975971
String notificationType = NotificationCenter.DecisionNotificationType.AB_TEST.toString();
976972

@@ -1084,7 +1080,7 @@ public Variation getForcedVariation(@Nonnull String experimentKey,
10841080
return null;
10851081
}
10861082

1087-
return decisionService.getForcedVariation(experiment, userId);
1083+
return decisionService.getForcedVariation(experiment, userId).getResult();
10881084
}
10891085

10901086
/**
@@ -1182,13 +1178,14 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
11821178
DecisionReasons decisionReasons = DefaultDecisionReasons.newInstance(allOptions);
11831179

11841180
Map<String, ?> copiedAttributes = new HashMap<>(attributes);
1185-
FeatureDecision flagDecision = decisionService.getVariationForFeature(
1181+
DecisionResponse<FeatureDecision> decisionVariation = decisionService.getVariationForFeature(
11861182
flag,
11871183
userId,
11881184
copiedAttributes,
11891185
projectConfig,
1190-
allOptions,
1191-
decisionReasons);
1186+
allOptions);
1187+
FeatureDecision flagDecision = decisionVariation.getResult();
1188+
decisionReasons.merge(decisionVariation.getReasons());
11921189

11931190
Boolean flagEnabled = false;
11941191
if (flagDecision.variation != null) {
@@ -1200,11 +1197,12 @@ OptimizelyDecision decide(@Nonnull OptimizelyUserContext user,
12001197

12011198
Map<String, Object> variableMap = new HashMap<>();
12021199
if (!allOptions.contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) {
1203-
variableMap = getDecisionVariableMap(
1200+
DecisionResponse<Map<String, Object>> decisionVariables = getDecisionVariableMap(
12041201
flag,
12051202
flagDecision.variation,
1206-
flagEnabled,
1207-
decisionReasons);
1203+
flagEnabled);
1204+
variableMap = decisionVariables.getResult();
1205+
decisionReasons.merge(decisionVariables.getReasons());
12081206
}
12091207
OptimizelyJSON optimizelyJSON = new OptimizelyJSON(variableMap);
12101208

@@ -1305,10 +1303,12 @@ private List<OptimizelyDecideOption> getAllOptions(List<OptimizelyDecideOption>
13051303
return copiedOptions;
13061304
}
13071305

1308-
private Map<String, Object> getDecisionVariableMap(@Nonnull FeatureFlag flag,
1309-
@Nonnull Variation variation,
1310-
@Nonnull Boolean featureEnabled,
1311-
@Nonnull DecisionReasons decisionReasons) {
1306+
@Nonnull
1307+
private DecisionResponse<Map<String, Object>> getDecisionVariableMap(@Nonnull FeatureFlag flag,
1308+
@Nonnull Variation variation,
1309+
@Nonnull Boolean featureEnabled) {
1310+
DecisionReasons reasons = new DecisionReasons();
1311+
13121312
Map<String, Object> valuesMap = new HashMap<String, Object>();
13131313
for (FeatureVariable variable : flag.getVariables()) {
13141314
String value = variable.getDefaultValue();
@@ -1321,15 +1321,15 @@ private Map<String, Object> getDecisionVariableMap(@Nonnull FeatureFlag flag,
13211321

13221322
Object convertedValue = convertStringToType(value, variable.getType());
13231323
if (convertedValue == null) {
1324-
decisionReasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey()));
1324+
reasons.addError(DecisionMessage.VARIABLE_VALUE_INVALID.reason(variable.getKey()));
13251325
} else if (convertedValue instanceof OptimizelyJSON) {
13261326
convertedValue = ((OptimizelyJSON) convertedValue).toMap();
13271327
}
13281328

13291329
valuesMap.put(variable.getKey(), convertedValue);
13301330
}
13311331

1332-
return valuesMap;
1332+
return new DecisionResponse(valuesMap, reasons);
13331333
}
13341334

13351335
/**

core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java

+23-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
*
3-
* Copyright 2016-2017, 2019, Optimizely and contributors
3+
* Copyright 2016-2017, 2019-2021 Optimizely and contributors
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -20,12 +20,12 @@
2020
import com.optimizely.ab.bucketing.internal.MurmurHash3;
2121
import com.optimizely.ab.config.*;
2222
import com.optimizely.ab.optimizelydecision.DecisionReasons;
23+
import com.optimizely.ab.optimizelydecision.DecisionResponse;
2324
import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
2627

2728
import javax.annotation.Nonnull;
28-
import javax.annotation.Nullable;
2929
import javax.annotation.concurrent.Immutable;
3030
import java.util.List;
3131

@@ -69,8 +69,7 @@ private String bucketToEntity(int bucketValue, List<TrafficAllocation> trafficAl
6969

7070
private Experiment bucketToExperiment(@Nonnull Group group,
7171
@Nonnull String bucketingId,
72-
@Nonnull ProjectConfig projectConfig,
73-
@Nonnull DecisionReasons reasons) {
72+
@Nonnull ProjectConfig projectConfig) {
7473
// "salt" the bucket id using the group id
7574
String bucketKey = bucketingId + group.getId();
7675

@@ -89,9 +88,11 @@ private Experiment bucketToExperiment(@Nonnull Group group,
8988
return null;
9089
}
9190

92-
private Variation bucketToVariation(@Nonnull Experiment experiment,
93-
@Nonnull String bucketingId,
94-
@Nonnull DecisionReasons reasons) {
91+
@Nonnull
92+
private DecisionResponse<Variation> bucketToVariation(@Nonnull Experiment experiment,
93+
@Nonnull String bucketingId) {
94+
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
95+
9596
// "salt" the bucket id using the experiment id
9697
String experimentId = experiment.getId();
9798
String experimentKey = experiment.getKey();
@@ -111,13 +112,13 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
111112
experimentKey);
112113
logger.info(message);
113114

114-
return bucketedVariation;
115+
return new DecisionResponse(bucketedVariation, reasons);
115116
}
116117

117118
// user was not bucketed to a variation
118119
String message = reasons.addInfo("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey);
119120
logger.info(message);
120-
return null;
121+
return new DecisionResponse(null, reasons);
121122
}
122123

123124
/**
@@ -126,26 +127,26 @@ private Variation bucketToVariation(@Nonnull Experiment experiment,
126127
* @param experiment The Experiment in which the user is to be bucketed.
127128
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash.
128129
* @param projectConfig The current projectConfig
129-
* @param reasons Decision log messages
130-
* @return Variation the user is bucketed into or null.
130+
* @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons
131131
*/
132-
@Nullable
133-
public Variation bucket(@Nonnull Experiment experiment,
134-
@Nonnull String bucketingId,
135-
@Nonnull ProjectConfig projectConfig,
136-
@Nonnull DecisionReasons reasons) {
132+
@Nonnull
133+
public DecisionResponse<Variation> bucket(@Nonnull Experiment experiment,
134+
@Nonnull String bucketingId,
135+
@Nonnull ProjectConfig projectConfig) {
136+
DecisionReasons reasons = DefaultDecisionReasons.newInstance();
137+
137138
// ---------- Bucket User ----------
138139
String groupId = experiment.getGroupId();
139140
// check whether the experiment belongs to a group
140141
if (!groupId.isEmpty()) {
141142
Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId);
142143
// bucket to an experiment only if group entities are to be mutually exclusive
143144
if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) {
144-
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, reasons);
145+
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig);
145146
if (bucketedExperiment == null) {
146147
String message = reasons.addInfo("User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId());
147148
logger.info(message);
148-
return null;
149+
return new DecisionResponse(null, reasons);
149150
} else {
150151

151152
}
@@ -155,7 +156,7 @@ public Variation bucket(@Nonnull Experiment experiment,
155156
String message = reasons.addInfo("User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(),
156157
experimentGroup.getId());
157158
logger.info(message);
158-
return null;
159+
return new DecisionResponse(null, reasons);
159160
}
160161

161162
String message = reasons.addInfo("User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(),
@@ -164,14 +165,9 @@ public Variation bucket(@Nonnull Experiment experiment,
164165
}
165166
}
166167

167-
return bucketToVariation(experiment, bucketingId, reasons);
168-
}
169-
170-
@Nullable
171-
public Variation bucket(@Nonnull Experiment experiment,
172-
@Nonnull String bucketingId,
173-
@Nonnull ProjectConfig projectConfig) {
174-
return bucket(experiment, bucketingId, projectConfig, DefaultDecisionReasons.newInstance());
168+
DecisionResponse<Variation> decisionResponse = bucketToVariation(experiment, bucketingId);
169+
reasons.merge(decisionResponse.getReasons());
170+
return new DecisionResponse<>(decisionResponse.getResult(), reasons);
175171
}
176172

177173
//======== Helper methods ========//

0 commit comments

Comments
 (0)