Skip to content

Conversation

@mnoman09
Copy link
Contributor

@mnoman09 mnoman09 commented Jan 2, 2019

Summary

  • Added logging in audience evaluation code
  • Added its unit tests to verify logging

@coveralls
Copy link

Pull Request Test Coverage Report for Build 813

  • 28 of 29 (96.55%) changed or added relevant lines in 5 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 90.01%

Changes Missing Coverage Covered Lines Changed/Added Lines %
core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java 12 13 92.31%
Totals Coverage Status
Change from base Build 807: 0.2%
Covered Lines: 2721
Relevant Lines: 3023

💛 - Coveralls

@coveralls
Copy link

coveralls commented Jan 2, 2019

Pull Request Test Coverage Report for Build 858

  • 35 of 37 (94.59%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.3%) to 89.709%

Changes Missing Coverage Covered Lines Changed/Added Lines %
core-api/src/main/java/com/optimizely/ab/config/audience/AudienceIdCondition.java 4 5 80.0%
core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java 15 16 93.75%
Totals Coverage Status
Change from base Build 857: 0.3%
Covered Lines: 2711
Relevant Lines: 3022

💛 - Coveralls

Copy link
Contributor

@loganlinn loganlinn left a comment

Choose a reason for hiding this comment

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

This isn't specific to this PR, but is there a reason why we use String.format when we log? The Logger class can format more efficiently using the info(String format, Object... arguments) methods. @mnoman09, I know you were following the existing conventions; this is more pointed at @optimizely/fullstack-devs.

Match matchType = MatchType.getMatchType(match, value, this).getMatcher();
Boolean result = matchType.eval(userAttributeValue);

if (!matchType.getClass().toString().contains("Null") && result == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

substring matching the class name is a very nasty hack 👎
any logging specific to that class is better off being inside that class, i.e. the eval method of NullMatch in this case

if (!"custom_attribute".equals(type)) {
MatchType.logger.error(String.format("condition type not equal to `custom_attribute` %s", type));
logger.error(String.format("Audience condition \"%s\" has an unknown condition type: %s", this.toString(), type));
logger.error(String.format("condition type not equal to `custom_attribute` %s", type));
Copy link
Contributor

Choose a reason for hiding this comment

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

single log line per error is highly preferred -- this is not a sufficient error message by itself. If we want additional information in the message, modify the existing logger.error() call, otherwise you can log "supplemental" information like this with a different log level here.

//Missing attribute value
logger.debug(String.format("Audience condition \"%s\" evaluated as UNKNOWN because no value was passed for user attribute \"%s\"", this.toString(), name));
} else {
logger.warn(
Copy link
Contributor

Choose a reason for hiding this comment

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

not justified as warn level IMO

Copy link
Contributor

Choose a reason for hiding this comment

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

@loganlinn, to clarify, are you saying that this message is redundant with the warning that we would have logged in castToValueType?

Copy link
Contributor

Choose a reason for hiding this comment

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

As of commit 2be5a31, we're no longer logging a warning in castToValueType

return new MatchType(matchType, new NullMatch());
}

logger.warn(String.format("Audience condition \"%s\" condition value \"%s\" data type is inapplicable", userAttribute.toString(), conditionValue));
Copy link
Contributor

Choose a reason for hiding this comment

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

not justified as warn level IMO.

this would be more informative if we logged matchType.
also, think we shouldn't be logging conditionValue object -- it is potentially very expensive (depends on it's toString() impl) and has potential to log undesired data (PII).

Copy link
Contributor

Choose a reason for hiding this comment

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

not justified as warn level IMO.

I dunno, a customer probably won't expect an audience condition to become inactive for this reason, and therefore probably ought to be warned. If it helps, the updated log message suggests that the customer upgrade their SDK.

this would be more informative if we logged matchType.

It's included (along with type, name, and value) in our logging of the audience condition (the userAttribute variable).

also, think we shouldn't be logging conditionValue object -- it is potentially very expensive (depends on it's toString() impl) and has potential to log undesired data (PII).

Good feedback. We're not logging it any more!

private Match matcher;

public static MatchType getMatchType(String matchType, Object conditionValue) {
public static MatchType getMatchType(String matchType, Object conditionValue, UserAttribute userAttribute) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👎 to adding an argument to the signature just for logging

Copy link
Contributor

Choose a reason for hiding this comment

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

@loganlinn Do you have any suggestion to deal it, where we need to log with userAttributes. I am thinking to add some enum types for NullMatch, when NullMatch is returned, we'll also set NullMatchReasonType in the object. So In two or many more different cases we will check it using ReasonType under NullMatch object.

try {
return conditions.evaluate(projectConfig, attributes);
Boolean result = conditions.evaluate(projectConfig, attributes);
logger.info(String.format("Audiences for experiment %s collectively evaluated as " + result, experiment.getKey()));
Copy link
Contributor

Choose a reason for hiding this comment

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

not justified as info level IMO.

I would rather leave it up to the user of SDK to log this on the caller side than to put here.

// check user attribute value is equal
try {
return MatchType.getMatchType(match, value).getMatcher().eval(userAttributeValue);
Match matchType = MatchType.getMatchType(match, value, this.toString()).getMatcher();
Copy link
Contributor

Choose a reason for hiding this comment

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

please see my comments below for this position.

private Match matcher;

public static MatchType getMatchType(String matchType, Object conditionValue) {
public static MatchType getMatchType(String matchType, Object conditionValue, String audienceCondition) {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove arg from here.

# Conflicts:
#	core-api/src/main/java/com/optimizely/ab/config/audience/UserAttribute.java
#	core-api/src/main/java/com/optimizely/ab/config/audience/match/AttributeMatch.java
#	core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java
#	core-api/src/test/java/com/optimizely/ab/internal/ExperimentUtilsTest.java
Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

Sorry for the late response! I left a bunch of comments; hope they're not too crazy. @msohailhussain @loganlinn I'd be happy to walk through these offline, if that helps

return result;
} catch (NullPointerException np) {
MatchType.logger.error(String.format("attribute or value null for match %s", match != null ? match : "legacy condition"), np);
logger.error(String.format("attribute or value null for match %s", match != null ? match : "legacy condition"), np);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This is a somewhat cryptic log message. @loganlinn @thomaszurkan-optimizely maybe we can audit our code to include all necessary null checks and then get rid of this NullPointerException catcher? Tho maybe it's not worthwhile if we don't expect to get here in practice...

//Missing attribute value
logger.debug(String.format("Audience condition \"%s\" evaluated as UNKNOWN because no value was passed for user attribute \"%s\"", this.toString(), name));
} else {
logger.warn(
Copy link
Contributor

Choose a reason for hiding this comment

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

@loganlinn, to clarify, are you saying that this message is redundant with the warning that we would have logged in castToValueType?

Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! Lemme see if I can get another code review from someone who's more familiar with this.

Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! And thanks for writing unit tests for this, too. It's looking good apart from a couple suggestions!

Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

One remaining request for the test cases!

Match matchType = MatchType.getMatchType(match, value).getMatcher();
Boolean result = matchType.eval(userAttributeValue);

if (!(matchType instanceof NullMatch) && result == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm opposed to this approach. The instanceof and numerous logic branches are hard to follow and logging makes assumptions about the implementation details of matcher. This may be correct right now, but not guaranteed in the future. It smells out of being out of place and not a proper utilization of MatchType/Matcher/etc

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, smells a little funky to me too.

The alternative is logging from the matcher itself. Given the type of log message that we want, that means passing down the entire audience condition to the matcher, which I think you'd been unsure about. And we'd also want to pass an additional boolean if we want to preserve distinct messages for "no value" and "a null value".

How should we proceed?

Copy link
Contributor

@nchilada nchilada Jan 18, 2019

Choose a reason for hiding this comment

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

My opinion (which I will also mention in other SDK's PRs) is that it's okay to pass additional data for logging purposes. @msohailhussain @mikeng13 FYI

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm so late to the party. But, I agree: It stinks. 2 questions on approach:

  1. What's wrong with logging the null messages on the match type and remove the if instanceof?
  2. Why not just create two exceptions and catch for those and log that way? It is way cleaner than this.

Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

Thanks for all the updates! The log messages look good to me, but will wait to hear @loganlinn's feedback on how the mismatch logs are instrumented.

Copy link
Contributor

@thomaszurkan-optimizely thomaszurkan-optimizely left a comment

Choose a reason for hiding this comment

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

Can we change the NullMatchType errors to exceptions we catch with the error string already there?

Match matchType = MatchType.getMatchType(match, value).getMatcher();
Boolean result = matchType.eval(userAttributeValue);

if (!(matchType instanceof NullMatch) && result == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I'm so late to the party. But, I agree: It stinks. 2 questions on approach:

  1. What's wrong with logging the null messages on the match type and remove the if instanceof?
  2. Why not just create two exceptions and catch for those and log that way? It is way cleaner than this.

this);
}
return result;
} catch (NullPointerException np) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a couple of new exception type instead of matchType instanceof NullMatch?

MatchType.logger.error(String.format("attribute or value null for match %s", match != null ? match : "legacy condition"), np);
logger.error("attribute or value null for match {}", match != null ? match : "legacy condition", np);
return null;
} catch (NullMatch nm) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be an exception. There should be one per error.


package com.optimizely.ab.config.audience.match;

public enum NullMatchTypeErrors {
Copy link
Contributor

Choose a reason for hiding this comment

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

get rid of this class

Copy link
Contributor

Choose a reason for hiding this comment

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

create two exceptions UnknownMatchTypeException and UnexpectedValueTypeException.


protected NullMatch() {
this.value = null;
protected NullMatch(NullMatchTypeErrors nullMatchTypeErrors) {
Copy link
Contributor

Choose a reason for hiding this comment

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

get rid of this constructor.

package com.optimizely.ab.config.audience.match;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class NullMatch extends Throwable {
Copy link
Contributor

Choose a reason for hiding this comment

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

create two new exceptions with the suffix Exception and set the appropriate errorDescription

Copy link
Contributor

@thomaszurkan-optimizely thomaszurkan-optimizely left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks!

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.

7 participants