diff --git a/packages/optimizely-sdk/CHANGELOG.md b/packages/optimizely-sdk/CHANGELOG.md index be864cdee..d7d558f7b 100644 --- a/packages/optimizely-sdk/CHANGELOG.md +++ b/packages/optimizely-sdk/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add package.json script for running Karma tests locally using Chrome ([#651](https://github.com/optimizely/javascript-sdk/pull/651)). - Replaced explicit typescript typings with auto generated ones ([#745](https://github.com/optimizely/javascript-sdk/pull/745)). - Integrated code from `utils` package into `optimizely-sdk` ([#749](https://github.com/optimizely/javascript-sdk/pull/749)). +- Added ODP Segments support in Audience Evaluation ([#765](https://github.com/optimizely/javascript-sdk/pull/765)). ## [4.9.1] - January 18, 2022 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 72571282e..6fb545f10 100644 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/index.tests.js @@ -18,13 +18,18 @@ import { assert } from 'chai'; import { sprintf } from '../../utils/fns'; import { getLogger } from '../../modules/logging'; -import { createAudienceEvaluator } from './index'; +import AudienceEvaluator, { createAudienceEvaluator } from './index'; import * as conditionTreeEvaluator from '../condition_tree_evaluator'; import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); var mockLogger = getLogger(); +var getMockUserContext = (attributes, segments) => ({ + getAttributes: () => ({ ... (attributes || {})}), + isQualifiedFor: segment => segments.indexOf(segment) > -1 +}); + var chromeUserAudience = { conditions: [ 'and', @@ -91,11 +96,11 @@ describe('lib/core/audience_evaluator', function() { }); describe('evaluate', function() { it('should return true if there are no audiences', function() { - assert.isTrue(audienceEvaluator.evaluate([], audiencesById, {})); + assert.isTrue(audienceEvaluator.evaluate([], audiencesById, getMockUserContext({}))); }); it('should return false if there are audiences but no attributes', function() { - assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, {})); + assert.isFalse(audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({}))); }); it('should return true if any of the audience conditions are met', function() { @@ -112,9 +117,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, getMockUserContext(iphoneUsers))); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(chromeUsers))); + assert.isTrue(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(iphoneChromeUsers))); }); it('should return false if none of the audience conditions are met', function() { @@ -131,31 +136,31 @@ 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, getMockUserContext(nexusUsers))); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(safariUsers))); + assert.isFalse(audienceEvaluator.evaluate(['0', '1'], audiencesById, getMockUserContext(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)); + assert.isTrue(audienceEvaluator.evaluate(['2'], audiencesById, getMockUserContext({}))); }); 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' }); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'chrome' })); assert.isTrue(result); }); it('should return true if all of the audiences in an "AND" condition pass', function() { - var result = audienceEvaluator.evaluate(['and', '0', '1'], audiencesById, { + var result = audienceEvaluator.evaluate(['and', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'chrome', device_model: 'iphone', - }); + })); assert.isTrue(result); }); it('should return true if the audience in a "NOT" condition does not pass', function() { - var result = audienceEvaluator.evaluate(['not', '1'], audiencesById, { device_model: 'android' }); + var result = audienceEvaluator.evaluate(['not', '1'], audiencesById, getMockUserContext({ device_model: 'android' })); assert.isTrue(result); }); }); @@ -174,19 +179,19 @@ describe('lib/core/audience_evaluator', function() { it('returns true if conditionTreeEvaluator.evaluate returns true', function() { conditionTreeEvaluator.evaluate.returns(true); - var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, { browser_type: 'chrome' }); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'chrome' })); assert.isTrue(result); }); it('returns false if conditionTreeEvaluator.evaluate returns false', function() { conditionTreeEvaluator.evaluate.returns(false); - var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, { browser_type: 'safari' }); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ browser_type: 'safari' })); assert.isFalse(result); }); it('returns false if conditionTreeEvaluator.evaluate returns null', function() { conditionTreeEvaluator.evaluate.returns(null); - var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, { state: 'California' }); + var result = audienceEvaluator.evaluate(['or', '0', '1'], audiencesById, getMockUserContext({ state: 'California' })); assert.isFalse(result); }); @@ -196,12 +201,13 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(false); var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); + var user = getMockUserContext(userAttributes); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly( customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], - userAttributes, + user, ); assert.isFalse(result); }); @@ -225,12 +231,13 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(null); var userAttributes = { device_model: 5.5 }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); + var user = getMockUserContext(userAttributes); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly( customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], - userAttributes, + user ); assert.isFalse(result); assert.strictEqual(2, mockLogger.log.callCount); @@ -247,12 +254,13 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(true); var userAttributes = { device_model: 'iphone' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); + var user = getMockUserContext(userAttributes); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly( customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], - userAttributes, + user, ); assert.isTrue(result); assert.strictEqual(2, mockLogger.log.callCount); @@ -269,12 +277,13 @@ describe('lib/core/audience_evaluator', function() { }); customAttributeConditionEvaluator.evaluate.returns(false); var userAttributes = { device_model: 'android' }; - var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, userAttributes); + var user = getMockUserContext(userAttributes); + var result = audienceEvaluator.evaluate(['or', '1'], audiencesById, user); sinon.assert.calledOnce(customAttributeConditionEvaluator.evaluate); sinon.assert.calledWithExactly( customAttributeConditionEvaluator.evaluate, iphoneUserAudience.conditions[1], - userAttributes, + user, ); assert.isFalse(result); assert.strictEqual(2, mockLogger.log.callCount); @@ -296,8 +305,8 @@ describe('lib/core/audience_evaluator', function() { }; audienceEvaluator = createAudienceEvaluator({ special_condition_type: { - evaluate: function(condition, userAttributes) { - const result = mockEnvironment[condition.value] && userAttributes[condition.match] > 0; + evaluate: function(condition, user) { + const result = mockEnvironment[condition.value] && user.getAttributes()[condition.match] > 0; return result; }, }, @@ -305,8 +314,8 @@ describe('lib/core/audience_evaluator', function() { }); it('should evaluate an audience properly using the custom condition evaluator', function() { - assert.isFalse(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 0 })); - assert.isTrue(audienceEvaluator.evaluate(['3'], audiencesById, { interest_level: 1 })); + assert.isFalse(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 0 }))); + assert.isTrue(audienceEvaluator.evaluate(['3'], audiencesById, getMockUserContext({ interest_level: 1 }))); }); }); @@ -323,12 +332,248 @@ describe('lib/core/audience_evaluator', function() { it('should not be able to overwrite built in `custom_attribute` evaluator', function() { assert.isTrue( - audienceEvaluator.evaluate(['0'], audiencesById, { + audienceEvaluator.evaluate(['0'], audiencesById, getMockUserContext({ browser_type: 'chrome', - }) + })) ); }); }); }); + + context('with odp segment evaluator', function() { + describe('Single ODP Audience', () => { + const singleAudience = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audiencesById = { + 0: singleAudience, + } + const audience = new AudienceEvaluator(); + + it('should evaluate to true if segment is found', () => { + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1']))); + }); + + it('should evaluate to false if segment is not found', () => { + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2']))); + }); + + it('should evaluate to false if not segments are provided', () => { + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}))); + }); + }); + + describe('Multiple ODP conditions in one Audience', () => { + const singleAudience = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + ] + ] + }; + const audiencesById = { + 0: singleAudience, + } + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']))); + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']))); + assert.isTrue(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']))); + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-1', 'odp-segment-3', 'odp-segment-4']))); + assert.isFalse(audience.evaluate(['or', '0'], audiencesById, getMockUserContext({}, ['odp-segment-2', 'odp-segment-3', 'odp-segment-4']))); + }); + }); + + describe('Multiple ODP conditions in multiple Audience', () => { + const audience1And2 = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audience3And4 = { + "conditions": [ + "and", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audience5And6 = { + "conditions": [ + "or", + { + "value": "odp-segment-5", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-6", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audiencesById = { + 0: audience1And2, + 1: audience3And4, + 2: audience5And6 + } + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isTrue( + audience.evaluate( + ['or', '0', '1', '2'], + audiencesById, + getMockUserContext({},['odp-segment-1', 'odp-segment-2']) + ) + ); + assert.isFalse( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4', 'odp-segment-6']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1',['not', '2']], + audiencesById, + getMockUserContext({}, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3', 'odp-segment-4']) + ) + ); + }); + }); + }); + + context('with multiple types of evaluators', function() { + const audience1And2 = { + "conditions": [ + "and", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + const audience3Or4 = { + "conditions": [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "odp-segment-4", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + }; + + const audiencesById = { + 0: audience1And2, + 1: audience3Or4, + 2: chromeUserAudience, + } + + const audience = new AudienceEvaluator(); + + it('should evaluate correctly based on the given segments', () => { + assert.isFalse( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'not_chrome' }, + ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ); + assert.isTrue( + audience.evaluate( + ['and', '0', '1', '2'], + audiencesById, + getMockUserContext({ browser_type: 'chrome' }, + ['odp-segment-1', 'odp-segment-2', 'odp-segment-4']) + ) + ); + }); + }); }); }); diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/index.ts b/packages/optimizely-sdk/lib/core/audience_evaluator/index.ts index 87b332103..c3d417fb3 100644 --- a/packages/optimizely-sdk/lib/core/audience_evaluator/index.ts +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/index.ts @@ -23,7 +23,8 @@ import { } from '../../utils/enums'; import * as conditionTreeEvaluator from '../condition_tree_evaluator'; import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator'; -import { UserAttributes, Audience, Condition } from '../../shared_types'; +import * as odpSegmentsConditionEvaluator from './odp_segment_condition_evaluator'; +import { Audience, Condition, OptimizelyUserContext } from '../../shared_types'; const logger = getLogger(); const MODULE_NAME = 'AUDIENCE_EVALUATOR'; @@ -31,7 +32,7 @@ const MODULE_NAME = 'AUDIENCE_EVALUATOR'; export class AudienceEvaluator { private typeToEvaluatorMap: { [key: string]: { - [key: string]: (condition: Condition, userAttributes: UserAttributes) => boolean | null + [key: string]: (condition: Condition, user: OptimizelyUserContext) => boolean | null }; }; @@ -45,6 +46,7 @@ export class AudienceEvaluator { constructor(UNSTABLE_conditionEvaluators: unknown) { this.typeToEvaluatorMap = fns.assign({}, UNSTABLE_conditionEvaluators, { custom_attribute: customAttributeConditionEvaluator, + third_party_dimension: odpSegmentsConditionEvaluator, }); } @@ -56,15 +58,15 @@ export class AudienceEvaluator { * @param {[id: string]: Audience} 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 {UserAttributes} userAttributes User attributes which will be used in determining if audience conditions - * are met. If not provided, defaults to an empty object + * @param {OptimizelyUserContext} userAttributes User context which contains the attributes and segments which will be used in + * determining if audience conditions are met. * @return {boolean} true if the user attributes match the given audience conditions, false * otherwise */ evaluate( audienceConditions: Array, audiencesById: { [id: string]: Audience }, - userAttributes: UserAttributes = {} + user: OptimizelyUserContext, ): boolean { // if there are no audiences, return true because that means ALL users are included in the experiment if (!audienceConditions || audienceConditions.length === 0) { @@ -80,7 +82,7 @@ export class AudienceEvaluator { ); const result = conditionTreeEvaluator.evaluate( audience.conditions as unknown[] , - this.evaluateConditionWithUserAttributes.bind(this, userAttributes) + this.evaluateConditionWithUserAttributes.bind(this, user) ); const resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase(); logger.log(LOG_LEVEL.DEBUG, LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText); @@ -95,18 +97,18 @@ export class AudienceEvaluator { /** * 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 {UserAttributes} userAttributes A map of user attributes. - * @param {Condition} condition A single condition object to evaluate. + * @param {OptimizelyUserContext} user Optimizely user context containing attributes and segments + * @param {Condition} condition A single condition object to evaluate. * @return {boolean|null} true if the condition is satisfied, null if a matcher is not found. */ - evaluateConditionWithUserAttributes(userAttributes: UserAttributes, condition: Condition): boolean | null { + evaluateConditionWithUserAttributes(user: OptimizelyUserContext, condition: Condition): boolean | null { const evaluator = this.typeToEvaluatorMap[condition.type]; if (!evaluator) { logger.log(LOG_LEVEL.WARNING, LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)); return null; } try { - return evaluator.evaluate(condition, userAttributes); + return evaluator.evaluate(condition, user); } catch (err) { logger.log( LOG_LEVEL.ERROR, diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js b/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js new file mode 100644 index 000000000..503017545 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.tests.js @@ -0,0 +1,73 @@ +/**************************************************************************** + * Copyright 2022, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import sinon from 'sinon'; +import { assert } from 'chai'; +import { sprintf } from '../../../utils/fns'; + +import { + LOG_LEVEL, + LOG_MESSAGES, +} from '../../../utils/enums'; +import * as logging from '../../../modules/logging'; +import * as odpSegmentEvalutor from './'; + +var odpSegment1Condition = { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" +}; + +var getMockUserContext = (attributes, segments) => ({ + getAttributes: () => ({ ... (attributes || {})}), + isQualifiedFor: segment => segments.indexOf(segment) > -1 +}); + +describe('lib/core/audience_evaluator/odp_segment_condition_evaluator', function() { + var stubLogHandler; + + beforeEach(function() { + stubLogHandler = { + log: sinon.stub(), + }; + logging.setLogLevel('notset'); + logging.setLogHandler(stubLogHandler); + }); + + afterEach(function() { + logging.resetLogger(); + }); + + it('should return true when segment qualifies and known match type is provided', () => { + assert.isTrue(odpSegmentEvalutor.evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-1']))); + }); + + it('should return false when segment does not qualify and known match type is provided', () => { + assert.isFalse(odpSegmentEvalutor.evaluate(odpSegment1Condition, getMockUserContext({}, ['odp-segment-2']))); + }) + + it('should return null when segment qualifies but unknown match type is provided', () => { + const invalidOdpMatchCondition = { + ... odpSegment1Condition, + "match": 'unknown', + }; + assert.isNull(odpSegmentEvalutor.evaluate(invalidOdpMatchCondition, getMockUserContext({}, ['odp-segment-1']))); + sinon.assert.calledOnce(stubLogHandler.log); + assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.WARNING); + var logMessage = stubLogHandler.log.args[0][1]; + assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.UNKNOWN_MATCH_TYPE, 'ODP_SEGMENT_CONDITION_EVALUATOR', JSON.stringify(invalidOdpMatchCondition))); + }); +}); diff --git a/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts b/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts new file mode 100644 index 000000000..3098ae2b0 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/audience_evaluator/odp_segment_condition_evaluator/index.ts @@ -0,0 +1,64 @@ +/**************************************************************************** + * Copyright 2022 Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ +import { getLogger } from '../../../modules/logging'; +import { Condition, OptimizelyUserContext } from '../../../shared_types'; + +import { LOG_MESSAGES } from '../../../utils/enums'; + +const MODULE_NAME = 'ODP_SEGMENT_CONDITION_EVALUATOR'; + +const logger = getLogger(); + +const QUALIFIED_MATCH_TYPE = 'qualified'; + +const MATCH_TYPES = [ + QUALIFIED_MATCH_TYPE, +]; + +type ConditionEvaluator = (condition: Condition, user: OptimizelyUserContext) => boolean | null; + +const EVALUATORS_BY_MATCH_TYPE: { [conditionType: string]: ConditionEvaluator | undefined } = {}; +EVALUATORS_BY_MATCH_TYPE[QUALIFIED_MATCH_TYPE] = qualifiedEvaluator; + +/** + * Given a custom attribute audience condition and user attributes, evaluate the + * condition against the attributes. + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @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 + */ +export function evaluate(condition: Condition, user: OptimizelyUserContext): boolean | null { + const conditionMatch = condition.match; + if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { + logger.warn(LOG_MESSAGES.UNKNOWN_MATCH_TYPE, MODULE_NAME, JSON.stringify(condition)); + return null; + } + + let evaluator; + if (!conditionMatch) { + evaluator = qualifiedEvaluator; + } else { + evaluator = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || qualifiedEvaluator; + } + + return evaluator(condition, user); +} + +function qualifiedEvaluator(condition: Condition, user: OptimizelyUserContext): boolean { + return user.isQualifiedFor(condition.value as string); +} 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 8981741c7..b594cf898 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 @@ -45,6 +45,10 @@ var doubleCondition = { type: 'custom_attribute', }; +var getMockUserContext = (attributes) => ({ + getAttributes: () => ({ ... (attributes || {})}) +}); + describe('lib/core/custom_attribute_condition_evaluator', function() { var stubLogHandler; @@ -65,7 +69,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { browser_type: 'safari', }; - assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes)); + assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, getMockUserContext(userAttributes))); }); it('should return false when the attributes do not pass the audience conditions and no match type is provided', function() { @@ -73,7 +77,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { browser_type: 'firefox', }; - assert.isFalse(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes)); + assert.isFalse(customAttributeEvaluator.evaluate(browserConditionSafari, getMockUserContext(userAttributes))); }); it('should evaluate different typed attributes', function() { @@ -84,17 +88,17 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { pi_value: 3.14, }; - assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, userAttributes)); - assert.isTrue(customAttributeEvaluator.evaluate(booleanCondition, userAttributes)); - assert.isTrue(customAttributeEvaluator.evaluate(integerCondition, userAttributes)); - assert.isTrue(customAttributeEvaluator.evaluate(doubleCondition, userAttributes)); + assert.isTrue(customAttributeEvaluator.evaluate(browserConditionSafari, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.evaluate(booleanCondition, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.evaluate(integerCondition, getMockUserContext(userAttributes))); + assert.isTrue(customAttributeEvaluator.evaluate(doubleCondition, getMockUserContext(userAttributes))); }); it('should log and return null when condition has an invalid match property', function() { var invalidMatchCondition = { match: 'weird', name: 'weird_condition', type: 'custom_attribute', value: 'hi' }; var result = customAttributeEvaluator.evaluate( invalidMatchCondition, - { weird_condition: 'bye' } + getMockUserContext({ weird_condition: 'bye' }) ); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); @@ -111,33 +115,33 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; it('should return false if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, {}); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({})); assert.isFalse(result); sinon.assert.notCalled(stubLogHandler.log); }); it('should return false if the user-provided value is undefined', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: undefined }); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({ input_value: undefined })); assert.isFalse(result); }); it('should return false if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: null }); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({ input_value: null })); assert.isFalse(result); }); it('should return true if the user-provided value is a string', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: 'hi' }); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({ input_value: 'hi' })); assert.isTrue(result); }); it('should return true if the user-provided value is a number', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: 10 }); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({ input_value: 10 })); assert.isTrue(result); }); it('should return true if the user-provided value is a boolean', function() { - var result = customAttributeEvaluator.evaluate(existsCondition, { input_value: true }); + var result = customAttributeEvaluator.evaluate(existsCondition, getMockUserContext({ input_value: true })); assert.isTrue(result); }); }); @@ -154,7 +158,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return true if the user-provided value is equal to the condition value', function() { var result = customAttributeEvaluator.evaluate( exactStringCondition, - { favorite_constellation: 'Lacerta' } + getMockUserContext({ favorite_constellation: 'Lacerta' }) ); assert.isTrue(result); }); @@ -162,7 +166,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is not equal to the condition value', function() { var result = customAttributeEvaluator.evaluate( exactStringCondition, - { favorite_constellation: 'The Big Dipper' } + getMockUserContext({ favorite_constellation: 'The Big Dipper' }) ); assert.isFalse(result); }); @@ -176,7 +180,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( invalidExactCondition, - { favorite_constellation: 'Lacerta' } + getMockUserContext({ favorite_constellation: 'Lacerta' }) ); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); @@ -189,7 +193,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes = { favorite_constellation: false }; var result = customAttributeEvaluator.evaluate( exactStringCondition, - unexpectedTypeUserAttributes + getMockUserContext(unexpectedTypeUserAttributes) ); assert.isNull(result); @@ -207,7 +211,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided value is null', function() { var result = customAttributeEvaluator.evaluate( exactStringCondition, - { favorite_constellation: null } + getMockUserContext({ favorite_constellation: null }) ); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); @@ -220,7 +224,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactStringCondition, {}); + var result = customAttributeEvaluator.evaluate(exactStringCondition, getMockUserContext({})); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.DEBUG); @@ -235,7 +239,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes = { favorite_constellation: [] }; var result = customAttributeEvaluator.evaluate( exactStringCondition, - unexpectedTypeUserAttributes + getMockUserContext(unexpectedTypeUserAttributes) ); assert.isNull(result); var userValue = unexpectedTypeUserAttributes[exactStringCondition.name]; @@ -259,12 +263,12 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; it('should return true if the user-provided value is equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 9000 }); + var result = customAttributeEvaluator.evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 9000 })); assert.isTrue(result); }); it('should return false if the user-provided value is not equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, { lasers_count: 8000 }); + var result = customAttributeEvaluator.evaluate(exactNumberCondition, getMockUserContext({ lasers_count: 8000 })); assert.isFalse(result); }); @@ -272,14 +276,14 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes1 = { lasers_count: 'yes' }; var result = customAttributeEvaluator.evaluate( exactNumberCondition, - unexpectedTypeUserAttributes1 + getMockUserContext(unexpectedTypeUserAttributes1) ); assert.isNull(result); var unexpectedTypeUserAttributes2 = { lasers_count: '1000' }; result = customAttributeEvaluator.evaluate( exactNumberCondition, - unexpectedTypeUserAttributes2 + getMockUserContext(unexpectedTypeUserAttributes2) ); assert.isNull(result); @@ -304,12 +308,12 @@ 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 }); + var result = customAttributeEvaluator.evaluate(exactNumberCondition, getMockUserContext({ lasers_count: -Infinity })); assert.isNull(result); result = customAttributeEvaluator.evaluate( exactNumberCondition, - { lasers_count: -Math.pow(2, 53) - 2 } + getMockUserContext({ lasers_count: -Math.pow(2, 53) - 2 }) ); assert.isNull(result); @@ -330,7 +334,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactNumberCondition, {}); + var result = customAttributeEvaluator.evaluate(exactNumberCondition, getMockUserContext({})); assert.isNull(result); }); @@ -341,7 +345,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { type: 'custom_attribute', value: Infinity, }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition1, { lasers_count: 9000 }); + var result = customAttributeEvaluator.evaluate(invalidValueCondition1, getMockUserContext({ lasers_count: 9000 })); assert.isNull(result); var invalidValueCondition2 = { @@ -350,7 +354,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { type: 'custom_attribute', value: Math.pow(2, 53) + 2, }; - result = customAttributeEvaluator.evaluate(invalidValueCondition2, { lasers_count: 9000 }); + result = customAttributeEvaluator.evaluate(invalidValueCondition2, getMockUserContext({ lasers_count: 9000 })); assert.isNull(result); assert.strictEqual(2, stubLogHandler.log.callCount); @@ -379,22 +383,22 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; it('should return true if the user-provided value is equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: false }); + var result = customAttributeEvaluator.evaluate(exactBoolCondition, getMockUserContext({ did_register_user: false })); assert.isTrue(result); }); it('should return false if the user-provided value is not equal to the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: true }); + var result = customAttributeEvaluator.evaluate(exactBoolCondition, getMockUserContext({ did_register_user: true })); assert.isFalse(result); }); it('should return null if the user-provided value is of a different type than the condition value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, { did_register_user: 10 }); + var result = customAttributeEvaluator.evaluate(exactBoolCondition, getMockUserContext({ did_register_user: 10 })); assert.isNull(result); }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(exactBoolCondition, {}); + var result = customAttributeEvaluator.evaluate(exactBoolCondition, getMockUserContext({})); assert.isNull(result); }); }); @@ -411,9 +415,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return true if the condition value is a substring of the user-provided value', function() { var result = customAttributeEvaluator.evaluate( substringCondition, - { + getMockUserContext({ headline_text: 'Limited time, buy now!', - } + }) ); assert.isTrue(result); }); @@ -421,9 +425,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is not a substring of the condition value', function() { var result = customAttributeEvaluator.evaluate( substringCondition, - { + getMockUserContext({ headline_text: 'Breaking news!', - } + }) ); assert.isFalse(result); }); @@ -432,7 +436,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes = { headline_text: 10 }; var result = customAttributeEvaluator.evaluate( substringCondition, - unexpectedTypeUserAttributes + getMockUserContext(unexpectedTypeUserAttributes) ); assert.isNull(result); var userValue = unexpectedTypeUserAttributes[substringCondition.name]; @@ -454,7 +458,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { value: 10, }; - var result = customAttributeEvaluator.evaluate(nonStringCondition, { headline_text: 'hello' }); + var result = customAttributeEvaluator.evaluate(nonStringCondition, getMockUserContext({ headline_text: 'hello' })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.WARNING); @@ -466,7 +470,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, { headline_text: null }); + var result = customAttributeEvaluator.evaluate(substringCondition, getMockUserContext({ headline_text: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.DEBUG); @@ -478,7 +482,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(substringCondition, {}); + var result = customAttributeEvaluator.evaluate(substringCondition, getMockUserContext({})); assert.isNull(result); }); }); @@ -494,9 +498,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return true if the user-provided value is greater than the condition value', function() { var result = customAttributeEvaluator.evaluate( gtCondition, - { + getMockUserContext({ meters_travelled: 58.4, - } + }) ); assert.isTrue(result); }); @@ -504,9 +508,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is not greater than the condition value', function() { var result = customAttributeEvaluator.evaluate( gtCondition, - { + getMockUserContext({ meters_travelled: 20, - } + }) ); assert.isFalse(result); }); @@ -515,14 +519,14 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes1 = { meters_travelled: 'a long way' }; var result = customAttributeEvaluator.evaluate( gtCondition, - unexpectedTypeUserAttributes1 + getMockUserContext(unexpectedTypeUserAttributes1) ); assert.isNull(result); var unexpectedTypeUserAttributes2 = { meters_travelled: '1000' }; result = customAttributeEvaluator.evaluate( gtCondition, - unexpectedTypeUserAttributes2 + getMockUserContext(unexpectedTypeUserAttributes2) ); assert.isNull(result); @@ -549,13 +553,13 @@ 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( gtCondition, - { meters_travelled: -Infinity } + getMockUserContext({ meters_travelled: -Infinity }) ); assert.isNull(result); result = customAttributeEvaluator.evaluate( gtCondition, - { meters_travelled: Math.pow(2, 53) + 2 } + getMockUserContext({ meters_travelled: Math.pow(2, 53) + 2 }) ); assert.isNull(result); @@ -576,7 +580,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, { meters_travelled: null }); + var result = customAttributeEvaluator.evaluate(gtCondition, getMockUserContext({ meters_travelled: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.DEBUG); @@ -588,7 +592,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(gtCondition, {}); + var result = customAttributeEvaluator.evaluate(gtCondition, getMockUserContext({})); assert.isNull(result); }); @@ -600,15 +604,15 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { type: 'custom_attribute', value: Infinity, }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + var result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); invalidValueCondition.value = null; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); invalidValueCondition.value = Math.pow(2, 53) + 2; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); sinon.assert.calledThrice(stubLogHandler.log); @@ -631,9 +635,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return true if the user-provided value is less than the condition value', function() { var result = customAttributeEvaluator.evaluate( ltCondition, - { + getMockUserContext({ meters_travelled: 10, - } + }) ); assert.isTrue(result); }); @@ -641,9 +645,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is not less than the condition value', function() { var result = customAttributeEvaluator.evaluate( ltCondition, - { + getMockUserContext({ meters_travelled: 64.64, - } + }) ); assert.isFalse(result); }); @@ -652,14 +656,14 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { var unexpectedTypeUserAttributes1 = { meters_travelled: true }; var result = customAttributeEvaluator.evaluate( ltCondition, - unexpectedTypeUserAttributes1 + getMockUserContext(unexpectedTypeUserAttributes1) ); assert.isNull(result); var unexpectedTypeUserAttributes2 = { meters_travelled: '48.2' }; result = customAttributeEvaluator.evaluate( ltCondition, - unexpectedTypeUserAttributes2 + getMockUserContext(unexpectedTypeUserAttributes2) ); assert.isNull(result); @@ -686,17 +690,17 @@ 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( ltCondition, - { + getMockUserContext({ meters_travelled: Infinity, - } + }) ); assert.isNull(result); result = customAttributeEvaluator.evaluate( ltCondition, - { + getMockUserContext({ meters_travelled: Math.pow(2, 53) + 2, - } + }) ); assert.isNull(result); @@ -717,7 +721,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, { meters_travelled: null }); + var result = customAttributeEvaluator.evaluate(ltCondition, getMockUserContext({ meters_travelled: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); assert.strictEqual(stubLogHandler.log.args[0][0], LOG_LEVEL.DEBUG); @@ -729,7 +733,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(ltCondition, {}); + var result = customAttributeEvaluator.evaluate(ltCondition, getMockUserContext({})); assert.isNull(result); }); @@ -741,15 +745,15 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { type: 'custom_attribute', value: Infinity, }; - var result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + var result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); invalidValueCondition.value = {}; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); invalidValueCondition.value = Math.pow(2, 53) + 2; - result = customAttributeEvaluator.evaluate(invalidValueCondition, userAttributes); + result = customAttributeEvaluator.evaluate(invalidValueCondition, getMockUserContext(userAttributes)); assert.isNull(result); sinon.assert.calledThrice(stubLogHandler.log); @@ -771,9 +775,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is greater than the condition value', function() { var result = customAttributeEvaluator.evaluate( leCondition, - { + getMockUserContext({ meters_travelled: 48.3, - } + }) ); assert.isFalse(result); }); @@ -783,9 +787,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { for (let userValue of versions) { var result = customAttributeEvaluator.evaluate( leCondition, - { + getMockUserContext({ meters_travelled: userValue, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for condition value: ${leCondition.value} and user value: ${userValue}`); } @@ -804,9 +808,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return false if the user-provided value is less than the condition value', function() { var result = customAttributeEvaluator.evaluate( geCondition, - { + getMockUserContext({ meters_travelled: 48, - } + }) ); assert.isFalse(result); }); @@ -816,9 +820,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { for (let userValue of versions) { var result = customAttributeEvaluator.evaluate( geCondition, - { + getMockUserContext({ meters_travelled: userValue, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for condition value: ${geCondition.value} and user value: ${userValue}`); } @@ -846,9 +850,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvergtCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -870,9 +874,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvergtCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -881,17 +885,17 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided version is not a string', function() { var result = customAttributeEvaluator.evaluate( semvergtCondition, - { + getMockUserContext({ app_version: 22, - } + }) ); assert.isNull(result); result = customAttributeEvaluator.evaluate( semvergtCondition, - { + getMockUserContext({ app_version: false, - } + }) ); assert.isNull(result); @@ -907,7 +911,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(semvergtCondition, { app_version: null }); + var result = customAttributeEvaluator.evaluate(semvergtCondition, getMockUserContext({ app_version: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); sinon.assert.calledWithExactly( @@ -918,7 +922,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(semvergtCondition, {}); + var result = customAttributeEvaluator.evaluate(semvergtCondition, getMockUserContext({})); assert.isNull(result); }); }); @@ -947,9 +951,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemverltCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -969,9 +973,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemverltCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -980,17 +984,17 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided version is not a string', function() { var result = customAttributeEvaluator.evaluate( semverltCondition, - { + getMockUserContext({ app_version: 22, - } + }) ); assert.isNull(result); result = customAttributeEvaluator.evaluate( semverltCondition, - { + getMockUserContext({ app_version: false, - } + }) ); assert.isNull(result); @@ -1006,7 +1010,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(semverltCondition, { app_version: null }); + var result = customAttributeEvaluator.evaluate(semverltCondition, getMockUserContext({ app_version: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); sinon.assert.calledWithExactly( @@ -1017,7 +1021,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(semverltCondition, {}); + var result = customAttributeEvaluator.evaluate(semverltCondition, getMockUserContext({})); assert.isNull(result); }); }); @@ -1045,9 +1049,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -1067,9 +1071,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -1078,17 +1082,17 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should log and return null if the user-provided version is not a string', function() { var result = customAttributeEvaluator.evaluate( semvereqCondition, - { + getMockUserContext({ app_version: 22, - } + }) ); assert.isNull(result); result = customAttributeEvaluator.evaluate( semvereqCondition, - { + getMockUserContext({ app_version: false, - } + }) ); assert.isNull(result); @@ -1104,7 +1108,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should log and return null if the user-provided value is null', function() { - var result = customAttributeEvaluator.evaluate(semvereqCondition, { app_version: null }); + var result = customAttributeEvaluator.evaluate(semvereqCondition, getMockUserContext({ app_version: null })); assert.isNull(result); sinon.assert.calledOnce(stubLogHandler.log); sinon.assert.calledWithExactly( @@ -1115,7 +1119,7 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }); it('should return null if there is no user-provided value', function() { - var result = customAttributeEvaluator.evaluate(semvereqCondition, {}); + var result = customAttributeEvaluator.evaluate(semvereqCondition, getMockUserContext({})); assert.isNull(result); }); }); @@ -1141,9 +1145,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -1164,9 +1168,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -1175,9 +1179,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { it('should return true if the user-provided version is equal to the condition version', function() { var result = customAttributeEvaluator.evaluate( semverleCondition, - { + getMockUserContext({ app_version: '2.0', - } + }) ); assert.isTrue(result); }); @@ -1206,9 +1210,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isTrue(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } @@ -1228,9 +1232,9 @@ describe('lib/core/custom_attribute_condition_evaluator', function() { }; var result = customAttributeEvaluator.evaluate( customSemvereqCondition, - { + getMockUserContext({ app_version: userVersion, - } + }) ); assert.isFalse(result, `Got result ${result}. Failed for target version: ${targetVersion} and user version: ${userVersion}`); } diff --git a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.ts b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.ts index 24f412652..775be8ad7 100644 --- a/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.ts +++ b/packages/optimizely-sdk/lib/core/custom_attribute_condition_evaluator/index.ts @@ -14,7 +14,7 @@ * limitations under the License. * ***************************************************************************/ import { getLogger } from '../../modules/logging'; -import { UserAttributes, Condition } from '../../shared_types'; +import { Condition, OptimizelyUserContext } from '../../shared_types'; import fns from '../../utils/fns'; import { LOG_MESSAGES } from '../../utils/enums'; @@ -52,7 +52,7 @@ const MATCH_TYPES = [ SEMVER_GREATER_OR_EQUAL_THAN_MATCH_TYPE ]; -type ConditionEvaluator = (condition: Condition, userAttributes: UserAttributes) => boolean | null; +type ConditionEvaluator = (condition: Condition, user: OptimizelyUserContext) => boolean | null; const EVALUATORS_BY_MATCH_TYPE: { [conditionType: string]: ConditionEvaluator | undefined } = {}; EVALUATORS_BY_MATCH_TYPE[EXACT_MATCH_TYPE] = exactEvaluator; @@ -71,14 +71,14 @@ EVALUATORS_BY_MATCH_TYPE[SEMVER_LESS_OR_EQUAL_THAN_MATCH_TYPE] = semverLessThanO /** * Given a custom attribute audience condition and user attributes, evaluate the * condition against the attributes. - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} 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 + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @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 */ -export function evaluate(condition: Condition, userAttributes: UserAttributes): boolean | null { +export function evaluate(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const conditionMatch = condition.match; if (typeof conditionMatch !== 'undefined' && MATCH_TYPES.indexOf(conditionMatch) === -1) { logger.warn(LOG_MESSAGES.UNKNOWN_MATCH_TYPE, MODULE_NAME, JSON.stringify(condition)); @@ -100,7 +100,7 @@ export function evaluate(condition: Condition, userAttributes: UserAttributes): evaluatorForMatch = EVALUATORS_BY_MATCH_TYPE[conditionMatch] || exactEvaluator; } - return evaluatorForMatch(condition, userAttributes); + return evaluatorForMatch(condition, user); } /** @@ -115,16 +115,16 @@ function isValueTypeValidForExactConditions(value: unknown): boolean { /** * Evaluate the given exact match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @return {?boolean} true if the user attribute value is equal (===) to the condition value, - * false if the user attribute value is not equal (!==) to the condition value, - * null if the condition value or user attribute value has an invalid type, or - * if there is a mismatch between the user attribute type and the condition value - * type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @return {?boolean} true if the user attribute value is equal (===) to the condition value, + * false if the user attribute value is not equal (!==) to the condition value, + * null if the condition value or user attribute value has an invalid type, or + * if there is a mismatch between the user attribute type and the condition value + * type */ -function exactEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function exactEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const conditionValue = condition.value; const conditionValueType = typeof conditionValue; const conditionName = condition.name; @@ -167,26 +167,28 @@ function exactEvaluator(condition: Condition, userAttributes: UserAttributes): b /** * Evaluate the given exists match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @returns {boolean} true if both: - * 1) the user attributes have a value for the given condition, and - * 2) the user attribute value is neither null nor undefined - * Returns false otherwise + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {boolean} true if both: + * 1) the user attributes have a value for the given condition, and + * 2) the user attribute value is neither null nor undefined + * Returns false otherwise */ -function existsEvaluator(condition: Condition, userAttributes: UserAttributes): boolean { +function existsEvaluator(condition: Condition, user: OptimizelyUserContext): boolean { + const userAttributes = user.getAttributes(); const userValue = userAttributes[condition.name]; return typeof userValue !== 'undefined' && userValue !== null; } /** * Validate user and condition values - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @returns {?boolean} true if values are valid, - * false if values are not valid + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if values are valid, + * false if values are not valid */ -function validateValuesForNumericCondition(condition: Condition, userAttributes: UserAttributes): boolean { +function validateValuesForNumericCondition(condition: Condition, user: OptimizelyUserContext): boolean { + const userAttributes = user.getAttributes(); const conditionName = condition.name; const userValue = userAttributes[conditionName]; const userValueType = typeof userValue; @@ -224,19 +226,19 @@ function validateValuesForNumericCondition(condition: Condition, userAttributes: /** * Evaluate the given greater than match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?boolean} true if the user attribute value is greater than the condition value, - * false if the user attribute value is less than or equal to the condition value, - * null if the condition value isn't a number or the user attribute value - * isn't a number + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if the user attribute value is greater than the condition value, + * false if the user attribute value is less than or equal to the condition value, + * null if the condition value isn't a number or the user attribute value + * isn't a number */ -function greaterThanEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function greaterThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const userValue = userAttributes[condition.name]; const conditionValue = condition.value; - if (!validateValuesForNumericCondition(condition, userAttributes) || conditionValue === null) { + if (!validateValuesForNumericCondition(condition, user) || conditionValue === null) { return null; } return userValue > conditionValue; @@ -244,19 +246,19 @@ function greaterThanEvaluator(condition: Condition, userAttributes: UserAttribut /** * Evaluate the given greater or equal than match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute value is greater or equal than the condition value, - * false if the user attribute value is less than to the condition value, - * null if the condition value isn't a number or the user attribute value isn't a - * number + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute value is greater or equal than the condition value, + * false if the user attribute value is less than to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number */ -function greaterThanOrEqualEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function greaterThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const userValue = userAttributes[condition.name]; const conditionValue = condition.value; - if (!validateValuesForNumericCondition(condition, userAttributes) || conditionValue === null) { + if (!validateValuesForNumericCondition(condition, user) || conditionValue === null) { return null; } @@ -265,19 +267,19 @@ function greaterThanOrEqualEvaluator(condition: Condition, userAttributes: UserA /** * Evaluate the given less than match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?boolean} true if the user attribute value is less than the condition value, - * false if the user attribute value is greater than or equal to the condition value, - * null if the condition value isn't a number or the user attribute value isn't a - * number + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?boolean} true if the user attribute value is less than the condition value, + * false if the user attribute value is greater than or equal to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number */ -function lessThanEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function lessThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const userValue = userAttributes[condition.name]; const conditionValue = condition.value; - if (!validateValuesForNumericCondition(condition, userAttributes) || conditionValue === null) { + if (!validateValuesForNumericCondition(condition, user) || conditionValue === null) { return null; } @@ -286,19 +288,19 @@ function lessThanEvaluator(condition: Condition, userAttributes: UserAttributes) /** * Evaluate the given less or equal than match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute value is less or equal than the condition value, - * false if the user attribute value is greater than to the condition value, - * null if the condition value isn't a number or the user attribute value isn't a - * number + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute value is less or equal than the condition value, + * false if the user attribute value is greater than to the condition value, + * null if the condition value isn't a number or the user attribute value isn't a + * number */ -function lessThanOrEqualEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function lessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const userValue = userAttributes[condition.name]; const conditionValue = condition.value; - if (!validateValuesForNumericCondition(condition, userAttributes) || conditionValue === null) { + if (!validateValuesForNumericCondition(condition, user) || conditionValue === null) { return null; } @@ -307,15 +309,15 @@ function lessThanOrEqualEvaluator(condition: Condition, userAttributes: UserAttr /** * Evaluate the given substring match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the condition value is a substring of the user attribute value, - * false if the condition value is not a substring of the user attribute value, - * null if the condition value isn't a string or the user attribute value - * isn't a string + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the condition value is a substring of the user attribute value, + * false if the condition value is not a substring of the user attribute value, + * null if the condition value isn't a string or the user attribute value + * isn't a string */ -function substringEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { +function substringEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const userAttributes = user.getAttributes(); const conditionName = condition.name; const userValue = userAttributes[condition.name]; const userValueType = typeof userValue; @@ -347,13 +349,13 @@ function substringEvaluator(condition: Condition, userAttributes: UserAttributes /** * Evaluate the given semantic version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?number} returns compareVersion result - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?number} returns compareVersion result + * null if the user attribute version has an invalid type */ -function evaluateSemanticVersion(condition: Condition, userAttributes: UserAttributes): number | null { +function evaluateSemanticVersion(condition: Condition, user: OptimizelyUserContext): number | null { + const userAttributes = user.getAttributes(); const conditionName = condition.name; const userValue = userAttributes[conditionName]; const userValueType = typeof userValue; @@ -379,22 +381,21 @@ function evaluateSemanticVersion(condition: Condition, userAttributes: UserAttri ); return null; } - + return compareVersion(conditionValue, userValue); } /** * Evaluate the given version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute version is equal (===) to the condition version, - * false if the user attribute version is not equal (!==) to the condition version, - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is equal (===) to the condition version, + * false if the user attribute version is not equal (!==) to the condition version, + * null if the user attribute version has an invalid type */ -function semverEqualEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { - const result = evaluateSemanticVersion(condition, userAttributes); - if (result === null ) { +function semverEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const result = evaluateSemanticVersion(condition, user); + if (result === null) { return null; } return result === 0; @@ -402,16 +403,15 @@ function semverEqualEvaluator(condition: Condition, userAttributes: UserAttribut /** * Evaluate the given version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute version is greater (>) than the condition version, - * false if the user attribute version is not greater than the condition version, - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is greater (>) than the condition version, + * false if the user attribute version is not greater than the condition version, + * null if the user attribute version has an invalid type */ -function semverGreaterThanEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { - const result = evaluateSemanticVersion(condition, userAttributes); - if (result === null ) { +function semverGreaterThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const result = evaluateSemanticVersion(condition, user); + if (result === null) { return null; } return result > 0; @@ -419,16 +419,15 @@ function semverGreaterThanEvaluator(condition: Condition, userAttributes: UserAt /** * Evaluate the given version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute version is less (<) than the condition version, - * false if the user attribute version is not less than the condition version, - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is less (<) than the condition version, + * false if the user attribute version is not less than the condition version, + * null if the user attribute version has an invalid type */ -function semverLessThanEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { - const result = evaluateSemanticVersion(condition, userAttributes); - if (result === null ) { +function semverLessThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const result = evaluateSemanticVersion(condition, user); + if (result === null) { return null; } return result < 0; @@ -436,16 +435,15 @@ function semverLessThanEvaluator(condition: Condition, userAttributes: UserAttri /** * Evaluate the given version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute version is greater than or equal (>=) to the condition version, - * false if the user attribute version is not greater than or equal to the condition version, - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is greater than or equal (>=) to the condition version, + * false if the user attribute version is not greater than or equal to the condition version, + * null if the user attribute version has an invalid type */ -function semverGreaterThanOrEqualEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { - const result = evaluateSemanticVersion(condition, userAttributes); - if (result === null ) { +function semverGreaterThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const result = evaluateSemanticVersion(condition, user); + if (result === null) { return null; } return result >= 0; @@ -453,18 +451,17 @@ function semverGreaterThanOrEqualEvaluator(condition: Condition, userAttributes: /** * Evaluate the given version match condition for the given user attributes - * @param {Condition} condition - * @param {UserAttributes} userAttributes - * @param {LoggerFacade} logger - * @returns {?Boolean} true if the user attribute version is less than or equal (<=) to the condition version, - * false if the user attribute version is not less than or equal to the condition version, - * null if the user attribute version has an invalid type + * @param {Condition} condition + * @param {OptimizelyUserContext} user + * @returns {?Boolean} true if the user attribute version is less than or equal (<=) to the condition version, + * false if the user attribute version is not less than or equal to the condition version, + * null if the user attribute version has an invalid type */ -function semverLessThanOrEqualEvaluator(condition: Condition, userAttributes: UserAttributes): boolean | null { - const result = evaluateSemanticVersion(condition, userAttributes); - if (result === null ) { +function semverLessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { + const result = evaluateSemanticVersion(condition, user); + if (result === null) { return null; } return result <= 0; - + } 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 747ef9720..5f624a12f 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js @@ -669,7 +669,7 @@ describe('lib/core/decision_service', function() { configObj, experiment, "experiment", - { browser_type: 'firefox' }, + { getAttributes: () => ({ browser_type: 'firefox' }) }, '' ).result ); diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.ts b/packages/optimizely-sdk/lib/core/decision_service/index.ts index 495ea412d..d561874b4 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.ts @@ -184,7 +184,7 @@ export class DecisionService { configObj, experiment, AUDIENCE_EVALUATION_TYPES.EXPERIMENT, - attributes, + user, '' ); decideReasons.push(...decisionifUserIsInAudience.reasons); @@ -362,7 +362,7 @@ export class DecisionService { configObj: ProjectConfig, experiment: Experiment, evaluationAttribute: string, - attributes?: UserAttributes, + user: OptimizelyUserContext, loggingKey?: string | number, ): DecisionResponse { const decideReasons: (string | number)[][] = []; @@ -383,7 +383,7 @@ export class DecisionService { loggingKey || experiment.key, JSON.stringify(experimentAudienceConditions), ]); - const result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, attributes); + const result = this.audienceEvaluator.evaluate(experimentAudienceConditions, audiencesById, user); this.logger.log( LOG_LEVEL.INFO, LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT_COMBINED, @@ -784,7 +784,7 @@ export class DecisionService { * @param {ruleKey} ruleKey A ruleKey (optional). * @return {DecisionResponse} DecisionResponse object containing valid variation object and decide reasons. */ - findValidatedForcedDecision( + findValidatedForcedDecision( config: ProjectConfig, user: OptimizelyUserContext, flagKey: string, @@ -1116,10 +1116,10 @@ export class DecisionService { const forcedDecisionResponse = this.findValidatedForcedDecision(configObj, user, flagKey, rule.key); decideReasons.push(...forcedDecisionResponse.reasons); - const forcedVariaton = forcedDecisionResponse.result; - if (forcedVariaton) { + const forcedVariation = forcedDecisionResponse.result; + if (forcedVariation) { return { - result: forcedVariaton.key, + result: forcedVariation.key, reasons: decideReasons, }; } @@ -1148,10 +1148,10 @@ export class DecisionService { const forcedDecisionResponse = this.findValidatedForcedDecision(configObj, user, flagKey, rule.key); decideReasons.push(...forcedDecisionResponse.reasons); - const forcedVariaton = forcedDecisionResponse.result; - if (forcedVariaton) { + const forcedVariation = forcedDecisionResponse.result; + if (forcedVariation) { return { - result: forcedVariaton, + result: forcedVariation, reasons: decideReasons, skipToEveryoneElse, }; @@ -1171,7 +1171,7 @@ export class DecisionService { configObj, rule, AUDIENCE_EVALUATION_TYPES.RULE, - attributes, + user, loggingKey ); decideReasons.push(...decisionifUserIsInAudience.reasons); diff --git a/packages/optimizely-sdk/lib/core/project_config/index.tests.js b/packages/optimizely-sdk/lib/core/project_config/index.tests.js index 6f22e3595..479d7c18c 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.tests.js @@ -33,13 +33,13 @@ import configValidator from '../../utils/config_validator'; var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); var logger = getLogger(); -describe('lib/core/project_config', function() { - describe('createProjectConfig method', function() { - it('should set properties correctly when createProjectConfig is called', function() { +describe('lib/core/project_config', function () { + describe('createProjectConfig method', function () { + it('should set properties correctly when createProjectConfig is called', function () { var testData = testDatafile.getTestProjectConfig(); var configObj = projectConfig.createProjectConfig(testData); - forEach(testData.audiences, function(audience) { + forEach(testData.audiences, function (audience) { audience.conditions = JSON.parse(audience.conditions); }); @@ -48,8 +48,8 @@ describe('lib/core/project_config', function() { assert.strictEqual(configObj.revision, testData.revision); assert.deepEqual(configObj.events, testData.events); assert.deepEqual(configObj.audiences, testData.audiences); - testData.groups.forEach(function(group) { - group.experiments.forEach(function(experiment) { + testData.groups.forEach(function (group) { + group.experiments.forEach(function (experiment) { experiment.groupId = group.id; experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); }); @@ -64,14 +64,14 @@ describe('lib/core/project_config', function() { assert.deepEqual(configObj.groupIdMap, expectedGroupIdMap); var expectedExperiments = testData.experiments; - forEach(configObj.groupIdMap, function(group, Id) { - forEach(group.experiments, function(experiment) { + forEach(configObj.groupIdMap, function (group, Id) { + forEach(group.experiments, function (experiment) { experiment.groupId = Id; expectedExperiments.push(experiment); }); }); - forEach(expectedExperiments, function(experiment) { + forEach(expectedExperiments, function (experiment) { experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); }); @@ -174,35 +174,35 @@ describe('lib/core/project_config', function() { }; }); - it('should not mutate the datafile', function() { + it('should not mutate the datafile', function () { var datafile = testDatafile.getTypedAudiencesConfig(); var datafileClone = cloneDeep(datafile); projectConfig.createProjectConfig(datafile); assert.deepEqual(datafileClone, datafile); }); - describe('feature management', function() { + describe('feature management', function () { var configObj; - beforeEach(function() { + beforeEach(function () { configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); }); - it('creates a rolloutIdMap from rollouts in the datafile', function() { + it('creates a rolloutIdMap from rollouts in the datafile', function () { assert.deepEqual(configObj.rolloutIdMap, testDatafile.datafileWithFeaturesExpectedData.rolloutIdMap); }); - it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function() { + it('creates a variationVariableUsageMap from rollouts and experiments with features in the datafile', function () { assert.deepEqual( configObj.variationVariableUsageMap, testDatafile.datafileWithFeaturesExpectedData.variationVariableUsageMap ); }); - it('creates a featureKeyMap from feature flags in the datafile', function() { + it('creates a featureKeyMap from feature flags in the datafile', function () { assert.deepEqual(configObj.featureKeyMap, testDatafile.datafileWithFeaturesExpectedData.featureKeyMap); }); - it('adds variations from rollout experiments to variationIdMap', function() { + it('adds variations from rollout experiments to variationIdMap', function () { assert.deepEqual(configObj.variationIdMap['594032'], { variables: [ { value: 'true', id: '4919852825313280' }, @@ -252,13 +252,13 @@ describe('lib/core/project_config', function() { }); }); - describe('flag variations', function() { + describe('flag variations', function () { var configObj; - beforeEach(function() { + beforeEach(function () { configObj = projectConfig.createProjectConfig(testDatafile.getTestDecideProjectConfig()); }); - it('it should populate flagVariationsMap correctly', function() { + it('it should populate flagVariationsMap correctly', function () { var allVariationsForFlag = configObj.flagVariationsMap; var feature1Variations = allVariationsForFlag.feature_1; var feature2Variations = allVariationsForFlag.feature_2; @@ -273,59 +273,59 @@ describe('lib/core/project_config', function() { return variation.key; }, {}); - assert.deepEqual(feature1VariationsKeys, [ 'a', 'b', '3324490633', '3324490562', '18257766532' ]); - assert.deepEqual(feature2VariationsKeys, [ 'variation_with_traffic', 'variation_no_traffic' ]); - assert.deepEqual(feature3VariationsKeys, [ ]); + assert.deepEqual(feature1VariationsKeys, ['a', 'b', '3324490633', '3324490562', '18257766532']); + assert.deepEqual(feature2VariationsKeys, ['variation_with_traffic', 'variation_no_traffic']); + assert.deepEqual(feature3VariationsKeys, []); }); }); }); - describe('projectConfig helper methods', function() { + describe('projectConfig helper methods', function () { var testData = cloneDeep(testDatafile.getTestProjectConfig()); var configObj; var createdLogger = loggerPlugin.createLogger({ logLevel: LOG_LEVEL.INFO }); - beforeEach(function() { + beforeEach(function () { configObj = projectConfig.createProjectConfig(cloneDeep(testData)); sinon.stub(createdLogger, 'log'); }); - afterEach(function() { + afterEach(function () { createdLogger.log.restore(); }); - it('should retrieve experiment ID for valid experiment key in getExperimentId', function() { + it('should retrieve experiment ID for valid experiment key in getExperimentId', function () { assert.strictEqual( projectConfig.getExperimentId(configObj, testData.experiments[0].key), testData.experiments[0].id ); }); - it('should throw error for invalid experiment key in getExperimentId', function() { - assert.throws(function() { + it('should throw error for invalid experiment key in getExperimentId', function () { + assert.throws(function () { projectConfig.getExperimentId(configObj, 'invalidExperimentKey'); }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); }); - it('should retrieve layer ID for valid experiment key in getLayerId', function() { + it('should retrieve layer ID for valid experiment key in getLayerId', function () { assert.strictEqual(projectConfig.getLayerId(configObj, '111127'), '4'); }); - it('should throw error for invalid experiment key in getLayerId', function() { - assert.throws(function() { + it('should throw error for invalid experiment key in getLayerId', function () { + assert.throws(function () { projectConfig.getLayerId(configObj, 'invalidExperimentKey'); }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentKey')); }); - it('should retrieve attribute ID for valid attribute key in getAttributeId', function() { + it('should retrieve attribute ID for valid attribute key in getAttributeId', function () { assert.strictEqual(projectConfig.getAttributeId(configObj, 'browser_type'), '111094'); }); - it('should retrieve attribute ID for reserved attribute key in getAttributeId', function() { + it('should retrieve attribute ID for reserved attribute key in getAttributeId', function () { assert.strictEqual(projectConfig.getAttributeId(configObj, '$opt_user_agent'), '$opt_user_agent'); }); - it('should return null for invalid attribute key in getAttributeId', function() { + it('should return null for invalid attribute key in getAttributeId', function () { assert.isNull(projectConfig.getAttributeId(configObj, 'invalidAttributeKey', createdLogger)); assert.strictEqual( buildLogMessageFromArgs(createdLogger.log.lastCall.args), @@ -333,7 +333,7 @@ describe('lib/core/project_config', function() { ); }); - it('should return null for invalid attribute key in getAttributeId', function() { + it('should return null for invalid attribute key in getAttributeId', function () { // Adding attribute in key map with reserved prefix configObj.attributeKeyMap['$opt_some_reserved_attribute'] = { id: '42', @@ -346,65 +346,65 @@ describe('lib/core/project_config', function() { ); }); - it('should retrieve event ID for valid event key in getEventId', function() { + it('should retrieve event ID for valid event key in getEventId', function () { assert.strictEqual(projectConfig.getEventId(configObj, 'testEvent'), '111095'); }); - it('should return null for invalid event key in getEventId', function() { + it('should return null for invalid event key in getEventId', function () { assert.isNull(projectConfig.getEventId(configObj, 'invalidEventKey')); }); - it('should retrieve experiment status for valid experiment key in getExperimentStatus', function() { + it('should retrieve experiment status for valid experiment key in getExperimentStatus', function () { assert.strictEqual( projectConfig.getExperimentStatus(configObj, testData.experiments[0].key), testData.experiments[0].status ); }); - it('should throw error for invalid experiment key in getExperimentStatus', function() { - assert.throws(function() { + it('should throw error for invalid experiment key in getExperimentStatus', function () { + assert.throws(function () { projectConfig.getExperimentStatus(configObj, 'invalidExperimentKey'); }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'PROJECT_CONFIG', 'invalidExperimentKey')); }); - it('should return true if experiment status is set to Running in isActive', function() { + it('should return true if experiment status is set to Running in isActive', function () { assert.isTrue(projectConfig.isActive(configObj, 'testExperiment')); }); - it('should return false if experiment status is not set to Running in isActive', function() { + it('should return false if experiment status is not set to Running in isActive', function () { assert.isFalse(projectConfig.isActive(configObj, 'testExperimentNotRunning')); }); - it('should return true if experiment status is set to Running in isRunning', function() { + it('should return true if experiment status is set to Running in isRunning', function () { assert.isTrue(projectConfig.isRunning(configObj, 'testExperiment')); }); - it('should return false if experiment status is not set to Running in isRunning', function() { + it('should return false if experiment status is not set to Running in isRunning', function () { assert.isFalse(projectConfig.isRunning(configObj, 'testExperimentLaunched')); }); - it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function() { + it('should retrieve variation key for valid experiment key and variation ID in getVariationKeyFromId', function () { assert.deepEqual( projectConfig.getVariationKeyFromId(configObj, testData.experiments[0].variations[0].id), testData.experiments[0].variations[0].key ); }); - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function () { assert.deepEqual( projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), testData.experiments[0].trafficAllocation ); }); - it('should throw error for invalid experient key in getTrafficAllocation', function() { - assert.throws(function() { + it('should throw error for invalid experient key in getTrafficAllocation', function () { + assert.throws(function () { projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentId')); }); - describe('#getVariationIdFromExperimentAndVariationKey', function() { - it('should return the variation id for the given experiment key and variation key', function() { + describe('#getVariationIdFromExperimentAndVariationKey', function () { + it('should return the variation id for the given experiment key and variation key', function () { assert.strictEqual( projectConfig.getVariationIdFromExperimentAndVariationKey( configObj, @@ -416,8 +416,8 @@ describe('lib/core/project_config', function() { }); }); - describe('#getSendFlagDecisionsValue', function() { - it('should return false when sendFlagDecisions is undefined', function() { + describe('#getSendFlagDecisionsValue', function () { + it('should return false when sendFlagDecisions is undefined', function () { configObj.sendFlagDecisions = undefined; assert.deepEqual( projectConfig.getSendFlagDecisionsValue(configObj), @@ -425,7 +425,7 @@ describe('lib/core/project_config', function() { ); }); - it('should return false when sendFlagDecisions is set to false', function() { + it('should return false when sendFlagDecisions is set to false', function () { configObj.sendFlagDecisions = false; assert.deepEqual( projectConfig.getSendFlagDecisionsValue(configObj), @@ -433,7 +433,7 @@ describe('lib/core/project_config', function() { ); }); - it('should return true when sendFlagDecisions is set to true', function() { + it('should return true when sendFlagDecisions is set to true', function () { configObj.sendFlagDecisions = true; assert.deepEqual( projectConfig.getSendFlagDecisionsValue(configObj), @@ -442,19 +442,19 @@ describe('lib/core/project_config', function() { }); }); - describe('feature management', function() { + describe('feature management', function () { var featureManagementLogger = loggerPlugin.createLogger({ logLevel: LOG_LEVEL.INFO }); - beforeEach(function() { + beforeEach(function () { configObj = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); sinon.stub(featureManagementLogger, 'log'); }); - afterEach(function() { + afterEach(function () { featureManagementLogger.log.restore(); }); - describe('getVariableForFeature', function() { - it('should return a variable object for a valid variable and feature key', function() { + describe('getVariableForFeature', function () { + it('should return a variable object for a valid variable and feature key', function () { var featureKey = 'test_feature_for_experiment'; var variableKey = 'num_buttons'; var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); @@ -466,45 +466,45 @@ describe('lib/core/project_config', function() { }); }); - it('should return null for an invalid variable key and a valid feature key', function() { + it('should return null for an invalid variable key and a valid feature key', function () { var featureKey = 'test_feature_for_experiment'; var variableKey = 'notARealVariable____'; var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); assert.strictEqual(result, null); sinon.assert.calledOnce(featureManagementLogger.log); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Variable with key "notARealVariable____" associated with feature with key "test_feature_for_experiment" is not in datafile.' ); }); - it('should return null for an invalid feature key', function() { + it('should return null for an invalid feature key', function () { var featureKey = 'notARealFeature_____'; var variableKey = 'num_buttons'; var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); assert.strictEqual(result, null); sinon.assert.calledOnce(featureManagementLogger.log); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.' ); }); - it('should return null for an invalid variable key and an invalid feature key', function() { + it('should return null for an invalid variable key and an invalid feature key', function () { var featureKey = 'notARealFeature_____'; var variableKey = 'notARealVariable____'; var result = projectConfig.getVariableForFeature(configObj, featureKey, variableKey, featureManagementLogger); assert.strictEqual(result, null); sinon.assert.calledOnce(featureManagementLogger.log); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Feature key notARealFeature_____ is not in datafile.' ); }); }); - describe('getVariableValueForVariation', function() { - it('returns a value for a valid variation and variable', function() { + describe('getVariableValueForVariation', function () { + it('returns a value for a valid variation and variable', function () { var variation = configObj.variationIdMap['594096']; var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; var result = projectConfig.getVariableValueForVariation( @@ -528,7 +528,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, '20.25'); }); - it('returns null for a null variation', function() { + it('returns null for a null variation', function () { var variation = null; var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; var result = projectConfig.getVariableValueForVariation( @@ -540,7 +540,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, null); }); - it('returns null for a null variable', function() { + it('returns null for a null variable', function () { var variation = configObj.variationIdMap['594096']; var variable = null; var result = projectConfig.getVariableValueForVariation( @@ -552,7 +552,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, null); }); - it('returns null for a null variation and null variable', function() { + it('returns null for a null variation and null variable', function () { var variation = null; var variable = null; var result = projectConfig.getVariableValueForVariation( @@ -564,7 +564,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, null); }); - it('returns null for a variation whose id is not in the datafile', function() { + it('returns null for a variation whose id is not in the datafile', function () { var variation = { key: 'some_variation', id: '999999999999', @@ -580,7 +580,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, null); }); - it('returns null if the variation does not have a value for this variable', function() { + it('returns null if the variation does not have a value for this variable', function () { var variation = configObj.variationIdMap['595008']; // This variation has no variable values associated with it var variable = configObj.featureKeyMap.test_feature_for_experiment.variableKeyMap.num_buttons; var result = projectConfig.getVariableValueForVariation( @@ -593,15 +593,15 @@ describe('lib/core/project_config', function() { }); }); - describe('getTypeCastValue', function() { - it('can cast a boolean', function() { + describe('getTypeCastValue', function () { + it('can cast a boolean', function () { var result = projectConfig.getTypeCastValue('true', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); assert.strictEqual(result, true); result = projectConfig.getTypeCastValue('false', FEATURE_VARIABLE_TYPES.BOOLEAN, featureManagementLogger); assert.strictEqual(result, false); }); - it('can cast an integer', function() { + it('can cast an integer', function () { var result = projectConfig.getTypeCastValue('50', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); assert.strictEqual(result, 50); var result = projectConfig.getTypeCastValue('-7', FEATURE_VARIABLE_TYPES.INTEGER, featureManagementLogger); @@ -610,7 +610,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, 0); }); - it('can cast a double', function() { + it('can cast a double', function () { var result = projectConfig.getTypeCastValue('89.99', FEATURE_VARIABLE_TYPES.DOUBLE, featureManagementLogger); assert.strictEqual(result, 89.99); var result = projectConfig.getTypeCastValue( @@ -625,7 +625,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, 10); }); - it('can return a string unmodified', function() { + it('can return a string unmodified', function () { var result = projectConfig.getTypeCastValue( 'message', FEATURE_VARIABLE_TYPES.STRING, @@ -634,7 +634,7 @@ describe('lib/core/project_config', function() { assert.strictEqual(result, 'message'); }); - it('returns null and logs an error for an invalid boolean', function() { + it('returns null and logs an error for an invalid boolean', function () { var result = projectConfig.getTypeCastValue( 'notabool', FEATURE_VARIABLE_TYPES.BOOLEAN, @@ -642,12 +642,12 @@ describe('lib/core/project_config', function() { ); assert.strictEqual(result, null); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Unable to cast value notabool to type boolean, returning null.' ); }); - it('returns null and logs an error for an invalid integer', function() { + it('returns null and logs an error for an invalid integer', function () { var result = projectConfig.getTypeCastValue( 'notanint', FEATURE_VARIABLE_TYPES.INTEGER, @@ -655,12 +655,12 @@ describe('lib/core/project_config', function() { ); assert.strictEqual(result, null); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Unable to cast value notanint to type integer, returning null.' ); }); - it('returns null and logs an error for an invalid double', function() { + it('returns null and logs an error for an invalid double', function () { var result = projectConfig.getTypeCastValue( 'notadouble', FEATURE_VARIABLE_TYPES.DOUBLE, @@ -668,39 +668,39 @@ describe('lib/core/project_config', function() { ); assert.strictEqual(result, null); assert.strictEqual( - buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), + buildLogMessageFromArgs(featureManagementLogger.log.lastCall.args), 'PROJECT_CONFIG: Unable to cast value notadouble to type double, returning null.' ); }); }); }); - describe('#getAudiencesById', function() { - beforeEach(function() { + describe('#getAudiencesById', function () { + beforeEach(function () { configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); }); - it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function() { + it('should retrieve audiences by checking first in typedAudiences, and then second in audiences', function () { assert.deepEqual(projectConfig.getAudiencesById(configObj), testDatafile.typedAudiencesById); }); }); - describe('#getExperimentAudienceConditions', function() { - it('should retrieve audiences for valid experiment key', function() { + describe('#getExperimentAudienceConditions', function () { + it('should retrieve audiences for valid experiment key', function () { configObj = projectConfig.createProjectConfig(cloneDeep(testData)); assert.deepEqual(projectConfig.getExperimentAudienceConditions(configObj, testData.experiments[1].id), [ '11154', ]); }); - it('should throw error for invalid experiment key', function() { + it('should throw error for invalid experiment key', function () { configObj = projectConfig.createProjectConfig(cloneDeep(testData)); - assert.throws(function() { + assert.throws(function () { projectConfig.getExperimentAudienceConditions(configObj, 'invalidExperimentId'); }, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, 'PROJECT_CONFIG', 'invalidExperimentId')); }); - it('should return experiment audienceIds if experiment has no audienceConditions', function() { + it('should return experiment audienceIds if experiment has no audienceConditions', function () { configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); var result = projectConfig.getExperimentAudienceConditions(configObj, '11564051718'); assert.deepEqual(result, [ @@ -714,7 +714,7 @@ describe('lib/core/project_config', function() { ]); }); - it('should return experiment audienceConditions if experiment has audienceConditions', function() { + it('should return experiment audienceConditions if experiment has audienceConditions', function () { configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig()); // audience_combinations_experiment has both audienceConditions and audienceIds // audienceConditions should be preferred over audienceIds @@ -727,20 +727,84 @@ describe('lib/core/project_config', function() { }); }); - describe('#isFeatureExperiment', function() { - it('returns true for a feature test', function() { + describe('#isFeatureExperiment', function () { + it('returns true for a feature test', function () { var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfigWithFeatures()); var result = projectConfig.isFeatureExperiment(config, '594098'); // id of 'testing_my_feature' assert.isTrue(result); }); - it('returns false for an A/B test', function() { + it('returns false for an A/B test', function () { var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' assert.isFalse(result); }); - it('returns true for a feature test in a mutex group', function() { + it('returns true for a feature test in a mutex group', function () { + var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); + var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' + assert.isTrue(result); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); + + describe('#getAudienceSegments', function () { + it('returns all qualified segments from an audience', function () { + const dummyQualifiedAudienceJson = { + "id": "13389142234", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-1" + }; + + const dummyQualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyQualifiedAudienceJson); + assert.deepEqual(dummyQualifiedAudienceJsonSegments, ['odp-segment-1']); + + const dummyUnqualifiedAudienceJson = { + "id": "13389142234", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "invalid" + } + ] + ] + ], + "name": "odp-segment-1" + }; + + const dummyUnqualifiedAudienceJsonSegments = projectConfig.getAudienceSegments(dummyUnqualifiedAudienceJson); + assert.deepEqual(dummyUnqualifiedAudienceJsonSegments, []); + }); + + it('returns false for an A/B test', function () { + var config = projectConfig.createProjectConfig(testDatafile.getTestProjectConfig()); + var result = projectConfig.isFeatureExperiment(config, '111127'); // id of 'testExperiment' + assert.isFalse(result); + }); + + it('returns true for a feature test in a mutex group', function () { var config = projectConfig.createProjectConfig(testDatafile.getMutexFeatureTestsConfig()); var result = projectConfig.isFeatureExperiment(config, '17128410791'); // id of 'f_test1' assert.isTrue(result); @@ -750,98 +814,166 @@ describe('lib/core/project_config', function() { }); }); - describe('#tryCreatingProjectConfig', function() { - var stubJsonSchemaValidator; - beforeEach(function() { - stubJsonSchemaValidator = { - validate: sinon.stub().returns(true), - }; - sinon.stub(configValidator, 'validateDatafile').returns(true); - sinon.spy(logger, 'error'); - }); - - afterEach(function() { - configValidator.validateDatafile.restore(); - logger.error.restore(); - }); - - it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function() { - var configDatafile = { - foo: 'bar', - experiments: [ - {key: 'a'}, - {key: 'b'} - ] - } - configValidator.validateDatafile.returns(configDatafile); - var configObj = { - foo: 'bar', - experimentKeyMap: { - "a": { key: "a", variationKeyMap: {} }, - "b": { key: "b", variationKeyMap: {} } - }, - }; + describe('integrations', () => { - stubJsonSchemaValidator.validate.returns(true); + describe('#withSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithSegments()); + }); - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 3); }); - assert.deepInclude(result.configObj, configObj) + it('should populate the public key value from the odp integration', () => { + assert.exists(config.publicKeyForOdp) + }) + + it('should populate the host value from the odp integration', () => { + assert.exists(config.hostForOdp) + }) + + it('should contain all expected unique odp segments in allSegments', () => { + assert.equal(config.allSegments.size, 3) + assert.deepEqual(config.allSegments, new Set(['odp-segment-1', 'odp-segment-2', 'odp-segment-3'])) + }) }); - it('returns an error when validateDatafile throws', function() { - configValidator.validateDatafile.throws(); - stubJsonSchemaValidator.validate.returns(true); - var { error } = projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, + describe('#withoutSegments', () => { + var config; + beforeEach(() => { + config = projectConfig.createProjectConfig(testDatafile.getOdpIntegratedConfigWithoutSegments()); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.exists(config.integrations); + assert.equal(config.integrations.length, 3); }); - assert.isNotNull(error); + + it('should populate the public key value from the odp integration', () => { + assert.exists(config.publicKeyForOdp) + assert.equal(config.publicKeyForOdp, 'W4WzcEs-ABgXorzY7h1LCQ') + }) + + it('should populate the host value from the odp integration', () => { + assert.exists(config.hostForOdp) + assert.equal(config.hostForOdp, 'https://api.zaius.com') + }) + + it('should contain all expected unique odp segments in all segments', () => { + assert.equal(config.allSegments.size, 0) + }) }); - it('returns an error when jsonSchemaValidator.validate throws', function() { - configValidator.validateDatafile.returns(true); - stubJsonSchemaValidator.validate.throws(); - var { error } = projectConfig.tryCreatingProjectConfig({ - datafile: { foo: 'bar' }, - jsonSchemaValidator: stubJsonSchemaValidator, - logger: logger, + describe('#withoutIntegrations', () => { + var config; + beforeEach(() => { + const odpIntegratedConfigWithSegments = testDatafile.getOdpIntegratedConfigWithSegments() + const noIntegrationsConfigWithSegments = { ...odpIntegratedConfigWithSegments, integrations: [] } + config = projectConfig.createProjectConfig(noIntegrationsConfigWithSegments); + }); + + it('should convert integrations from the datafile into the project config', () => { + assert.equal(config.integrations.length, 0); }); - assert.isNotNull(error); }); - it('skips json validation when jsonSchemaValidator is not provided', function() { + }) +}); - var configDatafile = { - foo: 'bar', - experiments: [ - {key: 'a'}, - {key: 'b'} - ] - } +describe('#tryCreatingProjectConfig', function () { + var stubJsonSchemaValidator; + beforeEach(function () { + stubJsonSchemaValidator = { + validate: sinon.stub().returns(true), + }; + sinon.stub(configValidator, 'validateDatafile').returns(true); + sinon.spy(logger, 'error'); + }); - configValidator.validateDatafile.returns(configDatafile); + afterEach(function () { + configValidator.validateDatafile.restore(); + logger.error.restore(); + }); - var configObj = { - foo: 'bar', - experimentKeyMap: { - a: { key: 'a', variationKeyMap: {} }, - b: { key: 'b', variationKeyMap: {} }, - }, - }; + it('returns a project config object created by createProjectConfig when all validation is applied and there are no errors', function () { + var configDatafile = { + foo: 'bar', + experiments: [ + { key: 'a' }, + { key: 'b' } + ] + } + configValidator.validateDatafile.returns(configDatafile); + var configObj = { + foo: 'bar', + experimentKeyMap: { + "a": { key: "a", variationKeyMap: {} }, + "b": { key: "b", variationKeyMap: {} } + }, + }; + + stubJsonSchemaValidator.validate.returns(true); + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + + assert.deepInclude(result.configObj, configObj) + }); - var result = projectConfig.tryCreatingProjectConfig({ - datafile: configDatafile, - logger: logger, - }); + it('returns an error when validateDatafile throws', function () { + configValidator.validateDatafile.throws(); + stubJsonSchemaValidator.validate.returns(true); + var { error } = projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + assert.isNotNull(error); + }); + + it('returns an error when jsonSchemaValidator.validate throws', function () { + configValidator.validateDatafile.returns(true); + stubJsonSchemaValidator.validate.throws(); + var { error } = projectConfig.tryCreatingProjectConfig({ + datafile: { foo: 'bar' }, + jsonSchemaValidator: stubJsonSchemaValidator, + logger: logger, + }); + assert.isNotNull(error); + }); + + it('skips json validation when jsonSchemaValidator is not provided', function () { + + var configDatafile = { + foo: 'bar', + experiments: [ + { key: 'a' }, + { key: 'b' } + ] + } - assert.deepInclude(result.configObj, configObj); - sinon.assert.notCalled(logger.error); + configValidator.validateDatafile.returns(configDatafile); + + var configObj = { + foo: 'bar', + experimentKeyMap: { + a: { key: 'a', variationKeyMap: {} }, + b: { key: 'b', variationKeyMap: {} }, + }, + }; + + var result = projectConfig.tryCreatingProjectConfig({ + datafile: configDatafile, + logger: logger, }); + + assert.deepInclude(result.configObj, configObj); + sinon.assert.notCalled(logger.error); }); }); diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index c8b9ec4bd..9733f7d49 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -43,6 +43,7 @@ import { Variation, VariableType, VariationVariable, + Integration, } from '../../shared_types'; interface TryCreatingProjectConfigConfig { @@ -98,6 +99,11 @@ export interface ProjectConfig { accountId: string; flagRulesMap: { [key: string]: Experiment[] }; flagVariationsMap: { [key: string]: Variation[] }; + integrations: Integration[]; + integrationKeyMap?: { [key: string]: Integration }; + publicKeyForOdp?: string; + hostForOdp?: string; + allSegments: Set; } const EXPERIMENT_RUNNING_STATUS = 'Running'; @@ -143,7 +149,7 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { * @param {string|null} datafileStr JSON string representation of the datafile * @return {ProjectConfig} Object representing project configuration */ -export const createProjectConfig = function( +export const createProjectConfig = function ( datafileObj?: JSON, datafileStr: string | null = null ): ProjectConfig { @@ -161,6 +167,16 @@ export const createProjectConfig = function( projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id'); assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id')); + projectConfig.allSegments = new Set([]) + + Object.keys(projectConfig.audiencesById) + .map((audience) => getAudienceSegments(projectConfig.audiencesById[audience])) + .forEach(audienceSegments => { + audienceSegments.forEach(segment => { + projectConfig.allSegments.add(segment) + }) + }) + projectConfig.attributeKeyMap = keyBy(projectConfig.attributes, 'key'); projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key'); projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id'); @@ -184,6 +200,16 @@ export const createProjectConfig = function( } ); + if (projectConfig.integrations) { + projectConfig.integrationKeyMap = keyBy(projectConfig.integrations, 'key'); + projectConfig.integrations + .filter((integration) => integration.key === 'odp') + .forEach((integration) => { + if (integration.publicKey) projectConfig.publicKeyForOdp = integration.publicKey + if (integration.host) projectConfig.hostForOdp = integration.host + }) + } + projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key'); projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id'); @@ -272,6 +298,37 @@ export const createProjectConfig = function( return projectConfig; }; +/** + * Extract all audience segments used in this audience's conditions + * @param {Audience} audience Object representing the audience being parsed + * @return {string[]} List of all audience segments + */ +export const getAudienceSegments = function (audience: Audience): string[] { + if (!audience.conditions) return [] + return getSegmentsFromConditions(audience.conditions); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getSegmentsFromConditions = (condition: any): string[] => { + const segments = []; + + if (isLogicalOperator(condition)) { + return [] + } + else if (Array.isArray(condition)) { + condition.forEach((nextCondition) => segments.push(...getSegmentsFromConditions(nextCondition))) + } + else if (condition['match'] === 'qualified') { + segments.push(condition['value']) + } + + return segments; +} + +function isLogicalOperator(condition: string): boolean { + return ['and', 'or', 'not'].includes(condition) +} + /** * Get experiment ID for the provided experiment key * @param {ProjectConfig} projectConfig Object representing project configuration @@ -279,7 +336,7 @@ export const createProjectConfig = function( * @return {string} Experiment ID corresponding to the provided experiment key * @throws If experiment key is not in datafile */ -export const getExperimentId = function(projectConfig: ProjectConfig, experimentKey: string): string { +export const getExperimentId = function (projectConfig: ProjectConfig, experimentKey: string): string { const experiment = projectConfig.experimentKeyMap[experimentKey]; if (!experiment) { throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); @@ -294,7 +351,7 @@ export const getExperimentId = function(projectConfig: ProjectConfig, experiment * @return {string} Layer ID corresponding to the provided experiment key * @throws If experiment key is not in datafile */ -export const getLayerId = function(projectConfig: ProjectConfig, experimentId: string): string { +export const getLayerId = function (projectConfig: ProjectConfig, experimentId: string): string { const experiment = projectConfig.experimentIdMap[experimentId]; if (!experiment) { throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME, experimentId)); @@ -309,7 +366,7 @@ export const getLayerId = function(projectConfig: ProjectConfig, experimentId: s * @param {LogHandler} logger * @return {string|null} Attribute ID corresponding to the provided attribute key. Attribute key if it is a reserved attribute. */ -export const getAttributeId = function( +export const getAttributeId = function ( projectConfig: ProjectConfig, attributeKey: string, logger: LogHandler @@ -340,7 +397,7 @@ export const getAttributeId = function( * @param {string} eventKey Event key for which ID is to be determined * @return {string|null} Event ID corresponding to the provided event key */ -export const getEventId = function(projectConfig: ProjectConfig, eventKey: string): string | null { +export const getEventId = function (projectConfig: ProjectConfig, eventKey: string): string | null { const event = projectConfig.eventKeyMap[eventKey]; if (event) { return event.id; @@ -355,7 +412,7 @@ export const getEventId = function(projectConfig: ProjectConfig, eventKey: strin * @return {string} Experiment status corresponding to the provided experiment key * @throws If experiment key is not in datafile */ -export const getExperimentStatus = function(projectConfig: ProjectConfig, experimentKey: string): string { +export const getExperimentStatus = function (projectConfig: ProjectConfig, experimentKey: string): string { const experiment = projectConfig.experimentKeyMap[experimentKey]; if (!experiment) { throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME, experimentKey)); @@ -369,7 +426,7 @@ export const getExperimentStatus = function(projectConfig: ProjectConfig, experi * @param {string} experimentKey Experiment key for which status is to be compared with 'Running' * @return {boolean} True if experiment status is set to 'Running', false otherwise */ -export const isActive = function(projectConfig: ProjectConfig, experimentKey: string): boolean { +export const isActive = function (projectConfig: ProjectConfig, experimentKey: string): boolean { return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; }; @@ -381,7 +438,7 @@ export const isActive = function(projectConfig: ProjectConfig, experimentKey: st * False if the experiment is not running * */ -export const isRunning = function(projectConfig: ProjectConfig, experimentKey: string): boolean { +export const isRunning = function (projectConfig: ProjectConfig, experimentKey: string): boolean { return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS; }; @@ -394,7 +451,7 @@ export const isRunning = function(projectConfig: ProjectConfig, experimentKey: s * Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"] * @throws If experiment key is not in datafile */ -export const getExperimentAudienceConditions = function( +export const getExperimentAudienceConditions = function ( projectConfig: ProjectConfig, experimentId: string ): Array { @@ -412,7 +469,7 @@ export const getExperimentAudienceConditions = function( * @param {string} variationId ID of the variation * @return {string|null} Variation key or null if the variation ID is not found */ -export const getVariationKeyFromId = function(projectConfig: ProjectConfig, variationId: string): string | null { +export const getVariationKeyFromId = function (projectConfig: ProjectConfig, variationId: string): string | null { if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { return projectConfig.variationIdMap[variationId].key; } @@ -426,7 +483,7 @@ export const getVariationKeyFromId = function(projectConfig: ProjectConfig, vari * @param {string} variationId ID of the variation * @return {Variation|null} Variation or null if the variation ID is not found */ - export const getVariationFromId = function(projectConfig: ProjectConfig, variationId: string): Variation | null { +export const getVariationFromId = function (projectConfig: ProjectConfig, variationId: string): Variation | null { if (projectConfig.variationIdMap.hasOwnProperty(variationId)) { return projectConfig.variationIdMap[variationId]; } @@ -441,7 +498,7 @@ export const getVariationKeyFromId = function(projectConfig: ProjectConfig, vari * @param {string} variationKey The variation key * @return {string|null} Variation ID or null */ -export const getVariationIdFromExperimentAndVariationKey = function( +export const getVariationIdFromExperimentAndVariationKey = function ( projectConfig: ProjectConfig, experimentKey: string, variationKey: string @@ -461,7 +518,7 @@ export const getVariationIdFromExperimentAndVariationKey = function( * @return {Experiment} Experiment * @throws If experiment key is not in datafile */ -export const getExperimentFromKey = function(projectConfig: ProjectConfig, experimentKey: string): Experiment { +export const getExperimentFromKey = function (projectConfig: ProjectConfig, experimentKey: string): Experiment { if (projectConfig.experimentKeyMap.hasOwnProperty(experimentKey)) { const experiment = projectConfig.experimentKeyMap[experimentKey]; if (experiment) { @@ -479,7 +536,7 @@ export const getExperimentFromKey = function(projectConfig: ProjectConfig, exper * @return {TrafficAllocation[]} Traffic allocation for the experiment * @throws If experiment key is not in datafile */ -export const getTrafficAllocation = function(projectConfig: ProjectConfig, experimentId: string): TrafficAllocation[] { +export const getTrafficAllocation = function (projectConfig: ProjectConfig, experimentId: string): TrafficAllocation[] { const experiment = projectConfig.experimentIdMap[experimentId]; if (!experiment) { throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME, experimentId)); @@ -495,7 +552,7 @@ export const getTrafficAllocation = function(projectConfig: ProjectConfig, exper * @param {LogHandler} logger * @return {Experiment|null} Experiment object or null */ -export const getExperimentFromId = function( +export const getExperimentFromId = function ( projectConfig: ProjectConfig, experimentId: string, logger: LogHandler @@ -517,7 +574,7 @@ export const getExperimentFromId = function( * @param {variationKey} string * @return {Variation|null} */ -export const getFlagVariationByKey = function(projectConfig: ProjectConfig, flagKey: string, variationKey: string): Variation | null { +export const getFlagVariationByKey = function (projectConfig: ProjectConfig, flagKey: string, variationKey: string): Variation | null { if (!projectConfig) { return null; } @@ -540,7 +597,7 @@ export const getFlagVariationByKey = function(projectConfig: ProjectConfig, flag * @return {FeatureFlag|null} Feature object, or null if no feature with the given * key exists */ -export const getFeatureFromKey = function( +export const getFeatureFromKey = function ( projectConfig: ProjectConfig, featureKey: string, logger: LogHandler @@ -567,7 +624,7 @@ export const getFeatureFromKey = function( * @return {FeatureVariable|null} Variable object, or null one or both of the given * feature and variable keys are invalid */ -export const getVariableForFeature = function( +export const getVariableForFeature = function ( projectConfig: ProjectConfig, featureKey: string, variableKey: string, @@ -606,7 +663,7 @@ export const getVariableForFeature = function( * variation, or null if the given variable has no value * for the given variation or if the variation or variable are invalid */ -export const getVariableValueForVariation = function( +export const getVariableValueForVariation = function ( projectConfig: ProjectConfig, variable: FeatureVariable, variation: Variation, @@ -648,7 +705,7 @@ export const getVariableValueForVariation = function( * @returns {*} Variable value of the appropriate type, or * null if the type cast failed */ -export const getTypeCastValue = function( +export const getTypeCastValue = function ( variableValue: string, variableType: VariableType, logger: LogHandler @@ -729,7 +786,7 @@ export const getTypeCastValue = function( * @param {ProjectConfig} projectConfig * @returns {{ [id: string]: Audience }} */ -export const getAudiencesById = function(projectConfig: ProjectConfig): { [id: string]: Audience } { +export const getAudiencesById = function (projectConfig: ProjectConfig): { [id: string]: Audience } { return projectConfig.audiencesById; }; @@ -739,7 +796,7 @@ export const getAudiencesById = function(projectConfig: ProjectConfig): { [id: s * @param {string} eventKey * @returns {boolean} */ -export const eventWithKeyExists = function(projectConfig: ProjectConfig, eventKey: string): boolean { +export const eventWithKeyExists = function (projectConfig: ProjectConfig, eventKey: string): boolean { return projectConfig.eventKeyMap.hasOwnProperty(eventKey); }; @@ -749,7 +806,7 @@ export const eventWithKeyExists = function(projectConfig: ProjectConfig, eventKe * @param {string} experimentId * @returns {boolean} */ -export const isFeatureExperiment = function(projectConfig: ProjectConfig, experimentId: string): boolean { +export const isFeatureExperiment = function (projectConfig: ProjectConfig, experimentId: string): boolean { return projectConfig.experimentFeatureMap.hasOwnProperty(experimentId); }; @@ -758,7 +815,7 @@ export const isFeatureExperiment = function(projectConfig: ProjectConfig, experi * @param {ProjectConfig} projectConfig * @returns {string} */ -export const toDatafile = function(projectConfig: ProjectConfig): string { +export const toDatafile = function (projectConfig: ProjectConfig): string { return projectConfig.__datafileStr; } @@ -780,7 +837,7 @@ export const toDatafile = function(projectConfig: ProjectConfig): string { * @param {Object} config.logger * @returns {Object} Object containing configObj and error properties */ -export const tryCreatingProjectConfig = function( +export const tryCreatingProjectConfig = function ( config: TryCreatingProjectConfigConfig ): { configObj: ProjectConfig | null; error: Error | null } { let newDatafileObj; @@ -820,7 +877,7 @@ export const tryCreatingProjectConfig = function( * @param {ProjectConfig} projectConfig * @return {boolean} A boolean value that indicates if we should send flag decisions */ -export const getSendFlagDecisionsValue = function(projectConfig: ProjectConfig): boolean { +export const getSendFlagDecisionsValue = function (projectConfig: ProjectConfig): boolean { return !!projectConfig.sendFlagDecisions; } @@ -847,6 +904,7 @@ export default { getTypeCastValue, getSendFlagDecisionsValue, getAudiencesById, + getAudienceSegments, eventWithKeyExists, isFeatureExperiment, toDatafile, diff --git a/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts b/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts index e21830b89..5830807a7 100644 --- a/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts +++ b/packages/optimizely-sdk/lib/core/project_config/project_config_schema.ts @@ -17,9 +17,9 @@ /** * Project Config JSON Schema file used to validate the project json datafile */ - import { JSONSchema4 } from 'json-schema'; +import { JSONSchema4 } from 'json-schema'; - var schemaDefinition = { +var schemaDefinition = { $schema: 'http://json-schema.org/draft-04/schema#', type: 'object', properties: { @@ -275,6 +275,24 @@ type: 'string', required: true, }, + integrations: { + type: 'array', + items: { + type: 'object', + properties: { + key: { + type: 'string', + required: true + }, + host: { + type: 'string' + }, + publicKey: { + type: 'string' + } + } + } + } }, }; diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 2a4c45f1c..e29a4af43 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -9166,6 +9166,7 @@ describe('lib/optimizely', function() { describe('audience combinations', function() { var sandbox = sinon.sandbox.create(); + var evalSpy; var createdLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO, logToConsole: false, @@ -9195,7 +9196,7 @@ describe('lib/optimizely', function() { sandbox.stub(errorHandler, 'handleError'); sandbox.stub(createdLogger, 'log'); - sandbox.spy(audienceEvaluator, 'evaluate'); + evalSpy = sandbox.spy(audienceEvaluator, 'evaluate'); }); afterEach(function() { @@ -9219,8 +9220,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Welcome to Slytherin!', lasers: 45.5 } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Welcome to Slytherin!', lasers: 45.5 }); }); it('can exclude a user from an experiment with complex audience conditions', function() { @@ -9236,8 +9238,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[2].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Hufflepuff', lasers: 45.5 } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Hufflepuff', lasers: 45.5 }); }); it('can track an experiment with complex audience conditions', function() { @@ -9266,8 +9269,9 @@ 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' } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: '...Slytherinnn...sss.', favorite_ice_cream: 'matcha' }); }); it('can exclude a user from a rollout with complex audience conditions via isFeatureEnabled', function() { @@ -9281,8 +9285,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().rollouts[2].experiments[0].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Lannister' } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Lannister' }); }); it('can return a variable value from a feature test with complex audience conditions via getFeatureVariableString', function() { @@ -9297,8 +9302,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Gryffindor', lasers: 700 } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Gryffindor', lasers: 700 }); }); it('can return a variable value from a feature test with complex audience conditions via getFeatureVariable', function() { @@ -9313,8 +9319,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - { house: 'Gryffindor', lasers: 700 } + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), { house: 'Gryffindor', lasers: 700 }); }); it('can return the default value for a feature variable from getFeatureVariable, via excluding a user from a feature test with complex audience conditions', function() { @@ -9326,8 +9333,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - {} + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), {}); }); it('can return the default value for a feature variable from getFeatureVariableString, via excluding a user from a feature test with complex audience conditions', function() { @@ -9339,8 +9347,9 @@ describe('lib/optimizely', function() { audienceEvaluator.evaluate, optlyInstance.projectConfigManager.getConfig().experiments[3].audienceConditions, optlyInstance.projectConfigManager.getConfig().audiencesById, - {} + sinon.match.any ); + assert.deepEqual(evalSpy.getCalls()[0].args[2].getAttributes(), {}); }); }); diff --git a/packages/optimizely-sdk/lib/optimizely_user_context/index.ts b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts index 7ab5f0a22..83bca5f97 100644 --- a/packages/optimizely-sdk/lib/optimizely_user_context/index.ts +++ b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts @@ -29,6 +29,7 @@ export default class OptimizelyUserContext { private userId: string; private attributes: UserAttributes; private forcedDecisionsMap: { [key: string]: { [key: string]: OptimizelyForcedDecision } }; + private _qualifiedSegments: string[] = []; constructor({ optimizely, @@ -66,6 +67,14 @@ export default class OptimizelyUserContext { return this.optimizely; } + public get qualifiedSegments(): string[] { + return this._qualifiedSegments; + } + + public set qualifiedSegments(qualifiedSegments: string[]) { + this._qualifiedSegments = [...qualifiedSegments]; + } + /** * Returns a decision result for a given flag key and a user context, which contains all data required to deliver the flag. * If the SDK finds an error, it will return a decision with null for variationKey. The decision will include an error message in reasons. @@ -128,7 +137,7 @@ export default class OptimizelyUserContext { const flagKey = context.flagKey; const ruleKey = context.ruleKey ?? CONTROL_ATTRIBUTES.FORCED_DECISION_NULL_RULE_KEY; - const variationKey = decision.variationKey; + const variationKey = decision.variationKey; const forcedDecision = { variationKey }; if (!this.forcedDecisionsMap[flagKey]) { @@ -203,6 +212,10 @@ export default class OptimizelyUserContext { return null; } + public isQualifiedFor(segment: string): boolean { + return this._qualifiedSegments.indexOf(segment) > -1; + } + private cloneUserContext(): OptimizelyUserContext { const userContext = new OptimizelyUserContext({ optimizely: this.getOptimizely(), @@ -214,6 +227,10 @@ export default class OptimizelyUserContext { userContext.forcedDecisionsMap = { ...this.forcedDecisionsMap }; } + if (this._qualifiedSegments) { + userContext._qualifiedSegments = [...this._qualifiedSegments]; + } + return userContext; } } diff --git a/packages/optimizely-sdk/lib/plugins/logger/index.react_native.tests.js b/packages/optimizely-sdk/lib/plugins/logger/index.react_native.tests.js index a62ec3c5a..7ea19e98a 100644 --- a/packages/optimizely-sdk/lib/plugins/logger/index.react_native.tests.js +++ b/packages/optimizely-sdk/lib/plugins/logger/index.react_native.tests.js @@ -46,7 +46,7 @@ describe('lib/plugins/logger/react_native', function() { console.error.restore(); }); - it('shoud use console.info when log level is info', function() { + it('should use console.info when log level is info', function() { defaultLogger.log(LOG_LEVEL.INFO, 'message'); sinon.assert.calledWithExactly(console.info, sinon.match(/.*INFO.*message.*/)); sinon.assert.notCalled(console.log); @@ -54,7 +54,7 @@ describe('lib/plugins/logger/react_native', function() { sinon.assert.notCalled(console.error); }); - it('shoud use console.log when log level is debug', function() { + it('should use console.log when log level is debug', function() { defaultLogger.log(LOG_LEVEL.DEBUG, 'message'); sinon.assert.calledWithExactly(console.log, sinon.match(/.*DEBUG.*message.*/)); sinon.assert.notCalled(console.info); @@ -62,7 +62,7 @@ describe('lib/plugins/logger/react_native', function() { sinon.assert.notCalled(console.error); }); - it('shoud use console.warn when log level is warn', function() { + it('should use console.warn when log level is warn', function() { defaultLogger.log(LOG_LEVEL.WARNING, 'message'); sinon.assert.calledWithExactly(console.warn, sinon.match(/.*WARNING.*message.*/)); sinon.assert.notCalled(console.log); @@ -70,7 +70,7 @@ describe('lib/plugins/logger/react_native', function() { sinon.assert.notCalled(console.error); }); - it('shoud use console.warn when log level is error', function() { + it('should use console.warn when log level is error', function() { defaultLogger.log(LOG_LEVEL.ERROR, 'message'); sinon.assert.calledWithExactly(console.warn, sinon.match(/.*ERROR.*message.*/)); sinon.assert.notCalled(console.log); diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index b6b1527ca..36bab45dd 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -16,7 +16,7 @@ import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from '@optimizely/js-sdk-logging'; import { EventProcessor } from '@optimizely/js-sdk-event-processor'; -import {NotificationCenter as NotificationCenterImpl} from './core/notification_center' +import { NotificationCenter as NotificationCenterImpl } from './core/notification_center' import { NOTIFICATION_TYPES } from './utils/enums'; export interface BucketerParams { @@ -183,6 +183,12 @@ export interface Audience { conditions: unknown[] | string; } +export interface Integration { + key: string; + host?: string; + publicKey?: string; +} + export interface TrafficAllocation { entityId: string; endOfRange: number; @@ -372,7 +378,7 @@ export interface TrackListenerPayload extends ListenerPayload { * Entry level Config Entities * For compatibility with the previous declaration file */ - export interface Config extends ConfigLite { +export interface Config extends ConfigLite { // options for Datafile Manager datafileOptions?: DatafileOptions; // limit of events to dispatch in a batch @@ -389,7 +395,7 @@ export interface TrackListenerPayload extends ListenerPayload { * Entry level Config Entities for Lite bundle * For compatibility with the previous declaration file */ - export interface ConfigLite { +export interface ConfigLite { // Datafile string // TODO[OASIS-6649]: Don't use object type // eslint-disable-next-line @typescript-eslint/ban-types @@ -503,6 +509,7 @@ export interface OptimizelyUserContext { getForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null; removeForcedDecision(context: OptimizelyDecisionContext): boolean; removeAllForcedDecisions(): boolean; + isQualifiedFor(segment: string): boolean; } export interface OptimizelyDecision { diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js index f35a47cd7..849cc5e86 100644 --- a/packages/optimizely-sdk/lib/tests/test_data.js +++ b/packages/optimizely-sdk/lib/tests/test_data.js @@ -705,11 +705,11 @@ export var getParsedAudiences = [ }, ]; -export var getTestProjectConfig = function() { +export var getTestProjectConfig = function () { return cloneDeep(config); }; -export var getTestDecideProjectConfig = function() { +export var getTestDecideProjectConfig = function () { return cloneDeep(decideConfig); }; @@ -816,7 +816,7 @@ var configWithFeatures = { key: 'button_txt', id: '5636734406623232', defaultValue: 'Buy me', - }, + }, { type: 'double', key: 'button_width', @@ -1044,7 +1044,7 @@ var configWithFeatures = { key: 'test_experiment3', status: 'Running', layerId: '6', - audienceConditions : [ + audienceConditions: [ "or", "11160" ], @@ -1117,8 +1117,8 @@ var configWithFeatures = { status: 'Running', layerId: '8', audienceConditions: [ - "or", - "11160" + "or", + "11160" ], audienceIds: ['11160'], id: '111136', @@ -1158,8 +1158,8 @@ var configWithFeatures = { id: '11160', name: 'Test attribute users 3', conditions: - '["and", ["or", ["or", {"match": "exact", "name": "experiment_attr", "type": "custom_attribute", "value": "group_experiment"}]]]', - } + '["and", ["or", ["or", {"match": "exact", "name": "experiment_attr", "type": "custom_attribute", "value": "group_experiment"}]]]', + } ], revision: '35', groups: [ @@ -1316,17 +1316,17 @@ var configWithFeatures = { key: 'group_2_exp_1', status: 'Running', audienceConditions: [ - "or", - "11160" + "or", + "11160" ], audienceIds: ['11160'], layerId: '211183', variations: [ - { - key: 'var_1', - id: '38901', - featureEnabled: false, - }, + { + key: 'var_1', + id: '38901', + featureEnabled: false, + }, ], forcedVariations: {}, trafficAllocation: [ @@ -1348,8 +1348,8 @@ var configWithFeatures = { key: 'group_2_exp_2', status: 'Running', audienceConditions: [ - "or", - "11160" + "or", + "11160" ], audienceIds: ['11160'], layerId: '211184', @@ -1373,8 +1373,8 @@ var configWithFeatures = { key: 'group_2_exp_3', status: 'Running', audienceConditions: [ - "or", - "11160" + "or", + "11160" ], audienceIds: ['11160'], layerId: '211185', @@ -1638,7 +1638,7 @@ var configWithFeatures = { variables: [], }; -export var getTestProjectConfigWithFeatures = function() { +export var getTestProjectConfigWithFeatures = function () { return cloneDeep(configWithFeatures); }; @@ -2003,7 +2003,7 @@ export var datafileWithFeaturesExpectedData = { value: 'Hello audience', }, 8765345281230956: { - id:'8765345281230956', + id: '8765345281230956', value: '{ "count": 2, "message": "Hello audience" }', } }, @@ -2025,7 +2025,7 @@ export var datafileWithFeaturesExpectedData = { value: 'Hello', }, 8765345281230956: { - id:'8765345281230956', + id: '8765345281230956', value: '{ "count": 1, "message": "Hello" }', } }, @@ -2083,7 +2083,7 @@ export var datafileWithFeaturesExpectedData = { id: '6199684360044544', }, 1547854156498475: { - id:'1547854156498475', + id: '1547854156498475', value: '{ "num_buttons": 1, "text": "first variation"}', }, }, @@ -2105,7 +2105,7 @@ export var datafileWithFeaturesExpectedData = { id: '6199684360044544', }, 1547854156498475: { - id:'1547854156498475', + id: '1547854156498475', value: '{ "num_buttons": 2, "text": "second variation"}', }, }, @@ -2127,7 +2127,7 @@ export var datafileWithFeaturesExpectedData = { id: '6199684360044544', }, 1547854156498475: { - id:'1547854156498475', + id: '1547854156498475', value: '{ "num_buttons": 3, "text": "third variation"}', }, }, @@ -2741,7 +2741,7 @@ var unsupportedVersionConfig = { projectId: '111001', }; -export var getUnsupportedVersionConfig = function() { +export var getUnsupportedVersionConfig = function () { return cloneDeep(unsupportedVersionConfig); }; @@ -3141,10 +3141,267 @@ var typedAudiencesConfig = { revision: '3', }; -export var getTypedAudiencesConfig = function() { +export var getTypedAudiencesConfig = function () { return cloneDeep(typedAudiencesConfig); }; +var odpIntegratedConfigWithSegments = { + "version": "4", + "sendFlagDecisions": true, + "rollouts": [ + { + "experiments": [ + { + "audienceIds": ["13389130056"], + "forcedVariations": {}, + "id": "3332020515", + "key": "rollout-rule-1", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 10000, + "entityId": "3324490633" + } + ], + "variations": [ + { + "featureEnabled": true, + "id": "3324490633", + "key": "rollout-variation-on", + "variables": [] + } + ] + }, + { + "audienceIds": [], + "forcedVariations": {}, + "id": "3332020556", + "key": "rollout-rule-2", + "layerId": "3319450668", + "status": "Running", + "trafficAllocation": [ + { + "endOfRange": 10000, + "entityId": "3324490644" + } + ], + "variations": [ + { + "featureEnabled": false, + "id": "3324490644", + "key": "rollout-variation-off", + "variables": [] + } + ] + } + ], + "id": "3319450668" + } + ], + "anonymizeIP": true, + "botFiltering": true, + "projectId": "10431130345", + "variables": [], + "featureFlags": [ + { + "experimentIds": ["10390977673"], + "id": "4482920077", + "key": "flag-segment", + "rolloutId": "3319450668", + "variables": [ + { + "defaultValue": "42", + "id": "2687470095", + "key": "i_42", + "type": "integer" + } + ] + } + ], + "experiments": [ + { + "status": "Running", + "key": "experiment-segment", + "layerId": "10420273888", + "trafficAllocation": [ + { + "entityId": "10389729780", + "endOfRange": 10000 + } + ], + "audienceIds": ["$opt_dummy_audience"], + "audienceConditions": ["or", "13389142234", "13389141123"], + "variations": [ + { + "variables": [], + "featureEnabled": true, + "id": "10389729780", + "key": "variation-a" + }, + { + "variables": [], + "id": "10416523121", + "key": "variation-b" + } + ], + "forcedVariations": {}, + "id": "10390977673" + } + ], + "groups": [], + "integrations": [ + { + "key": "odp", + "host": "https://api.zaius.com", + "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + }, + { + "key": "odp", + "a": "1", + "b": "2", + }, + { + "key": "x", + "test": "foobar" + } + ], + "typedAudiences": [ + { + "id": "13389142234", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-1" + }, + { + "id": "13389142234", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-1", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-1" + }, + { + "id": "13389130056", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "odp-segment-2", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + }, + { + "value": "us", + "type": "custom_attribute", + "name": "country", + "match": "exact" + } + ], + [ + "or", + { + "value": "odp-segment-3", + "type": "third_party_dimension", + "name": "odp.audiences", + "match": "qualified" + } + ] + ] + ], + "name": "odp-segment-2" + } + ], + "audiences": [ + { + "id": "13389141123", + "conditions": "[\"and\", [\"or\", [\"or\", {\"match\": \"gt\", \"name\": \"age\", \"type\": \"custom_attribute\", \"value\": 20}]]]", + "name": "adult" + } + ], + "attributes": [ + { + "id": "10401066117", + "key": "gender" + }, + { + "id": "10401066170", + "key": "testvar" + } + ], + "accountId": "10367498574", + "events": [], + "revision": "101" +} + +export var getOdpIntegratedConfigWithSegments = function () { + return cloneDeep(odpIntegratedConfigWithSegments); +}; + +var odpIntegratedConfigWithoutSegments = { + "version": "4", + "rollouts": [], + "anonymizeIP": true, + "projectId": "10431130345", + "variables": [], + "featureFlags": [], + "experiments": [], + "audiences": [], + "groups": [], + "attributes": [], + "accountId": "10367498574", + "events": [], + "integrations": [ + { + "key": "odp", + "host": "https://api.zaius.com", + "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + }, + { + "key": "odp", + "a": "1", + "b": "2", + }, + { + "key": "x", + "test": "foobar" + } + ], + "revision": "100" +} + +export var getOdpIntegratedConfigWithoutSegments = function () { + return cloneDeep(odpIntegratedConfigWithoutSegments); +}; + export var typedAudiencesById = { 3468206642: { id: '3468206642', @@ -3279,7 +3536,7 @@ var mutexFeatureTestsConfig = { revision: '12', }; -export var getMutexFeatureTestsConfig = function() { +export var getMutexFeatureTestsConfig = function () { return cloneDeep(mutexFeatureTestsConfig); }; @@ -3533,7 +3790,7 @@ var similarRuleKeyConfig = { sendFlagDecisions: true } -export var getSimilarRuleKeyConfig = function() { +export var getSimilarRuleKeyConfig = function () { return cloneDeep(similarRuleKeyConfig); }; @@ -3675,7 +3932,7 @@ var similarExperimentKeysConfig = { sendFlagDecisions: true } -export var getSimilarExperimentKeyConfig = function() { +export var getSimilarExperimentKeyConfig = function () { return cloneDeep(similarExperimentKeysConfig); }; @@ -3687,6 +3944,8 @@ export default { datafileWithFeaturesExpectedData: datafileWithFeaturesExpectedData, getUnsupportedVersionConfig: getUnsupportedVersionConfig, getTypedAudiencesConfig: getTypedAudiencesConfig, + getOdpIntegratedConfigWithSegments: getOdpIntegratedConfigWithSegments, + getOdpIntegratedConfigWithoutSegments: getOdpIntegratedConfigWithoutSegments, typedAudiencesById: typedAudiencesById, getMutexFeatureTestsConfig: getMutexFeatureTestsConfig, getSimilarRuleKeyConfig: getSimilarRuleKeyConfig,