-
Notifications
You must be signed in to change notification settings - Fork 83
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
Conversation
rebase with latest audience editor changes updates per code review
0112ff9
to
2ff9d2e
Compare
@@ -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; | |||
} |
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.
delete? because we're handling this in typeToEvaluate - we only call this if there's a type.
* @constructor | ||
*/ | ||
function AudienceEvaluator(logger, UNSTABLE_conditionEvaluators) { | ||
this.logger = logger; |
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.
Instead of passing logger, we can import a singleton logger and use it directly (example from project_config_manager)
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.
+1
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.
Mostly looks good. I just have a question.
* @constructor | ||
*/ | ||
function AudienceEvaluator(logger, UNSTABLE_conditionEvaluators) { | ||
this.logger = logger; |
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.
+1
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.
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 thecustom_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.
packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js
Outdated
Show resolved
Hide resolved
@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:
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? What do you think the user ID is needed for? |
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.
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.
@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 |
Co-Authored-By: Nikhil Chelliah <[email protected]>
I resolved things that seemed resolvable. |
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.
Looking good! Just a couple outstanding points of discussion
return null; | ||
} | ||
try { | ||
return evaluator.evaluate(condition, userAttributes); |
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.
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?
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.
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.
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.
Oh. I'm proposing named parameters like this:
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.
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.
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.
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.
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
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.
Changes lgtm
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.
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); |
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.
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.
Ok, added the logger. @nchilada I am also not sure what you intended with "Can we pass an object with named properties?" |
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.
Thanks for the fix!
return null; | ||
} | ||
try { | ||
return evaluator.evaluate(condition, userAttributes); |
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.
Oh. I'm proposing named parameters like this:
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.
@nchilada your "request changes" is blocking this from being merged, despite @mikeng13 approving it. Can you re-review please? |
@mikeng13 @mjc1283 @jamesopti |
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.
@mikeng13 says we'll try to clean up the audience-condition evaluation interface in the future
…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
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.
Test plan
Fixed existing tests. May need to add more unit tests.
Issues
https://optimizely.atlassian.net/browse/CJS-3034