-
Notifications
You must be signed in to change notification settings - Fork 32
feat(decide): add decide API #407
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
Changes from 8 commits
d476539
8bea738
6800e66
e8745ef
2d426a5
b8a19a7
e3cedab
2c83fa2
bb9f4dc
3223a64
7792926
77e09d5
4bfa1f7
fdbf75a
c0b6c85
a93fc23
f803cd7
43f7b01
7ce768d
5b6e798
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,11 +18,9 @@ | |
|
||
import com.optimizely.ab.annotations.VisibleForTesting; | ||
import com.optimizely.ab.bucketing.internal.MurmurHash3; | ||
import com.optimizely.ab.config.Experiment; | ||
import com.optimizely.ab.config.Group; | ||
import com.optimizely.ab.config.ProjectConfig; | ||
import com.optimizely.ab.config.TrafficAllocation; | ||
import com.optimizely.ab.config.Variation; | ||
import com.optimizely.ab.config.*; | ||
import com.optimizely.ab.optimizelyusercontext.DecisionReasons; | ||
import com.optimizely.ab.optimizelyusercontext.OptimizelyDecideOption; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
|
@@ -71,7 +69,9 @@ private String bucketToEntity(int bucketValue, List<TrafficAllocation> trafficAl | |
|
||
private Experiment bucketToExperiment(@Nonnull Group group, | ||
@Nonnull String bucketingId, | ||
@Nonnull ProjectConfig projectConfig) { | ||
@Nonnull ProjectConfig projectConfig, | ||
@Nullable List<OptimizelyDecideOption> options, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
@Nullable DecisionReasons reasons) { | ||
// "salt" the bucket id using the group id | ||
String bucketKey = bucketingId + group.getId(); | ||
|
||
|
@@ -91,7 +91,9 @@ private Experiment bucketToExperiment(@Nonnull Group group, | |
} | ||
|
||
private Variation bucketToVariation(@Nonnull Experiment experiment, | ||
@Nonnull String bucketingId) { | ||
@Nonnull String bucketingId, | ||
@Nullable List<OptimizelyDecideOption> options, | ||
@Nullable DecisionReasons reasons) { | ||
// "salt" the bucket id using the experiment id | ||
String experimentId = experiment.getId(); | ||
String experimentKey = experiment.getKey(); | ||
|
@@ -107,14 +109,14 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, | |
if (bucketedVariationId != null) { | ||
Variation bucketedVariation = experiment.getVariationIdToVariationMap().get(bucketedVariationId); | ||
String variationKey = bucketedVariation.getKey(); | ||
logger.info("User with bucketingId \"{}\" is in variation \"{}\" of experiment \"{}\".", bucketingId, variationKey, | ||
DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", bucketingId, variationKey, | ||
experimentKey); | ||
|
||
return bucketedVariation; | ||
} | ||
|
||
// user was not bucketed to a variation | ||
logger.info("User with bucketingId \"{}\" is not in any variation of experiment \"{}\".", bucketingId, experimentKey); | ||
DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", bucketingId, experimentKey); | ||
return null; | ||
} | ||
|
||
|
@@ -123,43 +125,77 @@ private Variation bucketToVariation(@Nonnull Experiment experiment, | |
* | ||
* @param experiment The Experiment in which the user is to be bucketed. | ||
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash. | ||
* @param projectConfig The current projectConfig | ||
* @param options An array of decision options | ||
* @param reasons Decision log messages | ||
* @return Variation the user is bucketed into or null. | ||
*/ | ||
@Nullable | ||
public Variation bucket(@Nonnull Experiment experiment, | ||
@Nonnull String bucketingId, | ||
@Nonnull ProjectConfig projectConfig, | ||
@Nullable List<OptimizelyDecideOption> options, | ||
@Nullable DecisionReasons reasons) { | ||
|
||
// support existing custom Bucketers | ||
if (isMethodOverriden("bucket", Experiment.class, String.class, ProjectConfig.class)) { | ||
return bucket(experiment, bucketingId, projectConfig); | ||
} | ||
|
||
return bucketCore(experiment, bucketingId, projectConfig, options, reasons); | ||
} | ||
|
||
/** | ||
* Assign a {@link Variation} of an {@link Experiment} to a user based on hashed value from murmurhash3. | ||
* | ||
* @param experiment The Experiment in which the user is to be bucketed. | ||
* @param bucketingId string A customer-assigned value used to create the key for the murmur hash. | ||
* @param projectConfig The current projectConfig | ||
* @return Variation the user is bucketed into or null. | ||
*/ | ||
@Nullable | ||
public Variation bucket(@Nonnull Experiment experiment, | ||
@Nonnull String bucketingId, | ||
@Nonnull ProjectConfig projectConfig) { | ||
return bucketCore(experiment, bucketingId, projectConfig, null, null); | ||
} | ||
|
||
@Nullable | ||
public Variation bucketCore(@Nonnull Experiment experiment, | ||
@Nonnull String bucketingId, | ||
@Nonnull ProjectConfig projectConfig, | ||
@Nullable List<OptimizelyDecideOption> options, | ||
@Nullable DecisionReasons reasons) { | ||
// ---------- Bucket User ---------- | ||
String groupId = experiment.getGroupId(); | ||
// check whether the experiment belongs to a group | ||
if (!groupId.isEmpty()) { | ||
Group experimentGroup = projectConfig.getGroupIdMapping().get(groupId); | ||
// bucket to an experiment only if group entities are to be mutually exclusive | ||
if (experimentGroup.getPolicy().equals(Group.RANDOM_POLICY)) { | ||
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig); | ||
Experiment bucketedExperiment = bucketToExperiment(experimentGroup, bucketingId, projectConfig, options, reasons); | ||
if (bucketedExperiment == null) { | ||
logger.info("User with bucketingId \"{}\" is not in any experiment of group {}.", bucketingId, experimentGroup.getId()); | ||
DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in any experiment of group %s.", bucketingId, experimentGroup.getId()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of these static // Add and return formatted message
public String addInfof(String format, Object... args) {
String message = String.format(format, args);
addInfo(message);
return message;
} So this one liner becomes two, but its very transparent what is happening, eg: String reason = reasons.addInfof("User with bucketingId \"{}\" is not in any experiment of group {}.", bucketingId, experimentGroup.getId());
logger.info(reason); You can even log directly within |
||
return null; | ||
} else { | ||
|
||
} | ||
// if the experiment a user is bucketed in within a group isn't the same as the experiment provided, | ||
// don't perform further bucketing within the experiment | ||
if (!bucketedExperiment.getId().equals(experiment.getId())) { | ||
logger.info("User with bucketingId \"{}\" is not in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(), | ||
DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is not in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), | ||
experimentGroup.getId()); | ||
return null; | ||
} | ||
|
||
logger.info("User with bucketingId \"{}\" is in experiment \"{}\" of group {}.", bucketingId, experiment.getKey(), | ||
DecisionService.logInfo(logger, reasons, "User with bucketingId \"%s\" is in experiment \"%s\" of group %s.", bucketingId, experiment.getKey(), | ||
experimentGroup.getId()); | ||
} | ||
} | ||
|
||
return bucketToVariation(experiment, bucketingId); | ||
return bucketToVariation(experiment, bucketingId, options, reasons); | ||
} | ||
|
||
|
||
//======== Helper methods ========// | ||
|
||
/** | ||
|
@@ -175,5 +211,12 @@ int generateBucketValue(int hashCode) { | |
return (int) Math.floor(MAX_TRAFFIC_VALUE * ratio); | ||
} | ||
|
||
Boolean isMethodOverriden(String methodName, Class<?>... params) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to worry about custom overrides such as this. Let me know if there's a specific concern or a hypothetical one. |
||
try { | ||
return getClass() != Bucketer.class && getClass().getDeclaredMethod(methodName, params) != null; | ||
} catch (NoSuchMethodException e) { | ||
return false; | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we move
OptimizelyUserContext
into the rootcom.optimizely.ab
package then we don't have to open this up so widely, but instead can be makepackage private