diff --git a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts b/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts index 030afbfa7..7b0c8df9d 100644 --- a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts +++ b/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2018, 2020, Optimizely, Inc. and contributors * + * Copyright 2018, 2021, 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. * @@ -18,7 +18,7 @@ const AND_CONDITION = 'and'; const OR_CONDITION = 'or'; const NOT_CONDITION = 'not'; -const DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION]; +export const DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION]; export type ConditionTree = Leaf | unknown[]; type LeafEvaluator = (leaf: Leaf) => boolean | null; diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js b/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js index e38798156..25ce515fe 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js @@ -16,11 +16,19 @@ import { assert } from 'chai'; import { cloneDeep } from 'lodash'; -import { createOptimizelyConfig } from './'; +import { createOptimizelyConfig, OptimizelyConfig } from './'; import { createProjectConfig } from '../project_config'; -import { getTestProjectConfigWithFeatures } from '../../tests/test_data'; +import { + getTestProjectConfigWithFeatures, + getTypedAudiencesConfig, + getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig +} from '../../tests/test_data'; var datafile = getTestProjectConfigWithFeatures(); +var typedAudienceDatafile = getTypedAudiencesConfig(); +var similarRuleKeyDatafile = getSimilarRuleKeyConfig(); +var similarExperimentKeyDatafile = getSimilarExperimentKeyConfig(); var getAllExperimentsFromDatafile = function(datafile) { var allExperiments = []; @@ -39,9 +47,21 @@ describe('lib/core/optimizely_config', function() { describe('Optimizely Config', function() { var optimizelyConfigObject; var projectConfigObject; + var optimizelyTypedAudienceConfigObject; + var projectTypedAudienceConfigObject; + var optimizelySimilarRuleKeyConfigObject; + var projectSimilarRuleKeyConfigObject; + var optimizelySimilarExperimentkeyConfigObject; + var projectSimilarExperimentKeyConfigObject; beforeEach(function() { projectConfigObject = createProjectConfig(cloneDeep(datafile)); optimizelyConfigObject = createOptimizelyConfig(projectConfigObject, JSON.stringify(datafile)); + projectTypedAudienceConfigObject = createProjectConfig(cloneDeep(typedAudienceDatafile)); + optimizelyTypedAudienceConfigObject = createOptimizelyConfig(projectTypedAudienceConfigObject, JSON.stringify(typedAudienceDatafile)); + projectSimilarRuleKeyConfigObject = createProjectConfig(cloneDeep(similarRuleKeyDatafile)); + optimizelySimilarRuleKeyConfigObject = createOptimizelyConfig(projectSimilarRuleKeyConfigObject, JSON.stringify(similarRuleKeyDatafile)); + projectSimilarExperimentKeyConfigObject = createProjectConfig(cloneDeep(similarExperimentKeyDatafile)); + optimizelySimilarExperimentkeyConfigObject = createOptimizelyConfig(projectSimilarExperimentKeyConfigObject, JSON.stringify(similarExperimentKeyDatafile)); }); it('should return all experiments except rollouts', function() { @@ -70,7 +90,654 @@ describe('lib/core/optimizely_config', function() { assert.equal(featureFlagsCount, 9); var featuresMap = optimizelyConfigObject.featuresMap; - datafile.featureFlags.forEach(function(featureFlag) { + var expectedDeliveryRules = [ + [ + { + id: "594031", + key: "594031", + audiences: "", + variationsMap: { + "594032": { + id: "594032", + key: "594032", + featureEnabled: true, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "true" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "395" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "4.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello audience" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 2, \"message\": \"Hello audience\" }" + } + } + } + } + }, { + id: "594037", + key: "594037", + audiences: "", + variationsMap: { + "594038": { + id: "594038", + key: "594038", + featureEnabled: false, + variablesMap: { + new_content: { + id: "4919852825313280", + key: "new_content", + type: "boolean", + value: "false" + }, + lasers: { + id: "5482802778734592", + key: "lasers", + type: "integer", + value: "400" + }, + price: { + id: "6045752732155904", + key: "price", + type: "double", + value: "14.99" + }, + message: { + id: "6327227708866560", + key: "message", + type: "string", + value: "Hello" + }, + message_info: { + id: "8765345281230956", + key: "message_info", + type: "json", + value: "{ \"count\": 1, \"message\": \"Hello\" }" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [], + [], + [ + { + id: "599056", + key: "599056", + audiences: "", + variationsMap: { + "599057": { + id: "599057", + key: "599057", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "200" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "i'm a rollout" + } + } + } + } + } + ], + [], + [], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ], + [ + { + id: "594060", + key: "594060", + audiences: "", + variationsMap: { + "594061": { + id: "594061", + key: "594061", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "27.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is NOT coming" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "10003" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "false" + } + } + } + } + }, { + id: "594066", + key: "594066", + audiences: "", + variationsMap: { + "594067": { + id: "594067", + key: "594067", + featureEnabled: true, + variablesMap: { + miles_to_the_wall: { + id: "5060590313668608", + key: "miles_to_the_wall", + type: "double", + value: "30.34" + }, + motto: { + id: "5342065290379264", + key: "motto", + type: "string", + value: "Winter is coming definitely" + }, + soldiers_available: { + id: "6186490220511232", + key: "soldiers_available", + type: "integer", + value: "500" + }, + is_winter_coming: { + id: "6467965197221888", + key: "is_winter_coming", + type: "boolean", + value: "true" + } + } + } + } + } + ] + ] + var expectedExperimentRules = [ + [], + [], + [ + { + id: "594098", + key: "testing_my_feature", + audiences: "", + variationsMap: { + variation: { + id: "594096", + key: "variation", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "2" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "true" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me NOW" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "20.25" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 1, \"text\": \"first variation\"}" + } + } + }, + control: { + id: "594097", + key: "control", + featureEnabled: true, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 2, \"text\": \"second variation\"}" + } + } + }, + "variation2": { + id: "594099", + key: "variation2", + featureEnabled: false, + variablesMap: { + num_buttons: { + id: "4792309476491264", + key: "num_buttons", + type: "integer", + value: "10" + }, + is_button_animated: { + id: "5073784453201920", + key: "is_button_animated", + type: "boolean", + value: "false" + }, + button_txt: { + id: "5636734406623232", + key: "button_txt", + type: "string", + value: "Buy me" + }, + button_width: { + id: "6199684360044544", + key: "button_width", + type: "double", + value: "50.55" + }, + button_info: { + id: "1547854156498475", + key: "button_info", + type: "json", + value: "{ \"num_buttons\": 0, \"text\": \"default value\"}" + } + } + } + } + } + ], + [ + { + id: "595010", + key: "exp_with_group", + audiences: "", + variationsMap: { + var: { + featureEnabled: undefined, + id: "595008", + key: "var", + variablesMap: {} + }, + con: { + featureEnabled: undefined, + id: "595009", + key: "con", + variablesMap: {} + } + } + } + ], + [ + { + id: "599028", + key: "test_shared_feature", + audiences: "", + variationsMap: { + treatment: { + id: "599026", + key: "treatment", + featureEnabled: true, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + }, + control: { + id: "599027", + key: "control", + featureEnabled: false, + variablesMap: { + lasers: { + id: "4937719889264640", + key: "lasers", + type: "integer", + value: "100" + }, + message: { + id: "6345094772817920", + key: "message", + type: "string", + value: "shared" + } + } + } + } + } + ], + [], + [ + { + id: "12115595439", + key: "no_traffic_experiment", + audiences: "", + variationsMap: { + "variation_5000": { + "featureEnabled": undefined, + id: "12098126629", + key: "variation_5000", + variablesMap: {} + }, + "variation_10000": { + "featureEnabled": undefined, + id: "12098126630", + key: "variation_10000", + variablesMap: {} + } + } + } + ], + [ + { + id: "42222", + key: "group_2_exp_1", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38901", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42223", + key: "group_2_exp_2", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38905", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "42224", + key: "group_2_exp_3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + "var_1": { + id: "38906", + key: "var_1", + featureEnabled: false, + variablesMap: {} + } + } + } + ], + [ + { + id: "111134", + key: "test_experiment3", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222239", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111135", + key: "test_experiment4", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222240", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + }, { + id: "111136", + key: "test_experiment5", + audiences: "\"Test attribute users 3\"", + variationsMap: { + control: { + id: "222241", + key: "control", + featureEnabled: false, + variablesMap: {} + } + } + } + ] + ] + + datafile.featureFlags.forEach(function(featureFlag, index) { assert.include(featuresMap[featureFlag.key], { id: featureFlag.id, key: featureFlag.key, @@ -80,6 +747,10 @@ describe('lib/core/optimizely_config', function() { assert.isTrue(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]); }); var variablesMap = featuresMap[featureFlag.key].variablesMap; + var deliveryRules = featuresMap[featureFlag.key].deliveryRules; + var experimentRules = featuresMap[featureFlag.key].experimentRules; + assert.deepEqual(deliveryRules, expectedDeliveryRules[index]); + assert.deepEqual(experimentRules, expectedExperimentRules[index]); featureFlag.variables.forEach(function(variable) { // json is represented as sub type of string to support backwards compatibility in datafile. // project config treats it as a first-class type. @@ -145,5 +816,80 @@ describe('lib/core/optimizely_config', function() { it('should return correct config environmentKey ', function() { assert.equal(optimizelyConfigObject.environmentKey, datafile.environmentKey); }); + + it('should return serialized audiences', function () { + const audiencesById = projectTypedAudienceConfigObject.audiencesById; + const audienceConditions = [ + ['or', '3468206642', '3988293898'], + ['or', '3468206642', '3988293898', '3468206646'], + ['not', '3468206642'], + ['or', '3468206642'], + ['and', '3468206642'], + ['3468206642'], + ['3468206642', '3988293898'], + ['and', ['or', '3468206642', '3988293898'], '3468206646'], + [ + 'and', + ['or', '3468206642', ['and', '3988293898', '3468206646']], + ['and', '3988293899', ['or', '3468206647', '3468206643']], + ], + ['and', 'and'], + ['not', ['and', '3468206642', '3988293898']], + [], + ['or', '3468206642', '999999999'], + ]; + + const expectedAudienceOutputs = [ + '"exactString" OR "substringString"', + '"exactString" OR "substringString" OR "exactNumber"', + 'NOT "exactString"', + '"exactString"', + '"exactString"', + '"exactString"', + '"exactString" OR "substringString"', + '("exactString" OR "substringString") AND "exactNumber"', + '("exactString" OR ("substringString" AND "exactNumber")) AND ("exists" AND ("gtNumber" OR "exactBoolean"))', + '', + 'NOT ("exactString" AND "substringString")', + '', + '"exactString" OR "999999999"', + ]; + + for (let testNo = 0; testNo < audienceConditions.length; testNo++) { + const serializedAudiences = OptimizelyConfig.getSerializedAudiences(audienceConditions[testNo], audiencesById); + assert.equal(serializedAudiences, expectedAudienceOutputs[testNo]); + } + }); + + it('should return correct rollouts', function () { + const rolloutFlag1 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_1'].deliveryRules[0]; + const rolloutFlag2 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_2'].deliveryRules[0]; + const rolloutFlag3 = optimizelySimilarRuleKeyConfigObject.featuresMap['flag_3'].deliveryRules[0]; + + assert.equal(rolloutFlag1.id, '9300000004977'); + assert.equal(rolloutFlag1.key, 'targeted_delivery'); + assert.equal(rolloutFlag2.id, '9300000004979'); + assert.equal(rolloutFlag2.key, 'targeted_delivery'); + assert.equal(rolloutFlag3.id, '9300000004981'); + assert.equal(rolloutFlag3.key, 'targeted_delivery'); + + }); + + it('should return default SDK and environment key', function() { + + assert.equal(optimizelySimilarRuleKeyConfigObject.sdkKey, ""); + assert.equal(optimizelySimilarRuleKeyConfigObject.environmentKey, ""); + + }); + + it('should return correct experiments with similar keys', function() { + + assert.equal(Object.keys(optimizelySimilarExperimentkeyConfigObject.experimentsMap).length, 1); + const experimentMapFlag1 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag1"].experimentsMap; + const experimentMapFlag2 = optimizelySimilarExperimentkeyConfigObject.featuresMap["flag2"].experimentsMap; + assert.equal(experimentMapFlag1["targeted_delivery"].id, "9300000007569"); + assert.equal(experimentMapFlag2["targeted_delivery"].id, "9300000007573"); + + }); }); }); diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 788339d3a..ebed3da8f 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020, Optimizely + * Copyright 2020-2021, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { isFeatureExperiment, ProjectConfig } from '../project_config'; +import { ProjectConfig } from '../project_config'; +import { DEFAULT_OPERATOR_TYPES } from '../condition_tree_evaluator'; import { + Audience, + Experiment, + FeatureVariable, + OptimizelyAttribute, + OptimizelyAudience, + OptimizelyEvent, + OptimizelyExperiment, OptimizelyExperimentsMap, OptimizelyFeaturesMap, + OptimizelyVariable, OptimizelyVariablesMap, - FeatureVariable, - VariationVariable, - Variation, + OptimizelyVariation, Rollout, + Variation, + VariationVariable, } from '../../shared_types'; interface FeatureVariablesMap { @@ -34,23 +43,41 @@ interface FeatureVariablesMap { * @param {string} datafile */ export class OptimizelyConfig { + public environmentKey: string; + public sdkKey: string; + public revision: string; + + /** + * This experimentsMap is for experiments of legacy projects only. + * For flag projects, experiment keys are not guaranteed to be unique + * across multiple flags, so this map may not include all experiments + * when keys conflict. + */ public experimentsMap: OptimizelyExperimentsMap; + public featuresMap: OptimizelyFeaturesMap; - public revision: string; - public sdkKey?: string; - public environmentKey?: string; + public attributes: OptimizelyAttribute[]; + public audiences: OptimizelyAudience[]; + public events: OptimizelyEvent[]; private datafile: string; constructor(configObj: ProjectConfig, datafile: string) { - this.experimentsMap = OptimizelyConfig.getExperimentsMap(configObj); - this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, this.experimentsMap); + this.sdkKey = configObj.sdkKey ?? ''; + this.environmentKey = configObj.environmentKey ?? ''; + this.attributes = configObj.attributes; + this.audiences = OptimizelyConfig.getAudiences(configObj); + this.events = configObj.events; this.revision = configObj.revision; - this.datafile = datafile; - if (configObj.sdkKey && configObj.environmentKey) { - this.sdkKey = configObj.sdkKey; - this.environmentKey = configObj.environmentKey; - } + const featureIdVariablesMap = (configObj.featureFlags || []).reduce((resultMap: FeatureVariablesMap, feature) => { + resultMap[feature.id] = feature.variables; + return resultMap; + }, {}); + + const experimentsMapById = OptimizelyConfig.getExperimentsMapById(configObj, featureIdVariablesMap); + this.experimentsMap = OptimizelyConfig.getExperimentsKeyMap(experimentsMapById); + this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, featureIdVariablesMap, experimentsMapById); + this.datafile = datafile; } /** @@ -62,157 +89,360 @@ export class OptimizelyConfig { } /** - * Get Experiment Ids which are part of rollout - * @param {Rollout[]} rollouts - * @returns {[key: string]: boolean} Map of experiment Ids to boolean + * Get Unique audiences list with typedAudiences as priority + * @param {ProjectConfig} configObj + * @returns {OptimizelyAudience[]} Array of unique audiences */ - static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { - return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { - rollout.experiments.forEach((e) => { - (experimentIds)[e.id] = true; + static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { + const audiences: OptimizelyAudience[] = []; + const typedAudienceIds: string[] = []; + + (configObj.typedAudiences || []).forEach((typedAudience) => { + audiences.push({ + id: typedAudience.id, + conditions: JSON.stringify(typedAudience.conditions), + name: typedAudience.name, }); + typedAudienceIds.push(typedAudience.id); + }); - return experimentIds; - }, {}); + (configObj.audiences || []).forEach((audience) => { + if (typedAudienceIds.indexOf(audience.id) === -1 && audience.id != '$opt_dummy_audience') { + audiences.push({ + id: audience.id, + conditions: JSON.stringify(audience.conditions), + name: audience.name, + }); + } + }); + + return audiences; } /** - * Get Map of all experiments except rollouts + * Converts list of audience conditions to serialized audiences used in experiment + * for examples: + * 1. Input: ["or", "1", "2"] + * Output: "\"us\" OR \"female\"" + * 2. Input: ["not", "1"] + * Output: "NOT \"us\"" + * 3. Input: ["or", "1"] + * Output: "\"us\"" + * 4. Input: ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]] + * Output: "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))" + * @param {Array} conditions + * @param {[id: string]: Audience} audiencesById + * @returns {string} Serialized audiences condition string + */ + static getSerializedAudiences( + conditions: Array, + audiencesById: { [id: string]: Audience } + ): string { + let serializedAudience = ''; + + if (conditions) { + let cond = ''; + conditions.forEach((item) => { + let subAudience = ''; + // Checks if item is list of conditions means it is sub audience + if (item instanceof Array) { + subAudience = OptimizelyConfig.getSerializedAudiences(item, audiencesById); + subAudience = `(${subAudience})`; + } else if (DEFAULT_OPERATOR_TYPES.indexOf(item) > -1) { + cond = item.toUpperCase(); + } else { + // Checks if item is audience id + const audienceName = audiencesById[item] ? audiencesById[item].name : item; + // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in serializedAudience then append condition between serializedAudience and item + if (serializedAudience || cond === 'NOT') { + cond = cond === '' ? 'OR' : cond; + if (serializedAudience === '') { + serializedAudience = `${cond} "${audiencesById[item].name}"`; + } else { + serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`); + } + } else { + serializedAudience = `"${audienceName}"`; + } + } + // Checks if sub audience is empty or not + if (subAudience !== '') { + if (serializedAudience !== '' || cond === 'NOT') { + cond = cond === '' ? 'OR' : cond; + if (serializedAudience === '') { + serializedAudience = `${cond} ${subAudience}`; + } else { + serializedAudience = serializedAudience.concat(` ${cond} ${subAudience}`); + } + } else { + serializedAudience = serializedAudience.concat(subAudience); + } + } + }); + } + return serializedAudience; + } + + /** + * Get serialized audience condition string for experiment + * @param {Experiment} experiment * @param {ProjectConfig} configObj - * @returns {OptimizelyExperimentsMap} Map of experiments excluding rollouts + * @returns {string} Serialized audiences condition string */ - static getExperimentsMap(configObj: ProjectConfig): OptimizelyExperimentsMap { - const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); - const featureVariablesMap = (configObj.featureFlags || []).reduce( - (resultMap: FeatureVariablesMap, feature) => { - resultMap[feature.id] = feature.variables; - return resultMap; + static getExperimentAudiences(experiment: Experiment, configObj: ProjectConfig): string { + if (!experiment.audienceConditions) { + return ''; + } + return OptimizelyConfig.getSerializedAudiences(experiment.audienceConditions, configObj.audiencesById); + } + + /** + * Make map of featureVariable which are associated with given feature experiment + * @param {FeatureVariablesMap} featureIdVariableMap + * @param {[id: string]: FeatureVariable} variableIdMap + * @param {string} featureId + * @param {VariationVariable[] | undefined} featureVariableUsages + * @param {boolean | undefined} isFeatureEnabled + * @returns {OptimizelyVariablesMap} FeatureVariables mapped by key + */ + static mergeFeatureVariables( + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string, + featureVariableUsages: VariationVariable[] | undefined, + isFeatureEnabled: boolean | undefined + ): OptimizelyVariablesMap { + const variablesMap = (featureIdVariableMap[featureId] || []).reduce( + (optlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { + optlyVariablesMap[featureVariable.key] = { + id: featureVariable.id, + key: featureVariable.key, + type: featureVariable.type, + value: featureVariable.defaultValue, + }; + return optlyVariablesMap; }, - {}, + {} ); - return (configObj.experiments || []).reduce( - (experiments: OptimizelyExperimentsMap, experiment) => { - // skip experiments that are part of a rollout - if (!rolloutExperimentIds[experiment.id]) { - experiments[experiment.key] = { - id: experiment.id, - key: experiment.key, - variationsMap: (experiment.variations || []).reduce( - (variations: { [key: string]: Variation }, variation) => { - variations[variation.key] = { - id: variation.id, - key: variation.key, - variablesMap: this.getMergedVariablesMap(configObj, variation, experiment.id, featureVariablesMap), - }; - if (isFeatureExperiment(configObj, experiment.id)) { - variations[variation.key].featureEnabled = variation.featureEnabled; - } - - return variations; - }, - {}, - ), - }; - } + (featureVariableUsages || []).forEach((featureVariableUsage) => { + const defaultVariable = variableIdMap[featureVariableUsage.id]; + const optimizelyVariable: OptimizelyVariable = { + id: featureVariableUsage.id, + key: defaultVariable.key, + type: defaultVariable.type, + value: isFeatureEnabled ? featureVariableUsage.value : defaultVariable.defaultValue, + }; + variablesMap[defaultVariable.key] = optimizelyVariable; + }); + return variablesMap; + } - return experiments; - }, - {}, - ) + /** + * Gets Map of all experiment variations and variables including rollouts + * @param {Variation[]} variations + * @param {FeatureVariablesMap} featureIdVariableMap + * @param {[id: string]: FeatureVariable} variableIdMap + * @param {string} featureId + * @returns {[key: string]: Variation} Variations mapped by key + */ + static getVariationsMap( + variations: Variation[], + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string + ): { [key: string]: Variation } { + let variationsMap: { [key: string]: OptimizelyVariation } = {}; + variationsMap = variations.reduce((optlyVariationsMap: { [key: string]: OptimizelyVariation }, variation) => { + const variablesMap = OptimizelyConfig.mergeFeatureVariables( + featureIdVariableMap, + variableIdMap, + featureId, + variation.variables, + variation.featureEnabled + ); + optlyVariationsMap[variation.key] = { + id: variation.id, + key: variation.key, + featureEnabled: variation.featureEnabled, + variablesMap: variablesMap, + }; + return optlyVariationsMap; + }, {}); + + return variationsMap; } /** - * Merge feature key and type from feature variables to variation variables - * @param {ProjectConfig} configObj - * @param {Variation} variation - * @param {string} experimentId - * @param {FeatureVariablesMap} featureVariablesMap - * @returns {OptimizelyVariablesMap} Map of variables + * Gets Map of FeatureVariable with respect to featureVariableId + * @param {ProjectConfig} configObj + * @returns {[id: string]: FeatureVariable} FeatureVariables mapped by id + */ + static getVariableIdMap(configObj: ProjectConfig): { [id: string]: FeatureVariable } { + let variablesIdMap: { [id: string]: FeatureVariable } = {}; + variablesIdMap = (configObj.featureFlags || []).reduce((resultMap: { [id: string]: FeatureVariable }, feature) => { + feature.variables.forEach((variable) => { + resultMap[variable.id] = variable; + }); + return resultMap; + }, {}); + + return variablesIdMap; + } + + /** + * Gets list of rollout experiments + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {string} featureId + * @param {Experiment[]} experiments + * @returns {OptimizelyExperiment[]} List of Optimizely rollout experiments */ - static getMergedVariablesMap( + static getDeliveryRules( configObj: ProjectConfig, - variation: Variation, - experimentId: string, - featureVariablesMap: FeatureVariablesMap, - ): OptimizelyVariablesMap { - const featureId = configObj.experimentFeatureMap[experimentId]; - - let variablesObject = {}; - if (featureId) { - const experimentFeatureVariables = featureVariablesMap[featureId.toString()]; - // Temporary variation variables map to get values to merge. - const tempVariablesIdMap = (variation.variables || []).reduce( - (variablesMap: { [key: string]: VariationVariable }, variable) => { - variablesMap[variable.id] = { - id: variable.id, - value: variable.value, - }; - - return variablesMap; - }, - {}, - ); - variablesObject = (experimentFeatureVariables || []).reduce( - (variablesMap: OptimizelyVariablesMap, featureVariable) => { - const variationVariable = tempVariablesIdMap[featureVariable.id]; - const variableValue = - variation.featureEnabled && variationVariable ? variationVariable.value : featureVariable.defaultValue; - variablesMap[featureVariable.key] = { - id: featureVariable.id, - key: featureVariable.key, - type: featureVariable.type, - value: variableValue, - }; - - return variablesMap; - }, - {}, - ); - } + featureVariableIdMap: FeatureVariablesMap, + featureId: string, + experiments: Experiment[] + ): OptimizelyExperiment[] { + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); + return experiments.map((experiment) => { + return { + id: experiment.id, + key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), + variationsMap: OptimizelyConfig.getVariationsMap( + experiment.variations, + featureVariableIdMap, + variableIdMap, + featureId + ), + }; + }); + } - return variablesObject; + /** + * Get Experiment Ids which are part of rollout + * @param {Rollout[]} rollouts + * @returns {string[]} Array of experiment Ids + */ + static getRolloutExperimentIds(rollouts: Rollout[]): string[] { + const experimentIds: string[] = []; + (rollouts || []).forEach((rollout) => { + rollout.experiments.forEach((e) => { + experimentIds.push(e.id); + }); + }); + return experimentIds; + } + + /** + * Get experiments mapped by their id's which are not part of a rollout + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureIdVariableMap + * @returns {[id: string]: OptimizelyExperiment} Experiments mapped by id + */ + static getExperimentsMapById( + configObj: ProjectConfig, + featureIdVariableMap: FeatureVariablesMap + ): { [id: string]: OptimizelyExperiment } { + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); + const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); + + const experiments = configObj.experiments; + + return (experiments || []).reduce((experimentsMap: { [id: string]: OptimizelyExperiment }, experiment) => { + if (rolloutExperimentIds.indexOf(experiment.id) === -1) { + const featureIds = configObj.experimentFeatureMap[experiment.id]; + let featureId = ''; + if (featureIds && featureIds.length > 0) { + featureId = featureIds[0]; + } + const variationsMap = OptimizelyConfig.getVariationsMap( + experiment.variations, + featureIdVariableMap, + variableIdMap, + featureId.toString() + ); + experimentsMap[experiment.id] = { + id: experiment.id, + key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), + variationsMap: variationsMap, + }; + } + return experimentsMap; + }, {}); } /** - * Get map of all experiments + * Get experiments mapped by their keys + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyExperimentsMap} Experiments mapped by key + */ + static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { + const experimentKeysMap: OptimizelyExperimentsMap = {}; + + for (const id in experimentsMapById) { + const experiment = experimentsMapById[id]; + experimentKeysMap[experiment.key] = experiment; + } + return experimentKeysMap; + } + + /** + * Gets Map of all FeatureFlags and associated experiment map inside it * @param {ProjectConfig} configObj - * @param {OptimizelyExperimentsMap} allExperiments - * @returns {OptimizelyFeaturesMap} Map of all experiments + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyFeaturesMap} OptimizelyFeature mapped by key */ static getFeaturesMap( configObj: ProjectConfig, - allExperiments: OptimizelyExperimentsMap + featureVariableIdMap: FeatureVariablesMap, + experimentsMapById: OptimizelyExperimentsMap ): OptimizelyFeaturesMap { - return (configObj.featureFlags || []).reduce((features: OptimizelyFeaturesMap, feature) => { - features[feature.key] = { - id: feature.id, - key: feature.key, - experimentsMap: (feature.experimentIds || []).reduce( - (experiments: OptimizelyExperimentsMap, experimentId) => { - const experimentKey = configObj.experimentIdMap[experimentId].key; - experiments[experimentKey] = allExperiments[experimentKey]; - return experiments; - }, - {}, - ), - variablesMap: (feature.variables || []).reduce( - (variables: OptimizelyVariablesMap, variable) => { - variables[variable.key] = { - id: variable.id, - key: variable.key, - type: variable.type, - value: variable.defaultValue, - }; - - return variables; - }, - {}, - ), + const featuresMap: OptimizelyFeaturesMap = {}; + configObj.featureFlags.forEach((featureFlag) => { + const featureExperimentMap: OptimizelyExperimentsMap = {}; + const experimentRules: OptimizelyExperiment[] = []; + for (const key in experimentsMapById) { + if (featureFlag.experimentIds.indexOf(key) > -1) { + const experiment = experimentsMapById[key]; + if (experiment) { + featureExperimentMap[experiment.key] = experiment; + } + experimentRules.push(experimentsMapById[key]); + } + } + const featureVariableMap = (featureFlag.variables || []).reduce((variables: OptimizelyVariablesMap, variable) => { + variables[variable.key] = { + id: variable.id, + key: variable.key, + type: variable.type, + value: variable.defaultValue, + }; + return variables; + }, {}); + let deliveryRules: OptimizelyExperiment[] = []; + const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; + if (rollout) { + deliveryRules = OptimizelyConfig.getDeliveryRules( + configObj, + featureVariableIdMap, + featureFlag.id, + rollout.experiments + ); + } + featuresMap[featureFlag.key] = { + id: featureFlag.id, + key: featureFlag.key, + experimentRules: experimentRules, + deliveryRules: deliveryRules, + experimentsMap: featureExperimentMap, + variablesMap: featureVariableMap, }; - - return features; - }, {}); + }); + return featuresMap; } } diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index c58b4e136..99cb0e6d8 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -26,17 +26,17 @@ import configValidator from '../../utils/config_validator'; import { LogHandler } from '@optimizely/js-sdk-logging'; import { + Audience, Experiment, FeatureFlag, + FeatureVariable, Group, - Audience, + OptimizelyVariation, Rollout, TrafficAllocation, - FeatureVariable, Variation, - OptimizelyVariation, VariableType, - VariationVariable + VariationVariable, } from '../../shared_types'; interface TryCreatingProjectConfigConfig { @@ -50,6 +50,7 @@ interface TryCreatingProjectConfigConfig { interface Event { key: string; id: string; + experimentsIds: string[]; } interface VariableUsageMap { @@ -59,12 +60,12 @@ interface VariableUsageMap { export interface ProjectConfig { revision: string; projectId: string; - sdkKey?: string; - environmentKey?: string; + sdkKey: string; + environmentKey: string; sendFlagDecisions?: boolean; experimentKeyMap: { [key: string]: Experiment }; featureKeyMap: { - [key: string]: FeatureFlag + [key: string]: FeatureFlag; }; rollouts: Rollout[]; featureFlags: FeatureFlag[]; @@ -81,7 +82,7 @@ export interface ProjectConfig { groupIdMap: { [id: string]: Group }; groups: Group[]; events: Event[]; - attributes: Array<{ id: string }>; + attributes: Array<{ id: string; key: string }>; typedAudiences: Audience[]; rolloutIdMap: { [id: string]: Rollout }; anonymizeIP?: boolean | null; @@ -120,10 +121,8 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { return rolloutCopy; }); - if (datafile.environmentKey && datafile.sdkKey) { - datafileCopy.environmentKey = datafile.environmentKey; - datafileCopy.sdkKey = datafile.sdkKey; - } + datafileCopy.environmentKey = datafile.environmentKey ?? ''; + datafileCopy.sdkKey = datafile.sdkKey ?? ''; return datafileCopy; } diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 2e41a6290..f69abba55 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -163,6 +163,7 @@ export type Condition = { } export interface Audience { + id: string; name: string; conditions: unknown[] | string; } @@ -248,6 +249,7 @@ export interface OptimizelyOptions { export interface OptimizelyExperiment { id: string; key: string; + audiences: string; variationsMap: { [variationKey: string]: OptimizelyVariation; }; @@ -300,11 +302,34 @@ export type OptimizelyFeaturesMap = { [featureKey: string]: OptimizelyFeature; } +export type OptimizelyAttribute = { + id: string; + key: string; +}; + +export type OptimizelyAudience = { + id: string; + name: string; + conditions: string; +}; + +export type OptimizelyEvent = { + id: string; + key: string; + experimentsIds: string[]; +}; + export interface OptimizelyFeature { id: string; key: string; - experimentsMap: OptimizelyExperimentsMap; + experimentRules: OptimizelyExperiment[]; + deliveryRules: OptimizelyExperiment[]; variablesMap: OptimizelyVariablesMap; + + /** + * @deprecated Use experimentRules and deliveryRules + */ + experimentsMap: OptimizelyExperimentsMap; } export interface OptimizelyVariation { @@ -315,11 +340,22 @@ export interface OptimizelyVariation { } export interface OptimizelyConfig { + environmentKey: string; + sdkKey: string; + revision: string; + + /** + * This experimentsMap is for experiments of legacy projects only. + * For flag projects, experiment keys are not guaranteed to be unique + * across multiple flags, so this map may not include all experiments + * when keys conflict. + */ experimentsMap: OptimizelyExperimentsMap; + featuresMap: OptimizelyFeaturesMap; - revision: string; - sdkKey?: string; - environmentKey?: string; + attributes: OptimizelyAttribute[]; + audiences: OptimizelyAudience[]; + events: OptimizelyEvent[]; getDatafile(): string; } diff --git a/packages/optimizely-sdk/lib/tests/test_data.js b/packages/optimizely-sdk/lib/tests/test_data.js index 39fa7bd4c..f35a47cd7 100644 --- a/packages/optimizely-sdk/lib/tests/test_data.js +++ b/packages/optimizely-sdk/lib/tests/test_data.js @@ -3339,6 +3339,346 @@ export var featureTestDecisionObj = { decisionSource: 'feature-test', } +var similarRuleKeyConfig = { + version: "4", + rollouts: [ + { + experiments: [ + { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5452", + key: "on", + featureEnabled: true + } + ], + forcedVariations: {}, + key: "targeted_delivery", + layerId: "9300000004981", + trafficAllocation: [ + { + entityId: "5452", + endOfRange: 10000 + } + ], + id: "9300000004981" + }, { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5451", + key: "off", + featureEnabled: false + } + ], + forcedVariations: {}, + key: "default-rollout-2029-20301771717", + layerId: "default-layer-rollout-2029-20301771717", + trafficAllocation: [ + { + entityId: "5451", + endOfRange: 10000 + } + ], + id: "default-rollout-2029-20301771717" + } + ], + id: "rollout-2029-20301771717" + }, { + experiments: [ + { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5450", + key: "on", + featureEnabled: true + } + ], + forcedVariations: {}, + key: "targeted_delivery", + layerId: "9300000004979", + trafficAllocation: [ + { + entityId: "5450", + endOfRange: 10000 + } + ], + id: "9300000004979" + }, { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5449", + key: "off", + featureEnabled: false + } + ], + forcedVariations: {}, + key: "default-rollout-2028-20301771717", + layerId: "default-layer-rollout-2028-20301771717", + trafficAllocation: [ + { + entityId: "5449", + endOfRange: 10000 + } + ], + id: "default-rollout-2028-20301771717" + } + ], + id: "rollout-2028-20301771717" + }, { + experiments: [ + { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5448", + key: "on", + featureEnabled: true + } + ], + forcedVariations: {}, + key: "targeted_delivery", + layerId: "9300000004977", + trafficAllocation: [ + { + entityId: "5448", + endOfRange: 10000 + } + ], + id: "9300000004977" + }, { + status: "Running", + audienceConditions: [], + audienceIds: [], + variations: [ + { + variables: [], + id: "5447", + key: "off", + featureEnabled: false + } + ], + forcedVariations: {}, + key: "default-rollout-2027-20301771717", + layerId: "default-layer-rollout-2027-20301771717", + trafficAllocation: [ + { + entityId: "5447", + endOfRange: 10000 + } + ], + id: "default-rollout-2027-20301771717" + } + ], + id: "rollout-2027-20301771717" + } + ], + typedAudiences: [], + anonymizeIP: true, + projectId: "20286295225", + variables: [], + featureFlags: [ + { + experimentIds: [], + rolloutId: "rollout-2029-20301771717", + variables: [], + id: "2029", + key: "flag_3" + }, { + experimentIds: [], + rolloutId: "rollout-2028-20301771717", + variables: [], + id: "2028", + key: "flag_2" + }, { + experimentIds: [], + rolloutId: "rollout-2027-20301771717", + variables: [], + id: "2027", + key: "flag_1" + } + ], + experiments: [], + audiences: [ + { + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "$opt_dummy_audience", + name: "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + groups: [], + attributes: [], + botFiltering: false, + accountId: "19947277778", + events: [], + revision: "11", + sendFlagDecisions: true +} + +export var getSimilarRuleKeyConfig = function() { + return cloneDeep(similarRuleKeyConfig); +}; + +var similarExperimentKeysConfig = { + version: "4", + rollouts: [], + typedAudiences: [ + { + id: "20415611520", + conditions: [ + "and", + [ + "or", + [ + "or", { + value: true, + type: "custom_attribute", + name: "hiddenLiveEnabled", + match: "exact" + } + ] + ] + ], + name: "test1" + }, { + id: "20406066925", + conditions: [ + "and", + [ + "or", + [ + "or", { + value: false, + type: "custom_attribute", + name: "hiddenLiveEnabled", + match: "exact" + } + ] + ] + ], + name: "test2" + } + ], + anonymizeIP: true, + projectId: "20430981610", + variables: [], + featureFlags: [ + { + experimentIds: ["9300000007569"], + rolloutId: "", + variables: [], + id: "3045", + key: "flag1" + }, { + experimentIds: ["9300000007573"], + rolloutId: "", + variables: [], + id: "3046", + key: "flag2" + } + ], + experiments: [ + { + status: "Running", + audienceConditions: [ + "or", "20415611520" + ], + audienceIds: ["20415611520"], + variations: [ + { + variables: [], + id: "8045", + key: "variation1", + featureEnabled: true + } + ], + forcedVariations: {}, + key: "targeted_delivery", + layerId: "9300000007569", + trafficAllocation: [ + { + entityId: "8045", + endOfRange: 10000 + } + ], + id: "9300000007569" + }, { + status: "Running", + audienceConditions: [ + "or", "20406066925" + ], + audienceIds: ["20406066925"], + variations: [ + { + variables: [], + id: "8048", + key: "variation2", + featureEnabled: true + } + ], + forcedVariations: {}, + key: "targeted_delivery", + layerId: "9300000007573", + trafficAllocation: [ + { + entityId: "8048", + endOfRange: 10000 + } + ], + id: "9300000007573" + } + ], + audiences: [ + { + id: "20415611520", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + name: "test1" + }, { + id: "20406066925", + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + name: "test2" + }, { + conditions: "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + id: "$opt_dummy_audience", + name: "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + groups: [], + attributes: [ + { + id: "20408641883", + key: "hiddenLiveEnabled" + } + ], + botFiltering: false, + accountId: "17882702980", + events: [], + revision: "25", + sendFlagDecisions: true +} + +export var getSimilarExperimentKeyConfig = function() { + return cloneDeep(similarExperimentKeysConfig); +}; + export default { getTestProjectConfig: getTestProjectConfig, getTestDecideProjectConfig: getTestDecideProjectConfig, @@ -3349,4 +3689,6 @@ export default { getTypedAudiencesConfig: getTypedAudiencesConfig, typedAudiencesById: typedAudiencesById, getMutexFeatureTestsConfig: getMutexFeatureTestsConfig, + getSimilarRuleKeyConfig: getSimilarRuleKeyConfig, + getSimilarExperimentKeyConfig: getSimilarExperimentKeyConfig };