Skip to content

Commit de83b9b

Browse files
mfahadahmednchilada
authored andcommitted
feat(Audience Evaluation): Use log messages to explain the outcome of audience evaluation (#210)
1 parent b8791d4 commit de83b9b

File tree

11 files changed

+643
-214
lines changed

11 files changed

+643
-214
lines changed

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

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2016, 2018 Optimizely
2+
* Copyright 2016, 2018-2019 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,12 @@
1515
*/
1616
var conditionTreeEvaluator = require('../condition_tree_evaluator');
1717
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator');
18+
var enums = require('../../utils/enums');
19+
var sprintf = require('sprintf-js').sprintf;
20+
21+
var LOG_LEVEL = enums.LOG_LEVEL;
22+
var LOG_MESSAGES = enums.LOG_MESSAGES;
23+
var MODULE_NAME = 'AUDIENCE_EVALUATOR';
1824

1925
module.exports = {
2026
/**
@@ -27,10 +33,11 @@ module.exports = {
2733
* should be full audience objects with conditions properties
2834
* @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions
2935
* are met. If not provided, defaults to an empty object
36+
* @param {Object} logger Logger instance.
3037
* @return {Boolean} true if the user attributes match the given audience conditions, false
3138
* otherwise
3239
*/
33-
evaluate: function(audienceConditions, audiencesById, userAttributes) {
40+
evaluate: function(audienceConditions, audiencesById, userAttributes, logger) {
3441
// if there are no audiences, return true because that means ALL users are included in the experiment
3542
if (!audienceConditions || audienceConditions.length === 0) {
3643
return true;
@@ -41,14 +48,19 @@ module.exports = {
4148
}
4249

4350
var evaluateConditionWithUserAttributes = function(condition) {
44-
return customAttributeConditionEvaluator.evaluate(condition, userAttributes);
51+
return customAttributeConditionEvaluator.evaluate(condition, userAttributes, logger);
4552
};
4653

4754
var evaluateAudience = function(audienceId) {
4855
var audience = audiencesById[audienceId];
4956
if (audience) {
50-
return conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes);
57+
logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions)));
58+
var result = conditionTreeEvaluator.evaluate(audience.conditions, evaluateConditionWithUserAttributes);
59+
var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase();
60+
logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
61+
return result;
5162
}
63+
5264
return null;
5365
};
5466

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

+96-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2016, 2018 Optimizely
2+
* Copyright 2016, 2018-2019 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,11 +15,14 @@
1515
*/
1616
var audienceEvaluator = require('./');
1717
var chai = require('chai');
18+
var sprintf = require('sprintf-js').sprintf;
1819
var conditionTreeEvaluator = require('../condition_tree_evaluator');
1920
var customAttributeConditionEvaluator = require('../custom_attribute_condition_evaluator');
2021
var sinon = require('sinon');
21-
2222
var assert = chai.assert;
23+
var logger = require('../../plugins/logger');
24+
var enums = require('../../utils/enums');
25+
var LOG_LEVEL = enums.LOG_LEVEL;
2326

2427
var chromeUserAudience = {
2528
conditions: ['and', {
@@ -52,12 +55,22 @@ var audiencesById = {
5255
describe('lib/core/audience_evaluator', function() {
5356
describe('APIs', function() {
5457
describe('evaluate', function() {
58+
var mockLogger = logger.createLogger({logLevel: LOG_LEVEL.INFO});
59+
60+
beforeEach(function () {
61+
sinon.stub(mockLogger, 'log');
62+
});
63+
64+
afterEach(function() {
65+
mockLogger.log.restore();
66+
});
67+
5568
it('should return true if there are no audiences', function() {
56-
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}));
69+
assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {}, mockLogger));
5770
});
5871

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

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

77-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers));
78-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers));
79-
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers));
90+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneUsers, mockLogger));
91+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, chromeUsers, mockLogger));
92+
assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, iphoneChromeUsers, mockLogger));
8093
});
8194

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

96-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers));
97-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers));
98-
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers));
109+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusUsers, mockLogger));
110+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, safariUsers, mockLogger));
111+
assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, nexusSafariUsers, mockLogger));
99112
});
100113

101114
it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() {
102-
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById));
115+
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null, mockLogger));
103116
});
104117

105118
describe('complex audience conditions', function() {
106119
it('should return true if any of the audiences in an "OR" condition pass', function() {
107120
var result = audienceEvaluator.evaluate(
108121
['or', '0', '1'],
109122
audiencesById,
110-
{ browser_type: 'chrome' }
123+
{ browser_type: 'chrome' },
124+
mockLogger
111125
);
112126
assert.isTrue(result);
113127
});
@@ -116,7 +130,8 @@ describe('lib/core/audience_evaluator', function() {
116130
var result = audienceEvaluator.evaluate(
117131
['and', '0', '1'],
118132
audiencesById,
119-
{ browser_type: 'chrome', device_model: 'iphone' }
133+
{ browser_type: 'chrome', device_model: 'iphone' },
134+
mockLogger
120135
);
121136
assert.isTrue(result);
122137
});
@@ -125,7 +140,8 @@ describe('lib/core/audience_evaluator', function() {
125140
var result = audienceEvaluator.evaluate(
126141
['not', '1'],
127142
audiencesById,
128-
{ device_model: 'android' }
143+
{ device_model: 'android' },
144+
mockLogger
129145
);
130146
assert.isTrue(result);
131147
});
@@ -149,7 +165,8 @@ describe('lib/core/audience_evaluator', function() {
149165
var result = audienceEvaluator.evaluate(
150166
['or', '0', '1'],
151167
audiencesById,
152-
{ browser_type: 'chrome' }
168+
{ browser_type: 'chrome' },
169+
mockLogger
153170
);
154171
assert.isTrue(result);
155172
});
@@ -159,7 +176,8 @@ describe('lib/core/audience_evaluator', function() {
159176
var result = audienceEvaluator.evaluate(
160177
['or', '0', '1'],
161178
audiencesById,
162-
{ browser_type: 'safari' }
179+
{ browser_type: 'safari' },
180+
mockLogger
163181
);
164182
assert.isFalse(result);
165183
});
@@ -169,7 +187,8 @@ describe('lib/core/audience_evaluator', function() {
169187
var result = audienceEvaluator.evaluate(
170188
['or', '0', '1'],
171189
audiencesById,
172-
{ state: 'California' }
190+
{ state: 'California' },
191+
mockLogger
173192
);
174193
assert.isFalse(result);
175194
});
@@ -180,10 +199,68 @@ describe('lib/core/audience_evaluator', function() {
180199
});
181200
customAttributeConditionEvaluator.evaluate.returns(false);
182201
var userAttributes = { device_model: 'android' };
183-
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes);
202+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
203+
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
204+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
205+
assert.isFalse(result);
206+
});
207+
});
208+
209+
describe('Audience evaluation logging', function() {
210+
var sandbox = sinon.sandbox.create();
211+
212+
beforeEach(function() {
213+
sandbox.stub(conditionTreeEvaluator, 'evaluate');
214+
sandbox.stub(customAttributeConditionEvaluator, 'evaluate');
215+
});
216+
217+
afterEach(function() {
218+
sandbox.restore();
219+
});
220+
221+
it('logs correctly when conditionTreeEvaluator.evaluate returns null', function() {
222+
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
223+
return leafEvaluator(conditions[1]);
224+
});
225+
customAttributeConditionEvaluator.evaluate.returns(null);
226+
var userAttributes = { device_model: 5.5 };
227+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
228+
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
229+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
230+
assert.isFalse(result);
231+
assert.strictEqual(2, mockLogger.log.callCount);
232+
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"}].');
233+
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to UNKNOWN.');
234+
});
235+
236+
it('logs correctly when conditionTreeEvaluator.evaluate returns true', function() {
237+
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
238+
return leafEvaluator(conditions[1]);
239+
});
240+
customAttributeConditionEvaluator.evaluate.returns(true);
241+
var userAttributes = { device_model: 'iphone' };
242+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
243+
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
244+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
245+
assert.isTrue(result);
246+
assert.strictEqual(2, mockLogger.log.callCount);
247+
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"}].');
248+
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to TRUE.');
249+
});
250+
251+
it('logs correctly when conditionTreeEvaluator.evaluate returns false', function() {
252+
conditionTreeEvaluator.evaluate.callsFake(function(conditions, leafEvaluator) {
253+
return leafEvaluator(conditions[1]);
254+
});
255+
customAttributeConditionEvaluator.evaluate.returns(false);
256+
var userAttributes = { device_model: 'android' };
257+
var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes, mockLogger);
184258
sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate);
185-
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes);
259+
sinon.assert.calledWithExactly(customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], userAttributes, mockLogger);
186260
assert.isFalse(result);
261+
assert.strictEqual(2, mockLogger.log.callCount);
262+
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"}].');
263+
assert.strictEqual(mockLogger.log.args[1][1], 'AUDIENCE_EVALUATOR: Audience "1" evaluated to FALSE.');
187264
});
188265
});
189266
});

0 commit comments

Comments
 (0)