Skip to content

Commit 0112ff9

Browse files
author
James Fox
committed
updates per code review
1 parent 2ae49e1 commit 0112ff9

File tree

7 files changed

+65
-47
lines changed

7 files changed

+65
-47
lines changed

packages/optimizely-sdk/lib/core/audience_evaluator/index.js

+40-19
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,29 @@
1515
*/
1616
var conditionTreeEvaluator = require('../condition_tree_evaluator');
1717
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator');
18-
var ERROR_MESSAGES = require('../../utils/enums').ERROR_MESSAGES;
1918
var enums = require('../../utils/enums');
19+
var fns = require('../../utils/fns');
2020
var sprintf = require('sprintf-js').sprintf;
2121

22+
var ERROR_MESSAGES = enums.ERROR_MESSAGES;
2223
var LOG_LEVEL = enums.LOG_LEVEL;
2324
var LOG_MESSAGES = enums.LOG_MESSAGES;
2425
var MODULE_NAME = 'AUDIENCE_EVALUATOR';
2526

2627

27-
function AudienceEvaluator(conditionEvaluators) {
28-
this.typeToEvaluatorMap = Object.assign({
28+
/**
29+
* Construct an instance of AudienceEvaluator with a given logger and options
30+
* @param {Logger} logger The Logger instance
31+
* @param {Object=} __exploratoryConditionEvaluators A map of condition evaluators provided by the consumer. This enables matching
32+
* condition types which are not supported natively by the SDK. Note that built in
33+
* Optimizely evaluators cannot be overridden.
34+
* @constructor
35+
*/
36+
function AudienceEvaluator(logger, __exploratoryConditionEvaluators) {
37+
this.logger = logger;
38+
this.typeToEvaluatorMap = fns.assignIn({}, __exploratoryConditionEvaluators, {
2939
'custom_attribute': customAttributeConditionEvaluator
30-
}, conditionEvaluators);
40+
});
3141
}
3242

3343
/**
@@ -40,11 +50,10 @@ function AudienceEvaluator(conditionEvaluators) {
4050
* should be full audience objects with conditions properties
4151
* @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions
4252
* are met. If not provided, defaults to an empty object
43-
* @param {Object} logger Logger instance.
4453
* @return {Boolean} true if the user attributes match the given audience conditions, false
4554
* otherwise
4655
*/
47-
AudienceEvaluator.prototype.evaluate = function(audienceConditions, audiencesById, userAttributes, logger) {
56+
AudienceEvaluator.prototype.evaluate = function(audienceConditions, audiencesById, userAttributes) {
4857
// if there are no audiences, return true because that means ALL users are included in the experiment
4958
if (!audienceConditions || audienceConditions.length === 0) {
5059
return true;
@@ -54,29 +63,41 @@ AudienceEvaluator.prototype.evaluate = function(audienceConditions, audiencesByI
5463
userAttributes = {};
5564
}
5665

57-
var evaluateConditionWithUserAttributes = function(condition) {
58-
var evaluator = this.typeToEvaluatorMap[condition.type];
59-
if (!evaluator) {
60-
logger.log(LOG_LEVEL.WARNING, sprintf(ERROR_MESSAGES.AUDIENCE_EVALUATOR_EXPECTED, MODULE_NAME, condition.type));
61-
return false;
62-
}
63-
return evaluator.evaluate(condition, userAttributes, logger);
64-
}.bind(this);
65-
6666
var evaluateAudience = function(audienceId) {
6767
var audience = audiencesById[audienceId];
6868
if (audience) {
69-
logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions)));
70-
var result = conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes);
69+
this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions)));
70+
var result = conditionTreeEvaluator.evaluate(audience.conditions, this.evaluateConditionWithUserAttributes.bind(this, userAttributes));
7171
var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase();
72-
logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
72+
this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
7373
return result;
7474
}
7575

7676
return null;
77-
};
77+
}.bind(this);
7878

7979
return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false;
8080
};
8181

82+
/**
83+
* Wrapper around evaluator.evaluate that is passed to the conditionTreeEvaluator.
84+
* Evaluates the condition provided given the user attributes if an evaluator has been defined for the condition type.
85+
* @param {Object} userAttributes A map of user attributes.
86+
* @param {Object} condition A single condition object to evaluate.
87+
* @return {Boolean|null} true if the condition is satisfied, null if a matcher is not found.
88+
*/
89+
AudienceEvaluator.prototype.evaluateConditionWithUserAttributes = function(userAttributes, condition) {
90+
var evaluator = this.typeToEvaluatorMap[condition.type];
91+
if (!evaluator) {
92+
this.logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)));
93+
return null;
94+
}
95+
try {
96+
return evaluator.evaluate(condition, userAttributes, this.logger);
97+
} catch (err) {
98+
this.logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.CONDITION_EVALUATOR_ERROR, MODULE_NAME, condition.type, err.message));
99+
}
100+
return null;
101+
};
102+
82103
module.exports = AudienceEvaluator;

packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js

+19-21
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
var AudienceEvaluator = require('./');
1717
var chai = require('chai');
18-
var sprintf = require('sprintf-js').sprintf;
1918
var conditionTreeEvaluator = require('../condition_tree_evaluator');
2019
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator');
2120
var sinon = require('sinon');
@@ -54,14 +53,13 @@ var audiencesById = {
5453

5554
describe('lib/core/audience_evaluator', function() {
5655
var audienceEvaluator;
56+
var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO});
5757
beforeEach(function() {
58-
audienceEvaluator = new AudienceEvaluator();
58+
audienceEvaluator = new AudienceEvaluator(mockLogger);
5959
});
6060

6161
describe('APIs', function() {
6262
describe('evaluate', function() {
63-
var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO});
64-
6563
beforeEach(function () {
6664
sinon.stub(mockLogger, 'log');
6765
});
@@ -71,11 +69,11 @@ describe('lib/core/audience_evaluator', function() {
7169
});
7270

7371
it('should return true if there are no audiences', function() {
74-
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}, mockLogger));
72+
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}));
7573
});
7674

7775
it('should return false if there are audiences but no attributes', function() {
78-
assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {}, mockLogger));
76+
assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {}));
7977
});
8078

8179
it('should return true if any of the audience conditions are met', function() {
@@ -92,9 +90,9 @@ describe('lib/core/audience_evaluator', function() {
9290
'device_model': 'iphone',
9391
};
9492

95-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers, mockLogger));
96-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers, mockLogger));
97-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers, mockLogger));
93+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers));
94+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers));
95+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers));
9896
});
9997

10098
it('should return false if none of the audience conditions are met', function() {
@@ -111,13 +109,13 @@ describe('lib/core/audience_evaluator', function() {
111109
'device_model': 'nexus5',
112110
};
113111

114-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers, mockLogger));
115-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers, mockLogger));
116-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers, mockLogger));
112+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers));
113+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers));
114+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers));
117115
});
118116

119117
it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() {
120-
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null, mockLogger));
118+
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null));
121119
});
122120

123121
describe('complex audience conditions', function() {
@@ -204,9 +202,9 @@ describe('lib/core/audience_evaluator', function() {
204202
});
205203
customAttributeConditionEvaluator.evaluate.returns(false);
206204
var userAttributes = { device_model: 'android' };
207-
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
205+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
208206
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
209-
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
207+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
210208
assert.isFalse(result);
211209
});
212210
});
@@ -229,9 +227,9 @@ describe('lib/core/audience_evaluator', function() {
229227
});
230228
customAttributeConditionEvaluator.evaluate.returns(null);
231229
var userAttributes = { device_model: 5.5 };
232-
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
230+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
233231
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
234-
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
232+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
235233
assert.isFalse(result);
236234
assert.strictEqual(2, mockLogger.log.callCount);
237235
assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].');
@@ -244,9 +242,9 @@ describe('lib/core/audience_evaluator', function() {
244242
});
245243
customAttributeConditionEvaluator.evaluate.returns(true);
246244
var userAttributes = { device_model: 'iphone' };
247-
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
245+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
248246
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
249-
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
247+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
250248
assert.isTrue(result);
251249
assert.strictEqual(2, mockLogger.log.callCount);
252250
assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].');
@@ -259,9 +257,9 @@ describe('lib/core/audience_evaluator', function() {
259257
});
260258
customAttributeConditionEvaluator.evaluate.returns(false);
261259
var userAttributes = { device_model: 'android' };
262-
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
260+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
263261
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
264-
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
262+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
265263
assert.isFalse(result);
266264
assert.strictEqual(2, mockLogger.log.callCount);
267265
assert.strictEqual(mockLogger.log.args[0][1], 'AUDIENCE_EVALUATOR: Starting to evaluate audience "1" with conditions: ["and",{"name":"device_model","value":"iphone","type":"custom_attribute"}].');

packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator;
5656
*/
5757
function evaluate(condition, userAttributes, logger) {
5858
if (condition.type !== CUSTOM_ATTRIBUTE_CONDITION_TYPE) {
59-
logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)));
6059
return null;
6160
}
6261

packages/optimizely-sdk/lib/core/decision_service/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function DecisionService(options) {
5252
this.configObj = options.configObj;
5353
this.userProfileService = options.userProfileService || null;
5454
this.logger = options.logger;
55-
this.audienceEvaluator = new AudienceEvaluator(options.conditionEvaluators);
55+
this.audienceEvaluator = new AudienceEvaluator(options.logger, options.__exploratoryConditionEvaluators);
5656
}
5757

5858
/**
@@ -169,7 +169,7 @@ DecisionService.prototype.__checkIfUserIsInAudience = function(experimentKey, us
169169
var experimentAudienceConditions = projectConfig.getExperimentAudienceConditions(this.configObj, experimentKey);
170170
var audiencesById = projectConfig.getAudiencesById(this.configObj);
171171
this.logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCES_COMBINED, MODULE_NAME, experimentKey, JSON.stringify(experimentAudienceConditions)));
172-
var result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes, this.logger);
172+
var result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes);
173173
this.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT_COMBINED, MODULE_NAME, experimentKey, result.toString().toUpperCase()));
174174

175175
if (!result) {

packages/optimizely-sdk/lib/optimizely/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function Optimizely(config) {
9898
configObj: this.configObj,
9999
userProfileService: userProfileService,
100100
logger: this.logger,
101-
conditionEvaluators: config.conditionEvaluators
101+
__exploratoryConditionEvaluators: config.__exploratoryConditionEvaluators
102102
});
103103

104104
this.notificationCenter = notificationCenter.createNotificationCenter({

packages/optimizely-sdk/lib/optimizely/index.tests.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ describe('lib/optimizely', function() {
238238
configObj: optlyInstance.configObj,
239239
userProfileService: userProfileServiceInstance,
240240
logger: createdLogger,
241-
conditionEvaluators: undefined
241+
__exploratoryConditionEvaluators: undefined
242242
});
243243

244244
// Checking the second log message as the first one just says "Datafile is valid"
@@ -263,7 +263,7 @@ describe('lib/optimizely', function() {
263263
configObj: optlyInstance.configObj,
264264
userProfileService: null,
265265
logger: createdLogger,
266-
conditionEvaluators: undefined
266+
__exploratoryConditionEvaluators: undefined
267267
});
268268

269269
// Checking the second log message as the first one just says "Datafile is valid"

packages/optimizely-sdk/lib/utils/enums/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ exports.LOG_LEVEL = {
2626
};
2727

2828
exports.ERROR_MESSAGES = {
29-
AUDIENCE_EVALUATOR_EXPECTED: '%s: No audience evaluator present for condition type: %s.',
29+
CONDITION_EVALUATOR_ERROR: '%s: Error evaluating condition type %s: %s',
3030
EXPERIMENT_KEY_NOT_IN_DATAFILE: '%s: Experiment key %s is not in datafile.',
3131
FEATURE_NOT_IN_DATAFILE: '%s: Feature key %s is not in datafile.',
3232
IMPROPERLY_FORMATTED_EXPERIMENT: '%s: Experiment key %s is improperly formatted.',

0 commit comments

Comments
 (0)