Skip to content

feat(Audience Evaluation): Use log messages to explain the outcome of audience evaluation #210

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 10 commits into from
Feb 8, 2019
20 changes: 16 additions & 4 deletions packages/optimizely-sdk/lib/core/audience_evaluator/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2016, 2018 Optimizely
* Copyright 2016, 2018-2019 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,12 @@
*/
var conditionTreeEvaluator = require('../condition_tree_evaluator');
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator');
var enums = require('../../utils/enums');
var sprintf = require('sprintf-js').sprintf;

var LOG_LEVEL = enums.LOG_LEVEL;
var LOG_MESSAGES = enums.LOG_MESSAGES;
var MODULE_NAME = 'AUDIENCE_EVALUATOR';

module.exports = {
/**
Expand All @@ -27,10 +33,11 @@ module.exports = {
* 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) {
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;
Expand All @@ -41,14 +48,19 @@ module.exports = {
}

var evaluateConditionWithUserAttributes = function(condition) {
return customAttributeConditionEvaluator.evaluate(condition, userAttributes);
return customAttributeConditionEvaluator.evaluate(condition, userAttributes, logger);
};

var evaluateAudience = function(audienceId) {
var audience = audiencesById[audienceId];
if (audience) {
return conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes);
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();
Copy link
Contributor

Choose a reason for hiding this comment

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

result.toString() only works if result isn't null or undefined, but I guess we should be okay in this case. 👍

logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
return result;
}

return null;
};

Expand Down
115 changes: 96 additions & 19 deletions packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2016, 2018 Optimizely
* Copyright 2016, 2018-2019 Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,11 +15,14 @@
*/
var audienceEvaluator = require('./');
var chai = require('chai');
var sprintf = require('sprintf-js').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 enums = require('../../utils/enums');
var LOG_LEVEL = enums.LOG_LEVEL;

var chromeUserAudience = {
conditions: ['and', {
Expand Down Expand Up @@ -52,12 +55,22 @@ var audiencesById = {
describe('lib/core/audience_evaluator', function() {
describe('APIs', function() {
describe('evaluate', function() {
var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO});

beforeEach(function () {
sinon.stub(mockLogger, 'log');
});

afterEach(function() {
mockLogger.log.restore();
});

it('should return true if there are no audiences', function() {
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}));
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}, mockLogger));
});

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

it('should return true if any of the audience conditions are met', function() {
Expand All @@ -74,9 +87,9 @@ describe('lib/core/audience_evaluator', function() {
'device_model': 'iphone',
};

assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers));
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers));
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers));
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));
});

it('should return false if none of the audience conditions are met', function() {
Expand All @@ -93,21 +106,22 @@ describe('lib/core/audience_evaluator', function() {
'device_model': 'nexus5',
};

assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers));
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers));
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers));
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));
});

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));
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null, mockLogger));
});

describe('complex audience conditions', function() {
it('should return true if any of the audiences in an "OR" condition pass', function() {
var result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
{ browser_type: 'chrome' }
{ browser_type: 'chrome' },
mockLogger
);
assert.isTrue(result);
});
Expand All @@ -116,7 +130,8 @@ describe('lib/core/audience_evaluator', function() {
var result = audienceEvaluator.evaluate(
['and', '0', '1'],
audiencesById,
{ browser_type: 'chrome', device_model: 'iphone' }
{ browser_type: 'chrome', device_model: 'iphone' },
mockLogger
);
assert.isTrue(result);
});
Expand All @@ -125,7 +140,8 @@ describe('lib/core/audience_evaluator', function() {
var result = audienceEvaluator.evaluate(
['not', '1'],
audiencesById,
{ device_model: 'android' }
{ device_model: 'android' },
mockLogger
);
assert.isTrue(result);
});
Expand All @@ -149,7 +165,8 @@ describe('lib/core/audience_evaluator', function() {
var result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
{ browser_type: 'chrome' }
{ browser_type: 'chrome' },
mockLogger
);
assert.isTrue(result);
});
Expand All @@ -159,7 +176,8 @@ describe('lib/core/audience_evaluator', function() {
var result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
{ browser_type: 'safari' }
{ browser_type: 'safari' },
mockLogger
);
assert.isFalse(result);
});
Expand All @@ -169,7 +187,8 @@ describe('lib/core/audience_evaluator', function() {
var result = audienceEvaluator.evaluate(
['or', '0', '1'],
audiencesById,
{ state: 'California' }
{ state: 'California' },
mockLogger
);
assert.isFalse(result);
});
Expand All @@ -180,10 +199,68 @@ describe('lib/core/audience_evaluator', function() {
});
customAttributeConditionEvaluator.evaluate.returns(false);
var userAttributes = { device_model: 'android' };
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
assert.isFalse(result);
});
});

describe('Audience evaluation logging', function() {
var sandbox = sinon.sandbox.create();

beforeEach(function() {
sandbox.stub(conditionTreeEvaluator, 'evaluate');
sandbox.stub(customAttributeConditionEvaluator, 'evaluate');
});

afterEach(function() {
sandbox.restore();
});

it('logs correctly when conditionTreeEvaluator.evaluate returns null', function() {
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
return leafEvaluator(conditions[1]);
});
customAttributeConditionEvaluator.evaluate.returns(null);
var userAttributes = { device_model: 5.5 };
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
assert.isFalse(result);
assert.strictEqual(2, mockLogger.log.callCount);
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"}].');
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to UNKNOWN.');
});

it('logs correctly when conditionTreeEvaluator.evaluate returns true', function() {
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
return leafEvaluator(conditions[1]);
});
customAttributeConditionEvaluator.evaluate.returns(true);
var userAttributes = { device_model: 'iphone' };
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
assert.isTrue(result);
assert.strictEqual(2, mockLogger.log.callCount);
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"}].');
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to TRUE.');
});

it('logs correctly when conditionTreeEvaluator.evaluate returns false', function() {
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
return leafEvaluator(conditions[1]);
});
customAttributeConditionEvaluator.evaluate.returns(false);
var userAttributes = { device_model: 'android' };
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
assert.isFalse(result);
assert.strictEqual(2, mockLogger.log.callCount);
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"}].');
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to FALSE.');
});
});
});
Expand Down
Loading