Skip to content

feat(api): Accepting all types for attributes values #207

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

Merged
merged 23 commits into from
Aug 31, 2018

Conversation

shihpatrick
Copy link
Contributor

@shihpatrick shihpatrick commented Aug 21, 2018

Summary

As a part of Audience Match Type, I changed all relevant function signatures so that it accepts an Object variable rather than just a String for functions that accept and uses attributes. Any filtering of types would be done later, when evaluating audience conditions.

Testing

Passes all current unit tests. Further testing of other types for attribute values (like filtering out types that aren't Boolean, String, or Number will be done when dealing with audience conditions.

@coveralls
Copy link

coveralls commented Aug 21, 2018

Pull Request Test Coverage Report for Build 599

  • 20 of 22 (90.91%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.02%) to 89.388%

Changes Missing Coverage Covered Lines Changed/Added Lines %
core-api/src/main/java/com/optimizely/ab/Optimizely.java 6 7 85.71%
core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java 8 9 88.89%
Totals Coverage Status
Change from base Build 589: -0.02%
Covered Lines: 2350
Relevant Lines: 2629

💛 - Coveralls

@tylerbrandt
Copy link
Contributor

build

@mikeproeng37
Copy link
Contributor

build-e2e

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.

Let’s add a instance of check for type string before cast for the bucketing id.

@@ -131,7 +131,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
if (ExperimentUtils.isUserInExperiment(projectConfig, experiment, filteredAttributes)) {
String bucketingId = userId;
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably check the type just to be safe.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If it isn't a string, should we keep it as userId?

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct, we don't use the bucketingId if it's not a string and instead keep using the userId

Copy link
Contributor

Choose a reason for hiding this comment

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

And I assume you're on top of this, but we should log a ~warning if we're ignoring the bucketingId due to type mismatch.

And while you're at it...we should also log at ~info level in cases where do ingest a usable bucketingId!

@@ -212,7 +212,7 @@ public DecisionService(@Nonnull Bucketer bucketer,
int rolloutRulesLength = rollout.getExperiments().size();
String bucketingId = userId;
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should check for null value and that the value is a string.

public boolean evaluate(Map<String, String> attributes) {
String userAttributeValue = attributes.get(name);
public boolean evaluate(Map<String, ?> attributes) {
Object userAttributeValue = attributes.get(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm should be careful with this. Eventually when we do accept objects and arrays to be passed in, the equals evaluation below will likely perform a reference check as well. That said, I think it's fine for now, but just leave a message saying that we should be extra considerate when we decide to allow objects and arrays.

Copy link
Contributor

@mikeproeng37 mikeproeng37 left a comment

Choose a reason for hiding this comment

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

Please add unit tests for the different value types that we support: integers, doubles, booleans, strings in the activate API

@shihpatrick shihpatrick changed the title refactor(api): Accepting all types for attributes values feat(api): Accepting all types for attributes values Aug 22, 2018
@@ -765,7 +766,7 @@ public UserProfileService getUserProfileService() {
if (unknownAttributes != null) {
logger.warn("Attribute(s) {} not in the datafile.", unknownAttributes);
// make a copy of the passed through attributes, then remove the unknown list
attributes = new HashMap<String, String>(attributes);
attributes = new HashMap<String, Object>(attributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

You should add a test that actually removes unknown attributes to see what happens here. But my understanding is that Object is actually different from ?.
See https://stackoverflow.com/questions/678822/what-is-the-difference-between-and-object-in-java-generics

Copy link
Contributor

Choose a reason for hiding this comment

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

You can't use a question mark in a constructor.

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think that the proper way to use the constructor is HashMap<>(attributes). So, @wangjoshuah is correct.

Please update to something like:
HashMap<String, ?> attr = new HashMap<>();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like <> was not introduced until Java 7 and we're currently on Java 6. Thoughts?

https://stackoverflow.com/questions/16612157/why-does-new-hashmap-produce-an-error-in-jdk-1-6-but-not-1-7?lq=1

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, it sounds like

new HashMap<String, Object>();

is indeed the way to go...

Copy link
Contributor

Choose a reason for hiding this comment

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

#208 is the way to go. Good call @thomaszurkan-optimizely

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.

lets change that constructor to new HashMap<>(attributes)

@@ -765,7 +766,7 @@ public UserProfileService getUserProfileService() {
if (unknownAttributes != null) {
logger.warn("Attribute(s) {} not in the datafile.", unknownAttributes);
// make a copy of the passed through attributes, then remove the unknown list
attributes = new HashMap<String, String>(attributes);
attributes = new HashMap<String, Object>(attributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think that the proper way to use the constructor is HashMap<>(attributes). So, @wangjoshuah is correct.

Please update to something like:
HashMap<String, ?> attr = new HashMap<>();

@shihpatrick
Copy link
Contributor Author

build

1 similar comment
@shihpatrick
Copy link
Contributor Author

build

@thomaszurkan-optimizely
Copy link
Contributor

build

1 similar comment
@mikeproeng37
Copy link
Contributor

build

@shihpatrick
Copy link
Contributor Author

build

@shihpatrick
Copy link
Contributor Author

shihpatrick commented Aug 28, 2018

Testing the changes

This e2e test does not have the proper configuration and the appropriate configuration is on Jenkins 4318.

Testing the original code

I pieced together some material to hopefully override the current failing of the e2e tests.
I wanted to see if the errors are consistent with the original masters of both the java-sdk and java-testapp.

Comparison

If we compare the consoles of the two tests, we would notice that the errors from the first test that includes changes to attributes are the same as the errors from the original code (but there is one more error that shows up in the original code*). This may suggest that the changes in this PR does not cause the errors.

*The extra error caused is with tests/test_user_profiles.py:285

@mikeng13 @thomaszurkan-optimizely

logger.info("bucketingId is valid");
}
else {
logger.warn("bucketingID has type mismatch, defaulted to userId ");
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 should just say BucketingID attribute is not a string. Defaulted to userId.

bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
if (String.class.isInstance(filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()))) {
bucketingId = (String) filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
logger.info("bucketingId is valid");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: would be useful to print out what that bucketing ID is. Also, this should be a debug message.

@@ -212,7 +218,13 @@ public DecisionService(@Nonnull Bucketer bucketer,
int rolloutRulesLength = rollout.getExperiments().size();
String bucketingId = userId;
if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
if (String.class.isInstance(filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString()))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic is repeated from up above. Can you consolidate into a helper function?

@@ -23,5 +23,5 @@
*/
public interface Condition {

boolean evaluate(Map<String, String> attributes);
boolean evaluate(Map<String, ?> attributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

Update the header on this file to be 2016-2018. Please also do it for other files you've touched in this commit.

@@ -37,7 +37,7 @@ public Condition getCondition() {
return condition;
}

public boolean evaluate(Map<String, String> attributes) {
public boolean evaluate(Map<String, ?> attributes) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to update the toString method on this class to account for non-string values? Seems like it could lead to exceptions

@@ -36,7 +36,7 @@ public OrCondition(@Nonnull List<Condition> conditions) {
return conditions;
}

public boolean evaluate(Map<String, String> attributes) {
public boolean evaluate(Map<String, ?> attributes) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above re: the toString method


public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable String value) {
public UserAttribute(@Nonnull String name, @Nonnull String type, @Nullable Object value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

@thomaszurkan-optimizely do we need to overload this constructor to allow you to pass in a String value? I don't think this change will break current usage of the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did you mean Object value?
Would this change be more appropriate when dealing with audience conditions?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind, I think this will be fine as is. Can ignore my previous comment.

@@ -43,6 +43,7 @@
import com.optimizely.ab.notification.NotificationCenter;
import com.optimizely.ab.notification.TrackNotificationListener;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.hamcrest.Matcher;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still needed? Maybe I am missing where it's used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Accidentally left that in, my bad!

@@ -70,7 +70,7 @@ public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @
public void testAddActivateNotification() {
int notificationId = notificationCenter.addActivateNotificationListener(new ActivateNotificationListenerInterface() {
@Override
public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map<String, String> attributes, @Nonnull Variation variation, @Nonnull LogEvent event) {
public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map<String, ?> attributes, @Nonnull Variation variation, @Nonnull LogEvent event) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this break current usage of the notification listeners? If someone has already implemented notification listeners with just Map<String, String>, will they suddenly see build errors?

Copy link
Contributor Author

@shihpatrick shihpatrick Aug 29, 2018

Choose a reason for hiding this comment

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

It would break current usage (I had to make changes in unit test and the java-testapp). Does it make sense to leave it as Map<String, String> though? I think there would be errors if someone tries to use Notification Listeners on attributes with Map<String, Object> if we leave this as Map<String, String>.

Maybe provide both as abstract methods?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's hold off on this until we discuss it today. My inclination is that we leave Map<String, String>, and in our notification center, coerce all values to String types

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From today's discussion -- looks like we're using Map<String, ?>

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep we are.

Copy link
Contributor

@mikeproeng37 mikeproeng37 left a comment

Choose a reason for hiding this comment

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

Almost there! I just have a few small suggestions

@@ -343,4 +337,25 @@ void saveVariation(@Nonnull Experiment experiment,
}
}
}

/**
* @param userId The userId of the user.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please provide a description for this method. Also, please make it private as it is not used outside this class.

if (filteredAttributes.containsKey(ControlAttribute.BUCKETING_ATTRIBUTE.toString())) {
bucketingId = filteredAttributes.get(ControlAttribute.BUCKETING_ATTRIBUTE.toString());
}
String bucketingId = getBucketingId(userId,filteredAttributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: space after filteredAttributes

@@ -1,6 +1,6 @@
/**
*
* Copyright 2016-2017, Optimizely and contributors
* Copyright 2016-2018, Optimizely and contributors
Copy link
Contributor

Choose a reason for hiding this comment

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

You didn't change this class so no need to update the year.

@@ -1,6 +1,6 @@
/**
*
* Copyright 2016-2017, Optimizely and contributors
* Copyright 2016-2018, Optimizely and contributors
Copy link
Contributor

Choose a reason for hiding this comment

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

Please also revert this change since we aren't touching this file this year

Copy link
Contributor

@mikeproeng37 mikeproeng37 left a comment

Choose a reason for hiding this comment

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

lgtm

@shihpatrick shihpatrick merged commit 4ecd765 into master Aug 31, 2018
@shihpatrick shihpatrick deleted the pshih/attribute-types branch September 5, 2018 01:13
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.

8 participants