-
Notifications
You must be signed in to change notification settings - Fork 83
DO NOT REVIEW - SEE https://github.com/optimizely/javascript-sdk/pull/288 #216
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
Changes from 3 commits
0738b58
2ae49e1
0112ff9
1ce7345
930975c
db29de1
7411b24
1102191
05deac9
76d155e
a023b08
0586dd8
f97df78
53f43a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,54 +16,88 @@ | |
var conditionTreeEvaluator = require('../condition_tree_evaluator'); | ||
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator'); | ||
var enums = require('../../utils/enums'); | ||
var fns = require('../../utils/fns'); | ||
var sprintf = require('sprintf-js').sprintf; | ||
|
||
var ERROR_MESSAGES = enums.ERROR_MESSAGES; | ||
var LOG_LEVEL = enums.LOG_LEVEL; | ||
var LOG_MESSAGES = enums.LOG_MESSAGES; | ||
var MODULE_NAME = 'AUDIENCE_EVALUATOR'; | ||
|
||
module.exports = { | ||
/** | ||
* Determine if the given user attributes satisfy the given audience conditions | ||
* @param {Array|String|null|undefined} audienceConditions Audience conditions to match the user attributes against - can be an array | ||
* of audience IDs, a nested array of conditions, or a single leaf condition. | ||
* Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1" | ||
* @param {Object} audiencesById Object providing access to full audience objects for audience IDs | ||
* contained in audienceConditions. Keys should be audience IDs, values | ||
* should be full audience objects with conditions properties | ||
* @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions | ||
* are met. If not provided, defaults to an empty object | ||
* @param {Object} logger Logger instance. | ||
* @return {Boolean} true if the user attributes match the given audience conditions, false | ||
* otherwise | ||
*/ | ||
evaluate: function(audienceConditions, audiencesById, userAttributes, logger) { | ||
// if there are no audiences, return true because that means ALL users are included in the experiment | ||
if (!audienceConditions || audienceConditions.length === 0) { | ||
return true; | ||
} | ||
|
||
if (!userAttributes) { | ||
userAttributes = {}; | ||
} | ||
/** | ||
* Construct an instance of AudienceEvaluator with a given logger and options | ||
* @param {Logger} logger The Logger instance | ||
* @param {Object=} __exploratoryConditionEvaluators A map of condition evaluators provided by the consumer. This enables matching | ||
* condition types which are not supported natively by the SDK. Note that built in | ||
* Optimizely evaluators cannot be overridden. | ||
* @constructor | ||
*/ | ||
function AudienceEvaluator(logger, __exploratoryConditionEvaluators) { | ||
this.logger = logger; | ||
this.typeToEvaluatorMap = fns.assignIn({}, __exploratoryConditionEvaluators, { | ||
'custom_attribute': customAttributeConditionEvaluator | ||
}); | ||
} | ||
|
||
var evaluateConditionWithUserAttributes = function(condition) { | ||
return customAttributeConditionEvaluator.evaluate(condition, userAttributes, logger); | ||
}; | ||
/** | ||
* Determine if the given user attributes satisfy the given audience conditions | ||
* @param {Array|String|null|undefined} audienceConditions Audience conditions to match the user attributes against - can be an array | ||
* of audience IDs, a nested array of conditions, or a single leaf condition. | ||
* Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1" | ||
* @param {Object} audiencesById Object providing access to full audience objects for audience IDs | ||
* contained in audienceConditions. Keys should be audience IDs, values | ||
* should be full audience objects with conditions properties | ||
* @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions | ||
* are met. If not provided, defaults to an empty object | ||
* @return {Boolean} true if the user attributes match the given audience conditions, false | ||
* otherwise | ||
*/ | ||
AudienceEvaluator.prototype.evaluate = function(audienceConditions, audiencesById, userAttributes) { | ||
// if there are no audiences, return true because that means ALL users are included in the experiment | ||
if (!audienceConditions || audienceConditions.length === 0) { | ||
return true; | ||
} | ||
nchilada marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var evaluateAudience = function(audienceId) { | ||
var audience = audiencesById[audienceId]; | ||
if (audience) { | ||
logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions))); | ||
var result = conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes); | ||
var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase(); | ||
logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText)); | ||
return result; | ||
} | ||
if (!userAttributes) { | ||
userAttributes = {}; | ||
} | ||
|
||
return null; | ||
}; | ||
var evaluateAudience = function(audienceId) { | ||
var audience = audiencesById[audienceId]; | ||
if (audience) { | ||
this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions))); | ||
var result = conditionTreeEvaluator.evaluate(audience.conditions, this.evaluateConditionWithUserAttributes.bind(this, userAttributes)); | ||
var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase(); | ||
this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText)); | ||
return result; | ||
} | ||
|
||
return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false; | ||
}, | ||
return null; | ||
}.bind(this); | ||
|
||
return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false; | ||
}; | ||
|
||
/** | ||
* Wrapper around evaluator.evaluate that is passed to the conditionTreeEvaluator. | ||
* Evaluates the condition provided given the user attributes if an evaluator has been defined for the condition type. | ||
* @param {Object} userAttributes A map of user attributes. | ||
* @param {Object} condition A single condition object to evaluate. | ||
* @return {Boolean|null} true if the condition is satisfied, null if a matcher is not found. | ||
*/ | ||
AudienceEvaluator.prototype.evaluateConditionWithUserAttributes = function(userAttributes, condition) { | ||
var evaluator = this.typeToEvaluatorMap[condition.type]; | ||
if (!evaluator) { | ||
this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition))); | ||
return null; | ||
} | ||
try { | ||
return evaluator.evaluate(condition, userAttributes, this.logger); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the evaluator we get is not the |
||
} catch (err) { | ||
this.logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.CONDITION_EVALUATOR_ERROR, MODULE_NAME, condition.type, err.message)); | ||
} | ||
return null; | ||
}; | ||
|
||
module.exports = AudienceEvaluator; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jamesopti I see this log message now in |
||
return null; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
* limitations under the License. * | ||
***************************************************************************/ | ||
|
||
var audienceEvaluator = require('../audience_evaluator'); | ||
var AudienceEvaluator = require('../audience_evaluator'); | ||
var bucketer = require('../bucketer'); | ||
var enums = require('../../utils/enums'); | ||
var fns = require('../../utils/fns'); | ||
|
@@ -52,6 +52,7 @@ function DecisionService(options) { | |
this.configObj = options.configObj; | ||
this.userProfileService = options.userProfileService || null; | ||
this.logger = options.logger; | ||
this.audienceEvaluator = new AudienceEvaluator(options.logger, options.__exploratoryConditionEvaluators); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels like we are polluting our factory method haha but I see no other easy way to integrate into the current setup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean the constructor? Yea in the current setup, all this state must be threaded through (config, logger instance, etc...) Would be nice to have a global registration system by instance ID so that this kinda global state can be easily accessed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is totally fine, by adding configuration to the As far as global state, things like audience evaluators should not be considered as global state. If some sort of global registry existed you'd still have to know the instance ID here to resolve the audience evaluators (which would have to be threaded through). However things like the logger and errorHandler should be global and having to thread them through here is not cool (this is changing soon in another PR) |
||
} | ||
|
||
/** | ||
|
@@ -168,9 +169,9 @@ DecisionService.prototype.__checkIfUserIsInAudience = function(experimentKey, us | |
var experimentAudienceConditions = projectConfig.getExperimentAudienceConditions(this.configObj, experimentKey); | ||
var audiencesById = projectConfig.getAudiencesById(this.configObj); | ||
this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCES_COMBINED, MODULE_NAME, experimentKey, JSON.stringify(experimentAudienceConditions))); | ||
var result = audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes, this.logger); | ||
var result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes); | ||
this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT_COMBINED, MODULE_NAME, experimentKey, result.toString().toUpperCase())); | ||
|
||
if (!result) { | ||
var userDoesNotMeetConditionsLogMessage = sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, MODULE_NAME, userId, experimentKey); | ||
this.logger.log(LOG_LEVEL.INFO, userDoesNotMeetConditionsLogMessage); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ | |
***************************************************************************/ | ||
|
||
var Optimizely = require('./'); | ||
var audienceEvaluator = require('../core/audience_evaluator'); | ||
var AudienceEvaluator = require('../core/audience_evaluator'); | ||
var bluebird = require('bluebird'); | ||
var bucketer = require('../core/bucketer'); | ||
var enums = require('../utils/enums'); | ||
|
@@ -238,6 +238,7 @@ describe('lib/optimizely', function() { | |
configObj: optlyInstance.configObj, | ||
userProfileService: userProfileServiceInstance, | ||
logger: createdLogger, | ||
__exploratoryConditionEvaluators: undefined | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is kind of a weird test assertion. It's probably fine, but I also think it makes sense for |
||
}); | ||
|
||
// Checking the second log message as the first one just says "Datafile is valid" | ||
|
@@ -262,6 +263,7 @@ describe('lib/optimizely', function() { | |
configObj: optlyInstance.configObj, | ||
userProfileService: null, | ||
logger: createdLogger, | ||
__exploratoryConditionEvaluators: undefined | ||
}); | ||
|
||
// Checking the second log message as the first one just says "Datafile is valid" | ||
|
@@ -3503,6 +3505,7 @@ describe('lib/optimizely', function() { | |
logToConsole: false, | ||
}); | ||
var optlyInstance; | ||
var audienceEvaluator; | ||
beforeEach(function() { | ||
optlyInstance = new Optimizely({ | ||
clientEngine: 'node-sdk', | ||
|
@@ -3514,6 +3517,7 @@ describe('lib/optimizely', function() { | |
logger: createdLogger, | ||
isValidInstance: true, | ||
}); | ||
audienceEvaluator = AudienceEvaluator.prototype; | ||
|
||
sandbox.stub(eventDispatcher, 'dispatchEvent'); | ||
sandbox.stub(errorHandler, 'handleError'); | ||
|
Uh oh!
There was an error while loading. Please reload this page.