Skip to content
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

feat(decide): add decide API #407

Merged
merged 20 commits into from
Oct 16, 2020
Merged

feat(decide): add decide API #407

merged 20 commits into from
Oct 16, 2020

Conversation

jaeopt
Copy link
Contributor

@jaeopt jaeopt commented Oct 9, 2020

Summary

The second part of 2 PRs for Decide API:

  • add decide/decideAll/trackEvent API implementation
  • fix DecisionService and Bucketer to support DecideOptions and DecisionReasons (this fix should not break existing custom DecisionServerice/Bucketer)

@jaeopt jaeopt requested a review from a team as a code owner October 9, 2020 00:20
@jaeopt jaeopt removed their assignment Oct 9, 2020
@jaeopt jaeopt requested a review from mikecdavis October 9, 2020 00:20
@@ -227,7 +226,7 @@ private Variation activate(@Nullable ProjectConfig projectConfig,
return variation;
}

private void sendImpression(@Nonnull ProjectConfig projectConfig,
public void sendImpression(@Nonnull ProjectConfig projectConfig,
Copy link
Contributor

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 root com.optimizely.ab package then we don't have to open this up so widely, but instead can be make package private

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 List makes sense as opposed to array. See comments in #406

Comment on lines 141 to 178
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of these static DecisionService.logXXX functions is adding more confusion than clarity. I think it would be more readable to have a function on DecisionReasons that formats the message, then log in the calling class.

// 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 DecisionReasons but I bet that will break a lot of log tests.

@@ -175,5 +211,12 @@ int generateBucketValue(int hashCode) {
return (int) Math.floor(MAX_TRAFFIC_VALUE * ratio);
}

Boolean isMethodOverriden(String methodName, Class<?>... params) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Comment on lines 641 to 663
public static void logError(Logger logger, DecisionReasons reasons, String format, Object... args) {
String message = String.format(format, args);
logger.error(message);
if (reasons != null) reasons.addInfo(message);
}

public static void logWarn(Logger logger, DecisionReasons reasons, String format, Object... args) {
String message = String.format(format, args);
logger.warn(message);
if (reasons != null) reasons.addInfo(message);
}

public static void logInfo(Logger logger, DecisionReasons reasons, String format, Object... args) {
String message = String.format(format, args);
logger.info(message);
if (reasons != null) reasons.addInfo(message);
}

public static void logDebug(Logger logger, DecisionReasons reasons, String format, Object... args) {
String message = String.format(format, args);
logger.debug(message);
if (reasons != null) reasons.addInfo(message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment. I think we can reduce boiler plate by adding functionality into DecisionReasons.

if (reasons != null) reasons.addInfo(message);
}

Boolean isMethodOverriden(String methodName, Class<?>... params) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here. This type of method raises a bunch of red-flags and it's not clear to me why this approach was taken.

Comment on lines 24 to 30
List<String> errors;
List<String> logs;

public DecisionReasons() {
this.errors = new ArrayList<String>();
this.logs = new ArrayList<String>();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but you don't need to explicitly define the constructor:

final List<String> errors = new ArrayList<>();
final List<String> logs = new ArrayList<>();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also consider making these private


logger.info("Feature \"{}\" is enabled for user \"{}\"? {}", key, userId, flagEnabled);

return new OptimizelyDecision(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptimizelyUserContext is more of a convenience wrapper, the core "decide" logic behavior should exist in Optimizely#decide.

Comment on lines 289 to 291
private Map<String, Object> copyAttributes() {
return new HashMap<>(attributes);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be static, but also this util is not strictly necessary.

Comment on lines 299 to 305
public static String getFlagKeyInvalidMessage(String flagKey) {
return String.format(FLAG_KEY_INVALID, flagKey);
}

public static String getVariableValueInvalidMessage(String variableKey) {
return String.format(VARIABLE_VALUE_INVALID, variableKey);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One-liner "helper" methods then to add more cognitive overhead than they alleviate. Consider in-lining the String.format directly. Alternatively if you wanted to provide some abstraction, then a customer enum class can be used to define the format functionality:

    private enum Messages {
        FLAG_KEY_INVALID("Flag key \"%s\" is not in datafile."),
        VARIABLE_VALUE_INVALID("Variable value for key \"%s\" is invalid or wrong type.");

        private final String format;

        Messages(String format) {
            this.format = format;
        }

        public String format(Object...args) {
            return String.format(format, args);
        }
    }

Which can then be consumed as:

Messages.FLAG_KEY_INVALID.format("key");

Note that this implementation reads more direct about what is happening. We're formatting a predefined message.

@jaeopt jaeopt requested a review from mikecdavis October 16, 2020 17:33
@jaeopt jaeopt merged commit 5b6e798 into jae/user-context Oct 16, 2020
@jaeopt jaeopt deleted the jae/decide-api branch October 16, 2020 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants