|
| 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 | +}; |
0 commit comments