From aace573a58054c6d975005b5840275e3c7137024 Mon Sep 17 00:00:00 2001 From: John Nguyen Date: Fri, 5 Aug 2022 05:33:46 -0400 Subject: [PATCH 1/3] ODP Datafile Parsing --- .../index.ts | 14 +- .../lib/core/decision_service/index.ts | 14 +- .../lib/core/project_config/index.tests.js | 409 ++++++++++-------- .../lib/core/project_config/index.ts | 91 ++-- .../project_config/project_config_schema.ts | 22 +- .../lib/optimizely_user_context/index.ts | 8 +- packages/optimizely-sdk/lib/shared_types.ts | 12 +- .../optimizely-sdk/lib/tests/test_data.js | 282 ++++++++++-- 8 files changed, 604 insertions(+), 248 deletions(-) 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 834639dbb..57eee9769 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 @@ -381,7 +381,7 @@ function evaluateSemanticVersion(condition: Condition, user: OptimizelyUserConte ); return null; } - + return compareVersion(conditionValue, userValue); } @@ -395,7 +395,7 @@ function evaluateSemanticVersion(condition: Condition, user: OptimizelyUserConte */ function semverEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { const result = evaluateSemanticVersion(condition, user); - if (result === null ) { + if (result === null) { return null; } return result === 0; @@ -411,7 +411,7 @@ function semverEqualEvaluator(condition: Condition, user: OptimizelyUserContext) */ function semverGreaterThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { const result = evaluateSemanticVersion(condition, user); - if (result === null ) { + if (result === null) { return null; } return result > 0; @@ -427,7 +427,7 @@ function semverGreaterThanEvaluator(condition: Condition, user: OptimizelyUserCo */ function semverLessThanEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { const result = evaluateSemanticVersion(condition, user); - if (result === null ) { + if (result === null) { return null; } return result < 0; @@ -443,7 +443,7 @@ function semverLessThanEvaluator(condition: Condition, user: OptimizelyUserConte */ function semverGreaterThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { const result = evaluateSemanticVersion(condition, user); - if (result === null ) { + if (result === null) { return null; } return result >= 0; @@ -459,9 +459,9 @@ function semverGreaterThanOrEqualEvaluator(condition: Condition, user: Optimizel */ function semverLessThanOrEqualEvaluator(condition: Condition, user: OptimizelyUserContext): boolean | null { const result = evaluateSemanticVersion(condition, user); - if (result === null ) { + if (result === null) { return null; } return result <= 0; - + } diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.ts b/packages/optimizely-sdk/lib/core/decision_service/index.ts index ecdb9d931..4f3a309eb 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.ts @@ -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, }; 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 401eefb4d..0c82e1fe0 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,20 @@ 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); @@ -750,98 +750,165 @@ 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, 1); }); - 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 odp segments in all segments', () => { + assert.equal(config.allSegments.length, 3) + assert.equal(config.allSegments, ['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, 1); }); - assert.isNotNull(error); + + 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 odp segments in all segments', () => { + assert.equal(config.allSegments.length, 0) + assert.equal(config.allSegments, []) + }) }); - 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); + }); - assert.deepInclude(result.configObj, configObj); - sinon.assert.notCalled(logger.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' } + ] + } + + 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 9f04289be..e62f756fb 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: string[]; } 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,11 @@ export const createProjectConfig = function( projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id'); assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id')); + if (!projectConfig.allSegments) projectConfig.allSegments = [] + Object.keys(projectConfig.audiencesById).forEach((audience) => { + projectConfig.allSegments.concat(getAudienceSegments(projectConfig.audiencesById[audience])) + }) + projectConfig.attributeKeyMap = keyBy(projectConfig.attributes, 'key'); projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key'); projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id'); @@ -184,6 +195,18 @@ export const createProjectConfig = function( } ); + if (projectConfig.integrations) { + projectConfig.integrationKeyMap = keyBy(projectConfig.integrations, 'key'); + projectConfig.integrations.forEach((integration) => { + if (integration.key === 'odp') { + projectConfig.publicKeyForOdp = integration.publicKey + projectConfig.hostForOdp = integration.host + } + }) + } + + projectConfig.audiencesById + projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key'); projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id'); @@ -272,6 +295,22 @@ 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 && Array.isArray(audience.conditions)) { + const qualifiedAudienceSegments = audience.conditions + .filter((condition) => (condition as string[])[3] === 'qualified') + .map((condition) => (condition as string[])[1]) + + return qualifiedAudienceSegments + } + return [] +}; + /** * Get experiment ID for the provided experiment key * @param {ProjectConfig} projectConfig Object representing project configuration @@ -279,7 +318,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 +333,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 +348,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 +379,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 +394,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 +408,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 +420,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 +433,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 +451,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 +465,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 +480,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 +500,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 +518,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 +534,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 +556,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 +579,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 +606,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 +645,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 +687,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 +768,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 +778,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 +788,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 +797,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 +819,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 +859,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; } 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_user_context/index.ts b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts index 64e4eec48..83bca5f97 100644 --- a/packages/optimizely-sdk/lib/optimizely_user_context/index.ts +++ b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts @@ -72,7 +72,7 @@ export default class OptimizelyUserContext { } public set qualifiedSegments(qualifiedSegments: string[]) { - this._qualifiedSegments = [ ... qualifiedSegments ]; + this._qualifiedSegments = [...qualifiedSegments]; } /** @@ -137,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]) { @@ -227,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/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index d1e0e52cc..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 diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js index f35a47cd7..313d26e7a 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,230 @@ 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" + } + ], + "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": "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" + } + ], + "revision": "100" +} + +export var getOdpIntegratedConfigWithoutSegments = function () { + return cloneDeep(odpIntegratedConfigWithoutSegments); +}; + export var typedAudiencesById = { 3468206642: { id: '3468206642', @@ -3279,7 +3499,7 @@ var mutexFeatureTestsConfig = { revision: '12', }; -export var getMutexFeatureTestsConfig = function() { +export var getMutexFeatureTestsConfig = function () { return cloneDeep(mutexFeatureTestsConfig); }; @@ -3533,7 +3753,7 @@ var similarRuleKeyConfig = { sendFlagDecisions: true } -export var getSimilarRuleKeyConfig = function() { +export var getSimilarRuleKeyConfig = function () { return cloneDeep(similarRuleKeyConfig); }; @@ -3675,7 +3895,7 @@ var similarExperimentKeysConfig = { sendFlagDecisions: true } -export var getSimilarExperimentKeyConfig = function() { +export var getSimilarExperimentKeyConfig = function () { return cloneDeep(similarExperimentKeysConfig); }; @@ -3687,6 +3907,8 @@ export default { datafileWithFeaturesExpectedData: datafileWithFeaturesExpectedData, getUnsupportedVersionConfig: getUnsupportedVersionConfig, getTypedAudiencesConfig: getTypedAudiencesConfig, + getOdpIntegratedConfigWithSegments: getOdpIntegratedConfigWithSegments, + getOdpIntegratedConfigWithoutSegments: getOdpIntegratedConfigWithoutSegments, typedAudiencesById: typedAudiencesById, getMutexFeatureTestsConfig: getMutexFeatureTestsConfig, getSimilarRuleKeyConfig: getSimilarRuleKeyConfig, From 986e69c7f27006a1cd3351369bcbe929bda90fc0 Mon Sep 17 00:00:00 2001 From: John Nguyen Date: Fri, 5 Aug 2022 06:49:25 -0400 Subject: [PATCH 2/3] Fix getAudienceSegments --- .../lib/core/project_config/index.tests.js | 71 ++++++++++++++++++- .../lib/core/project_config/index.ts | 35 ++++++--- 2 files changed, 93 insertions(+), 13 deletions(-) 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 0c82e1fe0..aca9aaf0a 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.tests.js @@ -748,6 +748,70 @@ describe('lib/core/project_config', function () { 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": "not-qualified" + } + ] + ] + ], + "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); + result = projectConfig.isFeatureExperiment(config, '17139931304'); // id of 'f_test2' + assert.isTrue(result); + }); + }); }); describe('integrations', () => { @@ -771,9 +835,9 @@ describe('lib/core/project_config', function () { assert.exists(config.hostForOdp) }) - it('should contain all expected odp segments in all segments', () => { + it('should contain all expected odp segments in allSegments', () => { assert.equal(config.allSegments.length, 3) - assert.equal(config.allSegments, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) + assert.deepEqual(config.allSegments, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) }) }); @@ -790,15 +854,16 @@ describe('lib/core/project_config', function () { 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 odp segments in all segments', () => { assert.equal(config.allSegments.length, 0) - assert.equal(config.allSegments, []) }) }); diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index e62f756fb..70b87839b 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -168,8 +168,9 @@ export const createProjectConfig = function ( assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id')); if (!projectConfig.allSegments) projectConfig.allSegments = [] + Object.keys(projectConfig.audiencesById).forEach((audience) => { - projectConfig.allSegments.concat(getAudienceSegments(projectConfig.audiencesById[audience])) + projectConfig.allSegments = projectConfig.allSegments.concat(getAudienceSegments(projectConfig.audiencesById[audience])) }) projectConfig.attributeKeyMap = keyBy(projectConfig.attributes, 'key'); @@ -205,8 +206,6 @@ export const createProjectConfig = function ( }) } - projectConfig.audiencesById - projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key'); projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id'); @@ -301,15 +300,30 @@ export const createProjectConfig = function ( * @return {string[]} List of all audience segments */ export const getAudienceSegments = function (audience: Audience): string[] { - if (audience.conditions && Array.isArray(audience.conditions)) { - const qualifiedAudienceSegments = audience.conditions - .filter((condition) => (condition as string[])[3] === 'qualified') - .map((condition) => (condition as string[])[1]) + if (!audience.conditions) return [] + return getSegmentsFromConditions(audience.conditions); +}; - return qualifiedAudienceSegments +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getSegmentsFromConditions = (condition: any): string[] => { + const segments = []; + + if (isLogicalOperator(condition)) { + return [] } - 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 @@ -886,6 +900,7 @@ export default { getTypeCastValue, getSendFlagDecisionsValue, getAudiencesById, + getAudienceSegments, eventWithKeyExists, isFeatureExperiment, toDatafile, From baa63ea428de6ff92fb8d98ccd7cae09caac3864 Mon Sep 17 00:00:00 2001 From: John Nguyen Date: Wed, 10 Aug 2022 11:58:51 -0400 Subject: [PATCH 3/3] Refactor allSegments as Set to ensure unique segments --- .../lib/core/project_config/index.tests.js | 16 ++++---- .../lib/core/project_config/index.ts | 26 +++++++------ .../optimizely-sdk/lib/tests/test_data.js | 37 +++++++++++++++++++ 3 files changed, 60 insertions(+), 19 deletions(-) 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 aca9aaf0a..90f0e452b 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.tests.js @@ -786,7 +786,7 @@ describe('lib/core/project_config', function () { "value": "odp-segment-1", "type": "third_party_dimension", "name": "odp.audiences", - "match": "not-qualified" + "match": "invalid" } ] ] @@ -824,7 +824,7 @@ describe('lib/core/project_config', function () { it('should convert integrations from the datafile into the project config', () => { assert.exists(config.integrations); - assert.equal(config.integrations.length, 1); + assert.equal(config.integrations.length, 3); }); it('should populate the public key value from the odp integration', () => { @@ -835,9 +835,9 @@ describe('lib/core/project_config', function () { assert.exists(config.hostForOdp) }) - it('should contain all expected odp segments in allSegments', () => { - assert.equal(config.allSegments.length, 3) - assert.deepEqual(config.allSegments, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) + 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'])) }) }); @@ -849,7 +849,7 @@ describe('lib/core/project_config', function () { it('should convert integrations from the datafile into the project config', () => { assert.exists(config.integrations); - assert.equal(config.integrations.length, 1); + assert.equal(config.integrations.length, 3); }); it('should populate the public key value from the odp integration', () => { @@ -862,8 +862,8 @@ describe('lib/core/project_config', function () { assert.equal(config.hostForOdp, 'https://api.zaius.com') }) - it('should contain all expected odp segments in all segments', () => { - assert.equal(config.allSegments.length, 0) + it('should contain all expected unique odp segments in all segments', () => { + assert.equal(config.allSegments.size, 0) }) }); diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index 70b87839b..3a27e2848 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -103,7 +103,7 @@ export interface ProjectConfig { integrationKeyMap?: { [key: string]: Integration }; publicKeyForOdp?: string; hostForOdp?: string; - allSegments: string[]; + allSegments: Set; } const EXPERIMENT_RUNNING_STATUS = 'Running'; @@ -167,11 +167,15 @@ export const createProjectConfig = function ( projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id'); assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id')); - if (!projectConfig.allSegments) projectConfig.allSegments = [] + projectConfig.allSegments = new Set([]) - Object.keys(projectConfig.audiencesById).forEach((audience) => { - projectConfig.allSegments = projectConfig.allSegments.concat(getAudienceSegments(projectConfig.audiencesById[audience])) - }) + 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'); @@ -198,12 +202,12 @@ export const createProjectConfig = function ( if (projectConfig.integrations) { projectConfig.integrationKeyMap = keyBy(projectConfig.integrations, 'key'); - projectConfig.integrations.forEach((integration) => { - if (integration.key === 'odp') { - projectConfig.publicKeyForOdp = integration.publicKey - projectConfig.hostForOdp = integration.host - } - }) + 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'); diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js index 313d26e7a..849cc5e86 100644 --- a/packages/optimizely-sdk/lib/tests/test_data.js +++ b/packages/optimizely-sdk/lib/tests/test_data.js @@ -3255,9 +3255,37 @@ var odpIntegratedConfigWithSegments = { "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": [ @@ -3356,6 +3384,15 @@ var odpIntegratedConfigWithoutSegments = { "key": "odp", "host": "https://api.zaius.com", "publicKey": "W4WzcEs-ABgXorzY7h1LCQ" + }, + { + "key": "odp", + "a": "1", + "b": "2", + }, + { + "key": "x", + "test": "foobar" } ], "revision": "100"