Skip to content

feat(AudienceEvaluator): Add the ability to provide custom condition evaluators #288

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 13 commits into from
Jun 19, 2019

Conversation

jamesopti
Copy link
Contributor

@jamesopti jamesopti commented Jun 7, 2019

Summary

This PR (replacing #216) adds the ability for an SDK consumer to pass their own set of condition evaluators when creating an optimizely instance.

The core motivator for this is to be able to evaluate WEB audience conditions using the Javascript SDK as part of the EDGE project (go/edge).

Future fullstack use cases for this functionality might include audience condition plugins, where the customer would define their own match types and provide their own matchers.

/**
 * Condition evaluators for conditions that can be evaluated in a Cloudflare worker using
 * information like cookies, query parameters, userAgent, etc...
 */
function getConditionEvaluators(inputs) {
  return {
    cookies: {
      evaluate: matchCookies.match.bind(this, { cookies: inputs.cookies }),
    },
    device: {
      evaluate: matchDevice.match.bind(this, { device: Detect.parseUA(inputs.ua).device }),
    },
    ...
    /* Other custom condition evaluators */
  }
}

var optlyInstance = optimizely.createInstance({
  datafile: datafile,
  conditionEvaluators: getConditionEvaluators(inputs),
  skipJSONValidation: true
});

Test plan

Fixed existing tests. May need to add more unit tests.

Issues

https://optimizely.atlassian.net/browse/CJS-3034

rebase with latest audience editor changes

updates per code review
@jamesopti jamesopti force-pushed the james/custom_condition_evaluators_OG branch from 0112ff9 to 2ff9d2e Compare June 7, 2019 21:11
@coveralls
Copy link

coveralls commented Jun 7, 2019

Coverage Status

Coverage decreased (-0.07%) to 97.531% when pulling 4da5d4a on james/custom_condition_evaluators_OG into bd49de0 on master.

@@ -56,7 +56,6 @@ EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator;
*/
function evaluate(condition, userAttributes, logger) {
if (condition.type !== CUSTOM_ATTRIBUTE_CONDITION_TYPE) {
logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)));
return null;
}
Copy link

Choose a reason for hiding this comment

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

delete? because we're handling this in typeToEvaluate - we only call this if there's a type.

@lpappone lpappone changed the title James/custom condition evaluators og feat(AudienceEvaluator): Add the ability to provide custom condition evaluators Jun 7, 2019
* @constructor
*/
function AudienceEvaluator(logger, UNSTABLE_conditionEvaluators) {
this.logger = logger;
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of passing logger, we can import a singleton logger and use it directly (example from project_config_manager)

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

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

Mostly looks good. I just have a question.

* @constructor
*/
function AudienceEvaluator(logger, UNSTABLE_conditionEvaluators) {
this.logger = logger;
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

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

This code looks really good. My main feedback relates to the interface, particularly if we think there's a chance that we'll eventually expose this kind of interface publicly. I'm going off the code sample in the PR description:

/**
 * Condition evaluators for conditions that can be evaluated in a Cloudflare worker using
 * information like cookies, query parameters, userAgent, etc...
 */
function getConditionEvaluators(inputs) {
  return {
    cookies: {
      evaluate: matchCookies.match.bind(this, { cookies: inputs.cookies }),
    },
    device: {
      evaluate: matchDevice.match.bind(this, { device: Detect.parseUA(inputs.ua).device }),
    },
    ...
    /* Other custom condition evaluators */
  }
}

var optlyInstance = optimizely.createInstance({
  datafile: datafile,
  conditionEvaluators: getConditionEvaluators(inputs),
  skipJSONValidation: true
});

My comments:

  • Why require customers to pass an object with an evaluate function rather than passing a naked function? Just because that's the shape of the custom_attribute_condition_evaluator module?
  • Should we think more deeply about all the context that we pass to the evaluate functions? It looks like we're passing the condition, the user attributes, and a logger. I wonder if we'll eventually need to document what to do with the logger, and I think we ought to pass the user ID in addition to the user attributes.

At any rate, I found the code sample very useful! Only one nit: it's a little confusing to capture inputs at the moment the SDK is initialized. Instead, can we declare a module-level variable named userState or something? I'd be happy to make this change in the PR description / commit message since it is de-facto documentation for the new feature.

@lpappone
Copy link

@mikeng13 @mjc1283 Could you guys weigh in on whether this is something you plan to consider exposing to customers? A lot of Nikhil's concerns seem to center on that, and it's not something that I feel I have the context to decide.

In the meantime to try to address a couple questions:

Why require customers to pass an object with an evaluate function rather than passing a naked function? Just because that's the shape of the custom_attribute_condition_evaluator module?

Yes, I think that was the case. Maybe @jamesopti has more to say.

I wonder if we'll eventually need to document what to do with the logger, and I think we ought to pass the user ID in addition to the user attributes.

I don't think we're passing a logger anymore - can you comment in the code if you see it? What do you think the user ID is needed for?

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.

Why require customers to pass an object with an evaluate function rather than passing a naked function? Just because that's the shape of the custom_attribute_condition_evaluator module?

Yes, I think that was the case. Maybe @jamesopti has more to say.

🙏

I don't think we're passing a logger anymore - can you comment in the code if you see it?

Hmm, maybe there's a bug here? You're right that we're only passing the condition and user attributes to the evaluator, but the "custom_attribute" evaluator still expects to receive a logger. We should fix this and add a unit test to make sure we're correctly calling the evaluator.

What do you think the user ID is needed for?

For customer-defined audience conditions, the user ID might be more useful than user attributes, since user attributes are primarily relevant for "custom_attribute" conditions. I'm sure the customer can stash the user ID in some closure, along with other user-specific state, but passing the user ID seems like a nice touch.

Perhaps we can pass named arguments (read: an "options" object, in the case of JavaScript) to the customer's callback. This way we can start passing the user ID in the future without breaking previously-instrumented callbacks. This is what we do for notification listeners.

@nchilada
Copy link
Contributor

Why require customers to pass an object with an evaluate function rather than passing a naked function? Just because that's the shape of the custom_attribute_condition_evaluator module?

Yes, I think that was the case. Maybe @jamesopti has more to say.

🙏

@lpappone ohh, my bad, I didn't realize you were authoring this! I can try to close out some of my comment threads if you're ready to move forward

@lpappone
Copy link

@lpappone ohh, my bad, I didn't realize you were authoring this! I can try to close out some of my comment threads if you're ready to move forward

I resolved things that seemed resolvable.

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.

Looking good! Just a couple outstanding points of discussion

return null;
}
try {
return evaluator.evaluate(condition, userAttributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we need to think about the arguments we're passing here.

  • Primarily we need to pass a logger or update the custom_attribute_condition_evaluator to import the logger directly
  • Can we pass an object with named properties?

Copy link
Contributor Author

@jamesopti jamesopti Jun 17, 2019

Choose a reason for hiding this comment

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

This is a good point.

We DO need to pass a logger - I mistakenly omitted this. We can't have custom_attribute_condition_evaluator import it as its stateful to this instance of AudienceEvaluator

Can we pass an object with named properties?

WDYM exactly? The custom_attribute_condition_evaluator needs the userAttributes map to do it's matching, but other matchers won't necessarily need them.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh. I'm proposing named parameters like this:

Suggested change
return evaluator.evaluate(condition, userAttributes);
return evaluator.evaluate({
condition: condition,
userAttributes: userAttributes,
});

This is how we invoke customer's notification listener callbacks (example) and, arguably, is how we ought to accept inputs to all of our public API methods.

Choose a reason for hiding this comment

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

Ok, doing that here would involve changing a lot of established code (like this). I don't think it makes a ton of sense to make that change in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, the changes ought to be limited to custom_attribute_condition_evaluator.evaluate and its unit tests. I think this is the best time to take this on - we're starting to move towards a public API, and we're prepare to introduce many more evaluators than custom_attribute_condition_evaluator

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.

Changes lgtm

Copy link
Contributor Author

@jamesopti jamesopti left a comment

Choose a reason for hiding this comment

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

I cant request changes on my own PR. Haha. Added to @nchilada 's comment though that we do need to update passing the logger.

return null;
}
try {
return evaluator.evaluate(condition, userAttributes);
Copy link
Contributor Author

@jamesopti jamesopti Jun 17, 2019

Choose a reason for hiding this comment

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

This is a good point.

We DO need to pass a logger - I mistakenly omitted this. We can't have custom_attribute_condition_evaluator import it as its stateful to this instance of AudienceEvaluator

Can we pass an object with named properties?

WDYM exactly? The custom_attribute_condition_evaluator needs the userAttributes map to do it's matching, but other matchers won't necessarily need them.

@lpappone
Copy link

Ok, added the logger. @nchilada I am also not sure what you intended with "Can we pass an object with named properties?"

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 fix!

return null;
}
try {
return evaluator.evaluate(condition, userAttributes);
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh. I'm proposing named parameters like this:

Suggested change
return evaluator.evaluate(condition, userAttributes);
return evaluator.evaluate({
condition: condition,
userAttributes: userAttributes,
});

This is how we invoke customer's notification listener callbacks (example) and, arguably, is how we ought to accept inputs to all of our public API methods.

@lpappone
Copy link

@nchilada your "request changes" is blocking this from being merged, despite @mikeng13 approving it. Can you re-review please?

@lpappone
Copy link

@mikeng13 @mjc1283 @jamesopti
Can you guys (at least a couple of you) weigh in on @nchilada's suggestion to pass an object with named properties to the custom_attribute_condition_evaluator? I'm not familiar enough with the SDK to have a strong opinion, and I don't want to put in the time to do the work unless there's some consensus that it's the right thing to do.

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.

@mikeng13 says we'll try to clean up the audience-condition evaluation interface in the future

@mikeproeng37 mikeproeng37 removed the request for review from jordangarcia June 19, 2019 22:24
@mikeproeng37 mikeproeng37 merged commit ed884fb into master Jun 19, 2019
@nchilada nchilada deleted the james/custom_condition_evaluators_OG branch June 24, 2019 18:49
jamesopti pushed a commit that referenced this pull request Jan 29, 2020
…condition evaluator (#389)

## Summary
#288 introduced an exploratory new feature, the ability to pass custom condition evaluators to the SDK constructor via the option `UNSTABLE_conditionEvaluators`.

The AudienceEvaluator (and its tests) were refactored as a part of that PR, but no tests were added to assert that the `UNSTABLE_conditionEvaluators` worked when passed.

This PR introduces tests to ensure that functionality continues to work.

## Test plan
Unit tests added to assert that:
- You cannot override the built in `custom_attribute` evaluator
- A passed in custom condition evaluator will evaluate conditions of that type
- That the audience condition, user attributes, and logger instance are all passed to the custom evaluator

## Issues
- CJS-3815
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.

6 participants