diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/index.js b/packages/optimizely-sdk/lib/core/audience_evaluator/index.js index 419086bbe..8ec9bb7d5 100644 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.js +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/index.js @@ -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('@optimizely/js-sdk-utils').sprintf; +var logging = require('@optimizely/js-sdk-logging'); +var logger = logging.getLogger(); +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 given options + * @param {Object=} UNSTABLE_conditionEvaluators 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(UNSTABLE_conditionEvaluators) { + this.typeToEvaluatorMap = fns.assignIn({}, UNSTABLE_conditionEvaluators, { + '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; + } - 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) { + 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(); + 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) { + logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition))); + return null; + } + try { + return evaluator.evaluate(condition, userAttributes, logger); + } catch (err) { + logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.CONDITION_EVALUATOR_ERROR, MODULE_NAME, condition.type, err.message)); + } + return null; }; + +module.exports = AudienceEvaluator; diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js b/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js index 7ea283b90..76076cdd3 100644 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var audienceEvaluator = require('./'); +var AudienceEvaluator = require('./'); var chai = require('chai'); -var sprintf = require('@optimizely/js-sdk-utils').sprintf; var conditionTreeEvaluator = require('../condition_tree_evaluator'); var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator'); var sinon = require('sinon'); var assert = chai.assert; -var logger = require('../../plugins/logger'); +var logging = require('@optimizely/js-sdk-logging'); +var mockLogger = logging.getLogger(); var enums = require('../../utils/enums'); var LOG_LEVEL = enums.LOG_LEVEL; @@ -53,11 +53,14 @@ var audiencesById = { }; describe('lib/core/audience_evaluator', function() { + var audienceEvaluator; + beforeEach(function() { + audienceEvaluator = new AudienceEvaluator(); + }); + describe('APIs', function() { describe('evaluate', function() { - var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO}); - - beforeEach(function () { + beforeEach(function() { sinon.stub(mockLogger, 'log'); }); @@ -66,11 +69,11 @@ describe('lib/core/audience_evaluator', function() { }); it('should return true if there are no audiences', function() { - assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}, mockLogger)); + assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {})); }); it('should return false if there are audiences but no attributes', function() { - assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {}, mockLogger)); + assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {})); }); it('should return true if any of the audience conditions are met', function() { @@ -87,9 +90,9 @@ describe('lib/core/audience_evaluator', function() { 'device_model': 'iphone', }; - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers, mockLogger)); - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers, mockLogger)); - assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers, mockLogger)); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers)); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers)); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers)); }); it('should return false if none of the audience conditions are met', function() { @@ -106,13 +109,13 @@ describe('lib/core/audience_evaluator', function() { 'device_model': 'nexus5', }; - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers, mockLogger)); - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers, mockLogger)); - assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers, mockLogger)); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers)); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers)); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers)); }); it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() { - assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null, mockLogger)); + assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null)); }); describe('complex audience conditions', function() { @@ -120,8 +123,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['or', '0', '1'], audiencesById, - { browser_type: 'chrome' }, - mockLogger + { browser_type: 'chrome' } ); assert.isTrue(result); }); @@ -130,8 +132,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['and', '0', '1'], audiencesById, - { browser_type: 'chrome', device_model: 'iphone' }, - mockLogger + { browser_type: 'chrome', device_model: 'iphone' } ); assert.isTrue(result); }); @@ -140,8 +141,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['not', '1'], audiencesById, - { device_model: 'android' }, - mockLogger + { device_model: 'android' } ); assert.isTrue(result); }); @@ -165,8 +165,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['or', '0', '1'], audiencesById, - { browser_type: 'chrome' }, - mockLogger + { browser_type: 'chrome' } ); assert.isTrue(result); }); @@ -176,8 +175,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['or', '0', '1'], audiencesById, - { browser_type: 'safari' }, - mockLogger + { browser_type: 'safari' } ); assert.isFalse(result); }); @@ -187,8 +185,7 @@ describe('lib/core/audience_evaluator', function() { var result = audienceEvaluator.evaluate( ['or', '0', '1'], audiencesById, - { state: 'California' }, - mockLogger + { state: 'California' } ); assert.isFalse(result); }); @@ -199,8 +196,9 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(false); var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); + console.log('args: ', customAttributeConditionEvaluator.evaluate.firstCall.args) sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); assert.isFalse(result); }); @@ -224,7 +222,7 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(null); var userAttributes = { device_model: 5.5 }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); assert.isFalse(result); @@ -239,7 +237,7 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(true); var userAttributes = { device_model: 'iphone' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); assert.isTrue(result); @@ -254,8 +252,9 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(false); var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); + console.log('args: ', customAttributeConditionEvaluator.evaluate.firstCall.args) sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger); assert.isFalse(result); assert.strictEqual(2, mockLogger.log.callCount); diff --git a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js index 8f83208c3..8b60fa509 100644 --- a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js +++ b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.js @@ -53,13 +53,9 @@ EVALUATORS_BY_MATCH_TYPE[SUBSTRING_MATCH_TYPE] = substringEvaluator; * @param {Object} logger * @return {?Boolean} true/false if the given user attributes match/don't match the given condition, * null if the given user attributes and condition can't be evaluated + * TODO: Change to accept and object with named properties */ 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; - } - var conditionMatch = condition.match; if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_MATCH_TYPE, MODULE_NAME, JSON.stringify(condition))); diff --git a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js index 9a1e7f4ef..417951487 100644 --- a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js +++ b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.tests.js @@ -85,30 +85,6 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { assert.isTrue(customAttributeEvaluator.evaluate(doubleCondition, userAttributes, mockLogger)); }); - it('should log and return null when condition has an invalid type property', function() { - var result = customAttributeEvaluator.evaluate( - { match: 'exact', name: 'weird_condition', type: 'weird', value: 'hi' }, - { weird_condition: 'bye' }, - mockLogger - ); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"weird_condition","type":"weird","value":"hi"} has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.'); - }); - - it('should log and return null when condition has no type property', function() { - var result = customAttributeEvaluator.evaluate( - { match: 'exact', name: 'weird_condition', value: 'hi' }, - { weird_condition: 'bye' }, - mockLogger - ); - assert.isNull(result); - sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.WARNING, - 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"weird_condition","value":"hi"} has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.'); - }); - it('should log and return null when condition has an invalid match property', function() { var result = customAttributeEvaluator.evaluate( { match: 'weird', name: 'weird_condition', type: 'custom_attribute', value: 'hi' }, @@ -213,7 +189,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var result = customAttributeEvaluator.evaluate(exactStringCondition, {}, mockLogger); assert.isNull(result); sinon.assert.calledOnce(mockLogger.log); - sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, + sinon.assert.calledWithExactly(mockLogger.log, LOG_LEVEL.DEBUG, 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"exact","name":"favorite_constellation","type":"custom_attribute","value":"Lacerta"} evaluated to UNKNOWN because no value was passed for user attribute "favorite_constellation".'); }); @@ -247,10 +223,10 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided value is of a different type than the condition value', function() { var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 'yes' }, mockLogger); assert.isNull(result); - + result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: '1000' }, mockLogger); assert.isNull(result); - + assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); assert.strictEqual(mockLogger.log.args[0][1], @@ -263,10 +239,10 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided number value is out of bounds', function() { var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: -Infinity }, mockLogger); assert.isNull(result); - + result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: -Math.pow(2, 53) - 2 }, mockLogger); assert.isNull(result); - + assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); assert.strictEqual(mockLogger.log.args[0][1], @@ -537,7 +513,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { meters_travelled: Math.pow(2, 53) + 2, }, mockLogger); assert.isNull(result); - + assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][0], LOG_LEVEL.WARNING); assert.strictEqual(mockLogger.log.args[0][1], @@ -581,7 +557,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { sinon.assert.calledThrice(mockLogger.log); var logMessage = mockLogger.log.args[2][1]; - assert.strictEqual(logMessage, + assert.strictEqual(logMessage, 'CUSTOM_ATTRIBUTE_CONDITION_EVALUATOR: Audience condition {"match":"lt","name":"meters_travelled","type":"custom_attribute","value":9007199254740994} evaluated to UNKNOWN because the condition value is not supported.'); }); }); diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index 45b5e2c4e..574cf6df8 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -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'); @@ -45,13 +45,14 @@ var DECISION_SOURCES = enums.DECISION_SOURCES; * @constructor * @param {Object} options * @param {Object} options.userProfileService An instance of the user profile service for sticky bucketing. - * @param {Object} options.logger An instance of a logger to log messages with. + * @param {Object} options.logger An instance of a logger to log messages. * @returns {Object} */ function DecisionService(options) { - this.userProfileService = options.userProfileService || null; - this.logger = options.logger; + this.audienceEvaluator = new AudienceEvaluator(options.UNSTABLE_conditionEvaluators); this.forcedVariationMap = {}; + this.logger = options.logger; + this.userProfileService = options.userProfileService || null; } /** @@ -171,7 +172,7 @@ DecisionService.prototype.__checkIfUserIsInAudience = function(configObj, experi var experimentAudienceConditions = projectConfig.getExperimentAudienceConditions(configObj, experimentKey); var audiencesById = projectConfig.getAudiencesById(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) { diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js index 5e46493e1..beed5bade 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js @@ -28,7 +28,7 @@ var sprintf = require('@optimizely/js-sdk-utils').sprintf; var testData = require('../../tests/test_data').getTestProjectConfig(); var testDataWithFeatures = require('../../tests/test_data').getTestProjectConfigWithFeatures(); var jsonSchemaValidator = require('../../utils/json_schema_validator'); -var audienceEvaluator = require('../audience_evaluator'); +var AudienceEvaluator = require('../audience_evaluator'); var chai = require('chai'); var sinon = require('sinon'); @@ -75,11 +75,11 @@ describe('lib/core/decision_service', function() { it('should return null if the user does not meet audience conditions', function () { assert.isNull(decisionServiceInstance.getVariation(configObj, 'testExperimentWithAudiences', 'user3', {foo: 'bar'})); - assert.strictEqual(7, mockLogger.log.callCount); + assert.strictEqual(4, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: User user3 is not in the forced variation map.'); assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[5][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[6][1], 'DECISION_SERVICE: User user3 does not meet conditions to be in experiment testExperimentWithAudiences.'); + assert.strictEqual(mockLogger.log.args[2][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); + assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: User user3 does not meet conditions to be in experiment testExperimentWithAudiences.'); }); it('should return null if the experiment is not running', function () { @@ -420,7 +420,7 @@ describe('lib/core/decision_service', function() { var __audienceEvaluateSpy; beforeEach(function() { - __audienceEvaluateSpy = sinon.spy(audienceEvaluator, 'evaluate'); + __audienceEvaluateSpy = sinon.spy(AudienceEvaluator.prototype, 'evaluate'); }); afterEach(function() { @@ -429,9 +429,9 @@ describe('lib/core/decision_service', function() { it('should return true when audience conditions are met', function () { assert.isTrue(decisionServiceInstance.__checkIfUserIsInAudience(configObj, 'testExperimentWithAudiences', 'testUser', {browser_type: 'firefox'})); - assert.strictEqual(4, mockLogger.log.callCount); + assert.strictEqual(2, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to TRUE.'); + assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to TRUE.'); }); it('should return true when experiment has no audience', function () { @@ -447,20 +447,20 @@ describe('lib/core/decision_service', function() { assert.isFalse(decisionServiceInstance.__checkIfUserIsInAudience(configObj, 'testExperimentWithAudiences', 'testUser')); assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); - assert.strictEqual(6, mockLogger.log.callCount); + assert.strictEqual(3, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[4][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[5][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); + assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); + assert.strictEqual(mockLogger.log.args[2][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); }); it('should return false when audience conditions are not met', function () { assert.isFalse(decisionServiceInstance.__checkIfUserIsInAudience(configObj, 'testExperimentWithAudiences', 'testUser', {browser_type: 'chrome'})); assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false)); - assert.strictEqual(5, mockLogger.log.callCount); + assert.strictEqual(3, mockLogger.log.callCount); assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'); - assert.strictEqual(mockLogger.log.args[3][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); - assert.strictEqual(mockLogger.log.args[4][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); + assert.strictEqual(mockLogger.log.args[1][1], 'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'); + assert.strictEqual(mockLogger.log.args[2][1], 'DECISION_SERVICE: User testUser does not meet conditions to be in experiment testExperimentWithAudiences.'); }); }); diff --git a/packages/optimizely-sdk/lib/optimizely/index.js b/packages/optimizely-sdk/lib/optimizely/index.js index 0bded5ca2..08dbfa4db 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.js +++ b/packages/optimizely-sdk/lib/optimizely/index.js @@ -100,6 +100,7 @@ function Optimizely(config) { this.decisionService = decisionService.createDecisionService({ userProfileService: userProfileService, logger: this.logger, + UNSTABLE_conditionEvaluators: config.UNSTABLE_conditionEvaluators }); this.notificationCenter = notificationCenter.createNotificationCenter({ diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 81fe1fda0..28597c194 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -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 projectConfigManager = require('../core/project_config/project_config_manager'); @@ -167,6 +167,7 @@ describe('lib/optimizely', function() { sinon.assert.calledWith(decisionService.createDecisionService, { userProfileService: userProfileServiceInstance, logger: createdLogger, + UNSTABLE_conditionEvaluators: undefined, }); var logMessage = createdLogger.log.args[0][1]; @@ -189,6 +190,7 @@ describe('lib/optimizely', function() { sinon.assert.calledWith(decisionService.createDecisionService, { userProfileService: null, logger: createdLogger, + UNSTABLE_conditionEvaluators: undefined, }); var logMessage = createdLogger.log.args[0][1]; @@ -4363,6 +4365,7 @@ describe('lib/optimizely', function() { logToConsole: false, }); var optlyInstance; + var audienceEvaluator; beforeEach(function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', @@ -4374,6 +4377,7 @@ describe('lib/optimizely', function() { logger: createdLogger, isValidInstance: true, }); + audienceEvaluator = AudienceEvaluator.prototype; sandbox.stub(eventDispatcher, 'dispatchEvent'); sandbox.stub(errorHandler, 'handleError'); @@ -4405,8 +4409,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Welcome to Slytherin!', lasers: 45.5 }, - createdLogger + { house: 'Welcome to Slytherin!', lasers: 45.5 } ); }); @@ -4423,8 +4426,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Hufflepuff', lasers: 45.5 }, - createdLogger + { house: 'Hufflepuff', lasers: 45.5 } ); }); @@ -4457,8 +4459,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().rollouts[2].experiments[0].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: '...Slytherinnn...sss.', favorite_ice_cream: 'matcha' }, - createdLogger + { house: '...Slytherinnn...sss.', favorite_ice_cream: 'matcha' } ); }); @@ -4473,8 +4474,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().rollouts[2].experiments[0].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Lannister' }, - createdLogger + { house: 'Lannister' } ); }); @@ -4490,8 +4490,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Gryffindor', lasers: 700 }, - createdLogger + { house: 'Gryffindor', lasers: 700 } ); }); @@ -4504,8 +4503,7 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - {}, - createdLogger + {} ); }); }); diff --git a/packages/optimizely-sdk/lib/utils/enums/index.js b/packages/optimizely-sdk/lib/utils/enums/index.js index 07874507a..93de70a71 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.js +++ b/packages/optimizely-sdk/lib/utils/enums/index.js @@ -26,6 +26,7 @@ exports.LOG_LEVEL = { }; exports.ERROR_MESSAGES = { + CONDITION_EVALUATOR_ERROR: '%s: Error evaluating audience condition of type %s: %s', DATAFILE_AND_SDK_KEY_MISSING: '%s: You must provide at least one of sdkKey or datafile. Cannot start Optimizely', EXPERIMENT_KEY_NOT_IN_DATAFILE: '%s: Experiment key %s is not in datafile.', FEATURE_NOT_IN_DATAFILE: '%s: Feature key %s is not in datafile.', diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 89ba42855..aee773f58 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -11,7 +11,7 @@ "test-umdbrowser": "npm run build-browser-umd && karma start karma.umd.conf.js --single-run", "build-browser-umd": "rm -rf dist && webpack", "test-ci": "npm run test-xbrowser && npm run test-umdbrowser", - "lint": "eslint lib/**", + "lint": "eslint 'lib/**/*.js'", "cover": "istanbul cover _mocha ./lib/*.tests.js ./lib/**/*.tests.js ./lib/**/**/*tests.js", "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls", "prepublishOnly": "npm run build-browser-umd && npm test && npm run test-xbrowser && npm run test-umdbrowser"