Skip to content

Commit 1b8557a

Browse files
authored
feat: Convert audience_evaluator module to TS (#672)
* Initial conversion + cleanup * Define conditions type * Incorporate comment
1 parent bec6f26 commit 1b8557a

File tree

7 files changed

+147
-181
lines changed

7 files changed

+147
-181
lines changed

packages/optimizely-sdk/lib/core/audience_evaluator/index.d.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

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

Lines changed: 0 additions & 117 deletions
This file was deleted.

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

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('lib/core/audience_evaluator', function() {
135135
});
136136

137137
it('should return true if no attributes are passed and the audience conditions evaluate to true in the absence of attributes', function() {
138-
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, null));
138+
assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById));
139139
});
140140

141141
describe('complex audience conditions', function() {
@@ -200,7 +200,6 @@ describe('lib/core/audience_evaluator', function() {
200200
customAttributeConditionEvaluator.evaluate,
201201
iphoneUserAudience.conditions[1],
202202
userAttributes,
203-
mockLogger
204203
);
205204
assert.isFalse(result);
206205
});
@@ -230,7 +229,6 @@ describe('lib/core/audience_evaluator', function() {
230229
customAttributeConditionEvaluator.evaluate,
231230
iphoneUserAudience.conditions[1],
232231
userAttributes,
233-
mockLogger
234232
);
235233
assert.isFalse(result);
236234
assert.strictEqual(2, mockLogger.log.callCount);
@@ -253,7 +251,6 @@ describe('lib/core/audience_evaluator', function() {
253251
customAttributeConditionEvaluator.evaluate,
254252
iphoneUserAudience.conditions[1],
255253
userAttributes,
256-
mockLogger
257254
);
258255
assert.isTrue(result);
259256
assert.strictEqual(2, mockLogger.log.callCount);
@@ -276,7 +273,6 @@ describe('lib/core/audience_evaluator', function() {
276273
customAttributeConditionEvaluator.evaluate,
277274
iphoneUserAudience.conditions[1],
278275
userAttributes,
279-
mockLogger
280276
);
281277
assert.isFalse(result);
282278
assert.strictEqual(2, mockLogger.log.callCount);
@@ -298,9 +294,8 @@ describe('lib/core/audience_evaluator', function() {
298294
};
299295
audienceEvaluator = createAudienceEvaluator({
300296
special_condition_type: {
301-
evaluate: function(condition, userAttributes, logger) {
297+
evaluate: function(condition, userAttributes) {
302298
const result = mockEnvironment[condition.value] && userAttributes[condition.match] > 0;
303-
logger.log(`special_condition_type: ${result} for ${condition.value}:${condition.match}`);
304299
return result;
305300
},
306301
},
@@ -311,13 +306,6 @@ describe('lib/core/audience_evaluator', function() {
311306
assert.isFalse(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 0 }));
312307
assert.isTrue(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 1 }));
313308
});
314-
315-
it('should pass the logger instance to the custom condition evaluator', function() {
316-
assert.isFalse(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 0 }));
317-
assert.isTrue(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 1 }));
318-
sinon.assert.calledWithExactly(mockLogger.log, 'special_condition_type: false for special:interest_level');
319-
sinon.assert.calledWithExactly(mockLogger.log, 'special_condition_type: true for special:interest_level');
320-
});
321309
});
322310

323311
describe('when passing an invalid additional evaluator', function() {
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2016, 2018-2021, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { sprintf } from '@optimizely/js-sdk-utils';
17+
import { getLogger } from '@optimizely/js-sdk-logging';
18+
19+
import fns from '../../utils/fns';
20+
import {
21+
LOG_LEVEL,
22+
LOG_MESSAGES,
23+
ERROR_MESSAGES,
24+
} from '../../utils/enums';
25+
import * as conditionTreeEvaluator from '../condition_tree_evaluator';
26+
import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator';
27+
import { UserAttributes, Audience, Condition } from '../../shared_types';
28+
29+
const logger = getLogger();
30+
const MODULE_NAME = 'AUDIENCE_EVALUATOR';
31+
32+
export class AudienceEvaluator {
33+
private typeToEvaluatorMap: {
34+
[key: string]: {
35+
[key: string]: (condition: Condition, userAttributes: UserAttributes) => boolean | null
36+
};
37+
};
38+
39+
/**
40+
* Construct an instance of AudienceEvaluator with given options
41+
* @param {Object=} UNSTABLE_conditionEvaluators A map of condition evaluators provided by the consumer. This enables matching
42+
* condition types which are not supported natively by the SDK. Note that built in
43+
* Optimizely evaluators cannot be overridden.
44+
* @constructor
45+
*/
46+
constructor(UNSTABLE_conditionEvaluators: unknown) {
47+
this.typeToEvaluatorMap = fns.assign({}, UNSTABLE_conditionEvaluators, {
48+
custom_attribute: customAttributeConditionEvaluator,
49+
});
50+
}
51+
52+
/**
53+
* Determine if the given user attributes satisfy the given audience conditions
54+
* @param {Array<string|string[]} audienceConditions Audience conditions to match the user attributes against - can be an array
55+
* of audience IDs, a nested array of conditions, or a single leaf condition.
56+
* Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1"
57+
* @param {[id: string]: Audience} audiencesById Object providing access to full audience objects for audience IDs
58+
* contained in audienceConditions. Keys should be audience IDs, values
59+
* should be full audience objects with conditions properties
60+
* @param {UserAttributes} userAttributes User attributes which will be used in determining if audience conditions
61+
* are met. If not provided, defaults to an empty object
62+
* @return {boolean} true if the user attributes match the given audience conditions, false
63+
* otherwise
64+
*/
65+
evaluate(
66+
audienceConditions: Array<string | string[]>,
67+
audiencesById: { [id: string]: Audience },
68+
userAttributes: UserAttributes = {}
69+
): boolean {
70+
// if there are no audiences, return true because that means ALL users are included in the experiment
71+
if (!audienceConditions || audienceConditions.length === 0) {
72+
return true;
73+
}
74+
75+
const evaluateAudience = (audienceId: string) => {
76+
const audience = audiencesById[audienceId];
77+
if (audience) {
78+
logger.log(
79+
LOG_LEVEL.DEBUG,
80+
sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions))
81+
);
82+
const result = conditionTreeEvaluator.evaluate(
83+
audience.conditions as unknown[] ,
84+
this.evaluateConditionWithUserAttributes.bind(this, userAttributes)
85+
);
86+
const resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase();
87+
logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
88+
return result;
89+
}
90+
return null;
91+
};
92+
93+
return !!conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience);
94+
}
95+
96+
/**
97+
* Wrapper around evaluator.evaluate that is passed to the conditionTreeEvaluator.
98+
* Evaluates the condition provided given the user attributes if an evaluator has been defined for the condition type.
99+
* @param {UserAttributes} userAttributes A map of user attributes.
100+
* @param {Condition} condition A single condition object to evaluate.
101+
* @return {boolean|null} true if the condition is satisfied, null if a matcher is not found.
102+
*/
103+
evaluateConditionWithUserAttributes(userAttributes: UserAttributes, condition: Condition): boolean | null {
104+
const evaluator = this.typeToEvaluatorMap[condition.type];
105+
if (!evaluator) {
106+
logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)));
107+
return null;
108+
}
109+
try {
110+
return evaluator.evaluate(condition, userAttributes);
111+
} catch (err) {
112+
logger.log(
113+
LOG_LEVEL.ERROR,
114+
sprintf(ERROR_MESSAGES.CONDITION_EVALUATOR_ERROR, MODULE_NAME, condition.type, err.message)
115+
);
116+
}
117+
118+
return null;
119+
}
120+
}
121+
122+
export default AudienceEvaluator;
123+
124+
export const createAudienceEvaluator = function(UNSTABLE_conditionEvaluators: unknown): AudienceEvaluator {
125+
return new AudienceEvaluator(UNSTABLE_conditionEvaluators);
126+
};

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License. *
1515
***************************************************************************/
1616
import { getLogger } from '@optimizely/js-sdk-logging';
17-
import { UserAttributes } from '../../shared_types';
17+
import { UserAttributes, Condition } from '../../shared_types';
1818

1919
import fns from '../../utils/fns';
2020
import { LOG_MESSAGES } from '../../utils/enums';
@@ -52,13 +52,6 @@ const MATCH_TYPES = [
5252
SEMVER_GREATER_OR_EQUAL_THAN_MATCH_TYPE
5353
];
5454

55-
type Condition = {
56-
name: string;
57-
type: string;
58-
match?: string;
59-
value: string | number | boolean | null;
60-
}
61-
6255
type ConditionEvaluator = (condition: Condition, userAttributes: UserAttributes) => boolean | null;
6356

6457
const EVALUATORS_BY_MATCH_TYPE: { [conditionType: string]: ConditionEvaluator | undefined } = {};

0 commit comments

Comments
 (0)