From c06fe4388a9186cf9ddc0f6ab1fcc2f424408c95 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Thu, 5 Aug 2021 22:15:47 +0500 Subject: [PATCH 01/12] Initial commit. --- .../lib/core/optimizely_config/index.ts | 511 +++++++++++++----- .../lib/core/project_config/index.ts | 16 +- packages/optimizely-sdk/lib/shared_types.ts | 30 +- 3 files changed, 417 insertions(+), 140 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 788339d3a..ee9c88365 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -22,6 +22,13 @@ import { VariationVariable, Variation, Rollout, + Experiment, + OptimizelyAttribute, + OptimizelyAudience, + OptimizelyEvent, + OptimizelyExperiment, + OptimizelyVariable, + OptimizelyVariation, } from '../../shared_types'; interface FeatureVariablesMap { @@ -37,20 +44,29 @@ export class OptimizelyConfig { public experimentsMap: OptimizelyExperimentsMap; public featuresMap: OptimizelyFeaturesMap; public revision: string; - public sdkKey?: string; - public environmentKey?: 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); + 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(featureIdVariablesMap, configObj, experimentsMapById); this.revision = configObj.revision; - this.datafile = datafile; + this.attributes = configObj.attributes; - if (configObj.sdkKey && configObj.environmentKey) { - this.sdkKey = configObj.sdkKey; - this.environmentKey = configObj.environmentKey; - } + this.audiences = OptimizelyConfig.getAudiences(configObj); + this.events = configObj.events; + this.datafile = datafile; + this.sdkKey = configObj.sdkKey ? configObj.sdkKey : ''; + this.environmentKey = configObj.environmentKey ? configObj.environmentKey : ''; } /** @@ -61,6 +77,43 @@ export class OptimizelyConfig { return this.datafile; } + /** + * Get Unique audiences list with typedAudiences as priority + * @param {ProjectConfig} configObj + * @returns {OptimizelyAudience[]} Array of unique audiences + */ + static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { + var finalAudiences: OptimizelyAudience[] = []; + var typedAudiencesMap = new Map(); + + (configObj.typedAudiences || []).forEach((typedAudience) => { + finalAudiences.push(typedAudience); + typedAudiencesMap.set(typedAudience.id, typedAudience); + }); + + (configObj.audiences || []).forEach((oldAudience) => { + if (!typedAudiencesMap.has(oldAudience.id) && oldAudience.id != '$opt_dummy_audience') { + finalAudiences.push(oldAudience); + } + }); + + return finalAudiences; + } + + static getExperimentAudiences(experiment: Experiment, configObj: ProjectConfig): string { + if (!experiment.audienceConditions || experiment.audienceConditions.length === 0) { + return ''; + } + return ''; + } + + static GetSerializedAudiences(experiment: Experiment, configObj: ProjectConfig): string { + if (!experiment.audienceConditions || experiment.audienceConditions.length === 0) { + return ''; + } + return ''; + } + /** * Get Experiment Ids which are part of rollout * @param {Rollout[]} rollouts @@ -69,110 +122,310 @@ export class OptimizelyConfig { static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { rollout.experiments.forEach((e) => { - (experimentIds)[e.id] = true; + experimentIds[e.id] = true; }); return experimentIds; }, {}); } - /** - * Get Map of all experiments except rollouts - * @param {ProjectConfig} configObj - * @returns {OptimizelyExperimentsMap} Map of experiments excluding rollouts - */ - static getExperimentsMap(configObj: ProjectConfig): OptimizelyExperimentsMap { + static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { + var experimentKeyMaps: OptimizelyExperimentsMap = {}; + + for (let key in experimentsMapById) { + let experiment = experimentsMapById[key]; + experimentKeyMaps[experiment.key] = experiment; + } + return experimentKeyMaps; + } + + static getExperimentsMapById( + configObj: ProjectConfig, + featureIdVariableMap: FeatureVariablesMap + ): OptimizelyExperimentsMap { + var experimentsMap: OptimizelyExperimentsMap = {}; + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); - const featureVariablesMap = (configObj.featureFlags || []).reduce( - (resultMap: FeatureVariablesMap, feature) => { - resultMap[feature.id] = feature.variables; - return resultMap; - }, - {}, - ); - 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; - }, - {}, - ), - }; + const experiments = configObj.experiments; + configObj.groups.forEach((group) => { + experiments.concat(group.experiments); + }); + + experiments.forEach((experiment) => { + if (!rolloutExperimentIds[experiment.id]) { + var featureIds = configObj.experimentFeatureMap[experiment.id]; + var featureId = ''; + if (featureIds && featureIds.length > 0) { + featureId = featureIds[0]; } + const variationsMap = OptimizelyConfig.GetVariationsMap( + experiment.variations, + featureIdVariableMap, + variableIdMap, + featureId.toString() + ); + + // TODO: fix audiences here + experimentsMap[experiment.id] = { + id: experiment.id, + key: experiment.key, + variationsMap: variationsMap, + audiences: '', + }; + } + }); + return experimentsMap; + } - return experiments; + static GetVariationsMap( + variations: Variation[], + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string + ): { [key: string]: Variation } { + var variationsMap: { [key: string]: OptimizelyVariation } = {}; + variations.forEach((variation) => { + const variablesMap = OptimizelyConfig.mergeFeatureVariables( + featureIdVariableMap, + variableIdMap, + featureId, + variation.variables, + variation.featureEnabled + ); + variationsMap[variation.key] = { + id: variation.id, + key: variation.key, + featureEnabled: variation.featureEnabled, + variablesMap: variablesMap, + }; + }); + return variationsMap; + } + + static mergeFeatureVariables( + featureIdVariableMap: FeatureVariablesMap, + variableIdMap: { [id: string]: FeatureVariable }, + featureId: string, + featureVariableUsages: VariationVariable[] | undefined, + isFeatureEnabled: boolean | undefined + ): OptimizelyVariablesMap { + var variablesMap: OptimizelyVariablesMap = {}; + if (!featureId) { + return variablesMap; + } + + variablesMap = (featureIdVariableMap[featureId] || []).reduce( + (variablesMap: OptimizelyVariablesMap, featureVariable) => { + variablesMap[featureVariable.key] = { + id: featureVariable.id, + key: featureVariable.key, + type: featureVariable.type, + value: featureVariable.defaultValue, + }; + return variablesMap; }, - {}, - ) + {} + ); + + (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; + } + + static getVariableIdMap(configObj: ProjectConfig): { [id: string]: FeatureVariable } { + var 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; + } + + static getDeliveryRules( + featureVariableIdMap: FeatureVariablesMap, + featureId: string, + experiments: Experiment[] | undefined, + configObj: ProjectConfig + ): OptimizelyExperiment[] { + var deliveryRules: OptimizelyExperiment[] = []; + if (!experiments) { + return deliveryRules; + } + + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); + + // TODO: fix audiences here + experiments.forEach((experiment) => { + deliveryRules.push({ + id: experiment.id, + key: experiment.key, + audiences: '', + variationsMap: OptimizelyConfig.GetVariationsMap( + experiment.variations, + featureVariableIdMap, + variableIdMap, + featureId + ), + }); + }); + return deliveryRules; + } + + static getFeaturesMap( + featureVariableIdMap: FeatureVariablesMap, + configObj: ProjectConfig, + experimentsMapById: OptimizelyExperimentsMap + ): OptimizelyFeaturesMap { + var featuresMap: OptimizelyFeaturesMap = {}; + configObj.featureFlags.forEach((featureFlag) => { + var featureExperimentMap: OptimizelyExperimentsMap = {}; + var experimentRules: OptimizelyExperiment[] = []; + for (let key in experimentsMapById) { + if (featureFlag.experimentIds.includes(key)) { + featureExperimentMap[key] = experimentsMapById[key]; + 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; + }, {}); + var deliveryRules: OptimizelyExperiment[] = [] + const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; + if (rollout) { + deliveryRules = OptimizelyConfig.getDeliveryRules( + featureVariableIdMap, + featureFlag.id, + rollout.experiments, + configObj + ); + } + featuresMap[featureFlag.key] = { + id: featureFlag.id, + key: featureFlag.key, + experimentRules: experimentRules, + deliveryRules: deliveryRules, + experimentsMap: featureExperimentMap, + variablesMap: featureVariableMap, + }; + }); + return featuresMap; } /** - * Merge feature key and type from feature variables to variation variables + * Get Map of all experiments except rollouts * @param {ProjectConfig} configObj - * @param {Variation} variation - * @param {string} experimentId - * @param {FeatureVariablesMap} featureVariablesMap - * @returns {OptimizelyVariablesMap} Map of variables + * @returns {OptimizelyExperimentsMap} Map of experiments excluding rollouts */ - static getMergedVariablesMap( - 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; - }, - {}, - ); - } + // 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; + // }, + // {}, + // ); - return variablesObject; - } + // 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; + // }, + // {}, + // ), + // }; + // } + + // return experiments; + // }, + // {}, + // ) + // } + + // /** + // * 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 + // */ + // static getMergedVariablesMap( + // 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; + // }, + // {}, + // ); + // } + + // return variablesObject; + // } /** * Get map of all experiments @@ -180,40 +433,40 @@ export class OptimizelyConfig { * @param {OptimizelyExperimentsMap} allExperiments * @returns {OptimizelyFeaturesMap} Map of all experiments */ - static getFeaturesMap( - configObj: ProjectConfig, - allExperiments: 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; - }, - {}, - ), - }; + // static getFeaturesMap( + // configObj: ProjectConfig, + // allExperiments: 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 features; - }, {}); - } + // return variables; + // }, + // {}, + // ), + // }; + + // return features; + // }, {}); + // } } /** diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index c58b4e136..05da2935c 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -36,7 +36,8 @@ import { Variation, OptimizelyVariation, VariableType, - VariationVariable + VariationVariable, + OptimizelyAttribute } from '../../shared_types'; interface TryCreatingProjectConfigConfig { @@ -50,6 +51,7 @@ interface TryCreatingProjectConfigConfig { interface Event { key: string; id: string; + experimentsIds: string[]; } interface VariableUsageMap { @@ -59,8 +61,8 @@ interface VariableUsageMap { export interface ProjectConfig { revision: string; projectId: string; - sdkKey?: string; - environmentKey?: string; + sdkKey: string; + environmentKey: string; sendFlagDecisions?: boolean; experimentKeyMap: { [key: string]: Experiment }; featureKeyMap: { @@ -81,7 +83,7 @@ export interface ProjectConfig { groupIdMap: { [id: string]: Group }; groups: Group[]; events: Event[]; - attributes: Array<{ id: string }>; + attributes: OptimizelyAttribute[]; typedAudiences: Audience[]; rolloutIdMap: { [id: string]: Rollout }; anonymizeIP?: boolean | null; @@ -120,10 +122,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 ? datafile.environmentKey : "" + datafileCopy.sdkKey = datafile.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..83f9befd7 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: unknown[] | string; variationsMap: { [variationKey: string]: OptimizelyVariation; }; @@ -288,7 +290,7 @@ export interface SDKOptions { defaultDecideOptions?: OptimizelyDecideOption[]; } -export type OptimizelyExperimentsMap = { +export type OptimizelyExperimentsMap = { [experimentKey: string]: OptimizelyExperiment; } @@ -300,11 +302,30 @@ export type OptimizelyFeaturesMap = { [featureKey: string]: OptimizelyFeature; } +export type OptimizelyAttribute = { + id: string; + key: string; +} + +export type OptimizelyAudience = { + id: string; + name: string; + conditions: unknown[] | string; +} + +export type OptimizelyEvent = { + id: string; + key: string; + experimentsIds: string[]; +} + export interface OptimizelyFeature { id: string; key: string; experimentsMap: OptimizelyExperimentsMap; variablesMap: OptimizelyVariablesMap; + experimentRules: OptimizelyExperiment[]; + deliveryRules: OptimizelyExperiment[]; } export interface OptimizelyVariation { @@ -317,9 +338,12 @@ export interface OptimizelyVariation { export interface OptimizelyConfig { experimentsMap: OptimizelyExperimentsMap; featuresMap: OptimizelyFeaturesMap; + attributes: OptimizelyAttribute[]; + audiences: OptimizelyAudience[]; + events: OptimizelyEvent[]; revision: string; - sdkKey?: string; - environmentKey?: string; + sdkKey: string; + environmentKey: string; getDatafile(): string; } From d313457bdf15ae863b88afff732dc48f981fac1e Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Fri, 6 Aug 2021 16:55:23 +0500 Subject: [PATCH 02/12] Code for condition serialization added. --- .../lib/core/optimizely_config/index.ts | 245 ++++++------------ 1 file changed, 73 insertions(+), 172 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index ee9c88365..cc0808463 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -23,6 +23,7 @@ import { Variation, Rollout, Experiment, + Audience, OptimizelyAttribute, OptimizelyAudience, OptimizelyEvent, @@ -50,23 +51,25 @@ export class OptimizelyConfig { public audiences: OptimizelyAudience[]; public events: OptimizelyEvent[]; private datafile: string; + private static audienceConditionalOperators: {[operator:string]:boolean} = {"and": true, "or": true, "not": true} constructor(configObj: ProjectConfig, datafile: string) { + this.sdkKey = configObj.sdkKey ? configObj.sdkKey : ''; + this.environmentKey = configObj.environmentKey ? configObj.environmentKey : ''; + this.attributes = configObj.attributes; + this.audiences = OptimizelyConfig.getAudiences(configObj); + this.events = configObj.events; + this.revision = configObj.revision; + 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(featureIdVariablesMap, configObj, experimentsMapById); - this.revision = configObj.revision; - this.attributes = configObj.attributes; - - this.audiences = OptimizelyConfig.getAudiences(configObj); - this.events = configObj.events; + this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj,featureIdVariablesMap, experimentsMapById); this.datafile = datafile; - this.sdkKey = configObj.sdkKey ? configObj.sdkKey : ''; - this.environmentKey = configObj.environmentKey ? configObj.environmentKey : ''; } /** @@ -84,16 +87,16 @@ export class OptimizelyConfig { */ static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { var finalAudiences: OptimizelyAudience[] = []; - var typedAudiencesMap = new Map(); + var typedAudienceIds: {[id: string]: boolean} = {}; (configObj.typedAudiences || []).forEach((typedAudience) => { - finalAudiences.push(typedAudience); - typedAudiencesMap.set(typedAudience.id, typedAudience); + finalAudiences.push({id: typedAudience.id, conditions: JSON.stringify(typedAudience.conditions), name: typedAudience.name}); + typedAudienceIds[typedAudience.id] = true; }); (configObj.audiences || []).forEach((oldAudience) => { - if (!typedAudiencesMap.has(oldAudience.id) && oldAudience.id != '$opt_dummy_audience') { - finalAudiences.push(oldAudience); + if (!typedAudienceIds[oldAudience.id] && oldAudience.id != '$opt_dummy_audience') { + finalAudiences.push({id: oldAudience.id, conditions: JSON.stringify(oldAudience.conditions), name: oldAudience.name}); } }); @@ -104,14 +107,53 @@ export class OptimizelyConfig { if (!experiment.audienceConditions || experiment.audienceConditions.length === 0) { return ''; } - return ''; + return OptimizelyConfig.GetSerializedAudiences(experiment.audienceConditions,configObj.audiencesById); } - static GetSerializedAudiences(experiment: Experiment, configObj: ProjectConfig): string { - if (!experiment.audienceConditions || experiment.audienceConditions.length === 0) { - return ''; + static GetSerializedAudiences(conditions: Array, audiencesById: {[id:string]:Audience}): string { + var serializedAudience = "" + if (conditions) { + var cond = "" + conditions.forEach(item => { + var subAudience = "" + // Checks if item is list of conditions means if it is sub audience + if (Array.isArray(item)) { + subAudience = OptimizelyConfig.GetSerializedAudiences(item,audiencesById) + subAudience = `(${subAudience})` + } else if (OptimizelyConfig.audienceConditionalOperators[item.toString()]) { + cond = item.toString().toUpperCase() + } else { + // Checks if item is audience id + const itemStr = item.toString() + var audienceName = audiencesById[itemStr] ? audiencesById[itemStr].name : itemStr + // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in sAudience then append condition between saudience and item + if (serializedAudience || cond == "NOT") { + cond = cond ? cond : "OR" + if (serializedAudience) { + serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`) + } else { + serializedAudience = `"${audiencesById[itemStr].name}"` + } + } else { + serializedAudience = `"${audienceName}"` + } + } + // Checks if sub audience is empty or not + if(subAudience) { + if (serializedAudience || cond == "NOT") { + cond = cond ? cond : "OR" + if (serializedAudience) { + serializedAudience = ` ${cond} ${subAudience}` + } else { + serializedAudience = `${cond} ${subAudience}` + } + } else { + serializedAudience = serializedAudience.concat(subAudience) + } + } + }); } - return ''; + return serializedAudience; } /** @@ -130,20 +172,20 @@ export class OptimizelyConfig { } static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { - var experimentKeyMaps: OptimizelyExperimentsMap = {}; + var experimentKeysMap: OptimizelyExperimentsMap = {}; - for (let key in experimentsMapById) { - let experiment = experimentsMapById[key]; - experimentKeyMaps[experiment.key] = experiment; + for (let id in experimentsMapById) { + let experiment = experimentsMapById[id]; + experimentKeysMap[experiment.key] = experiment; } - return experimentKeyMaps; + return experimentKeysMap; } static getExperimentsMapById( configObj: ProjectConfig, featureIdVariableMap: FeatureVariablesMap - ): OptimizelyExperimentsMap { - var experimentsMap: OptimizelyExperimentsMap = {}; + ): {[id:string]: OptimizelyExperiment} { + var experimentsMap: {[id:string]: OptimizelyExperiment} = {}; const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); @@ -166,12 +208,11 @@ export class OptimizelyConfig { featureId.toString() ); - // TODO: fix audiences here experimentsMap[experiment.id] = { id: experiment.id, key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment,configObj), variationsMap: variationsMap, - audiences: '', }; } }); @@ -254,10 +295,10 @@ export class OptimizelyConfig { } static getDeliveryRules( + configObj: ProjectConfig, featureVariableIdMap: FeatureVariablesMap, featureId: string, experiments: Experiment[] | undefined, - configObj: ProjectConfig ): OptimizelyExperiment[] { var deliveryRules: OptimizelyExperiment[] = []; if (!experiments) { @@ -266,12 +307,11 @@ export class OptimizelyConfig { const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); - // TODO: fix audiences here experiments.forEach((experiment) => { deliveryRules.push({ id: experiment.id, key: experiment.key, - audiences: '', + audiences: OptimizelyConfig.getExperimentAudiences(experiment,configObj), variationsMap: OptimizelyConfig.GetVariationsMap( experiment.variations, featureVariableIdMap, @@ -284,9 +324,9 @@ export class OptimizelyConfig { } static getFeaturesMap( - featureVariableIdMap: FeatureVariablesMap, configObj: ProjectConfig, - experimentsMapById: OptimizelyExperimentsMap + featureVariableIdMap: FeatureVariablesMap, + experimentsMapById: OptimizelyExperimentsMap, ): OptimizelyFeaturesMap { var featuresMap: OptimizelyFeaturesMap = {}; configObj.featureFlags.forEach((featureFlag) => { @@ -311,10 +351,10 @@ export class OptimizelyConfig { const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; if (rollout) { deliveryRules = OptimizelyConfig.getDeliveryRules( + configObj, featureVariableIdMap, featureFlag.id, rollout.experiments, - configObj ); } featuresMap[featureFlag.key] = { @@ -328,145 +368,6 @@ export class OptimizelyConfig { }); return featuresMap; } - - /** - * Get Map of all experiments except rollouts - * @param {ProjectConfig} configObj - * @returns {OptimizelyExperimentsMap} Map of experiments excluding rollouts - */ - // 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; - // }, - // {}, - // ); - - // 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; - // }, - // {}, - // ), - // }; - // } - - // return experiments; - // }, - // {}, - // ) - // } - - // /** - // * 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 - // */ - // static getMergedVariablesMap( - // 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; - // }, - // {}, - // ); - // } - - // return variablesObject; - // } - - /** - * Get map of all experiments - * @param {ProjectConfig} configObj - * @param {OptimizelyExperimentsMap} allExperiments - * @returns {OptimizelyFeaturesMap} Map of all experiments - */ - // static getFeaturesMap( - // configObj: ProjectConfig, - // allExperiments: 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; - // }, - // {}, - // ), - // }; - - // return features; - // }, {}); - // } } /** From 24fb82a88675340e822b465eeddf884100656fd8 Mon Sep 17 00:00:00 2001 From: Sohail Hussain Date: Fri, 6 Aug 2021 11:45:28 -0700 Subject: [PATCH 03/12] fix optimizely experiments map --- .../optimizely-sdk/lib/core/optimizely_config/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index cc0808463..caa521241 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -140,7 +140,7 @@ export class OptimizelyConfig { } // Checks if sub audience is empty or not if(subAudience) { - if (serializedAudience || cond == "NOT") { + if (serializedAudience || cond == "NOT") { cond = cond ? cond : "OR" if (serializedAudience) { serializedAudience = ` ${cond} ${subAudience}` @@ -334,7 +334,10 @@ export class OptimizelyConfig { var experimentRules: OptimizelyExperiment[] = []; for (let key in experimentsMapById) { if (featureFlag.experimentIds.includes(key)) { - featureExperimentMap[key] = experimentsMapById[key]; + var experiment = experimentsMapById[key]; + if (experiment) { + featureExperimentMap[experiment.key] = experiment; + } experimentRules.push(experimentsMapById[key]); } } From 8021a9755d4299711c5b195e04af6a9de1f5e384 Mon Sep 17 00:00:00 2001 From: Sohail Hussain Date: Fri, 6 Aug 2021 15:41:10 -0700 Subject: [PATCH 04/12] improvements --- .../lib/core/optimizely_config/index.ts | 105 +++++++++--------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index caa521241..175fb7db9 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -51,7 +51,7 @@ export class OptimizelyConfig { public audiences: OptimizelyAudience[]; public events: OptimizelyEvent[]; private datafile: string; - private static audienceConditionalOperators: {[operator:string]:boolean} = {"and": true, "or": true, "not": true} + private static audienceConditionalOperators: string[] = ['and', 'or', 'not']; constructor(configObj: ProjectConfig, datafile: string) { this.sdkKey = configObj.sdkKey ? configObj.sdkKey : ''; @@ -117,10 +117,10 @@ export class OptimizelyConfig { conditions.forEach(item => { var subAudience = "" // Checks if item is list of conditions means if it is sub audience - if (Array.isArray(item)) { + if (item instanceof Array) { subAudience = OptimizelyConfig.GetSerializedAudiences(item,audiencesById) subAudience = `(${subAudience})` - } else if (OptimizelyConfig.audienceConditionalOperators[item.toString()]) { + } else if (OptimizelyConfig.audienceConditionalOperators.includes(item)) { cond = item.toString().toUpperCase() } else { // Checks if item is audience id @@ -156,20 +156,20 @@ export class OptimizelyConfig { return serializedAudience; } - /** - * Get Experiment Ids which are part of rollout - * @param {Rollout[]} rollouts - * @returns {[key: string]: boolean} Map of experiment Ids to boolean - */ - static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { - return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { - rollout.experiments.forEach((e) => { - experimentIds[e.id] = true; - }); + // /** + // * Get Experiment Ids which are part of rollout + // * @param {Rollout[]} rollouts + // * @returns {[key: string]: boolean} Map of experiment Ids to boolean + // */ + // static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { + // return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { + // rollout.experiments.forEach((e) => { + // experimentIds[e.id] = true; + // }); - return experimentIds; - }, {}); - } + // return experimentIds; + // }, {}); + // } static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { var experimentKeysMap: OptimizelyExperimentsMap = {}; @@ -185,37 +185,31 @@ export class OptimizelyConfig { configObj: ProjectConfig, featureIdVariableMap: FeatureVariablesMap ): {[id:string]: OptimizelyExperiment} { - var experimentsMap: {[id:string]: OptimizelyExperiment} = {}; const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); - const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); const experiments = configObj.experiments; configObj.groups.forEach((group) => { experiments.concat(group.experiments); }); + let experimentsMap = experiments.reduce(( experimentsMap: { [id: string]: OptimizelyExperiment }, experiment) => { + var featureIds = configObj.experimentFeatureMap[experiment.id] || []; + var featureId = featureIds.length > 0 ? featureIds[0] : ''; - experiments.forEach((experiment) => { - if (!rolloutExperimentIds[experiment.id]) { - var featureIds = configObj.experimentFeatureMap[experiment.id]; - var featureId = ''; - if (featureIds && featureIds.length > 0) { - featureId = featureIds[0]; - } - const variationsMap = OptimizelyConfig.GetVariationsMap( - experiment.variations, - featureIdVariableMap, - variableIdMap, - featureId.toString() - ); + 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, - }; - } - }); + experimentsMap[experiment.id] = { + id: experiment.id, + key: experiment.key, + audiences: OptimizelyConfig.getExperimentAudiences(experiment,configObj), + variationsMap: variationsMap, + }; + return experimentsMap; + }, {}); return experimentsMap; } @@ -226,21 +220,24 @@ export class OptimizelyConfig { featureId: string ): { [key: string]: Variation } { var variationsMap: { [key: string]: OptimizelyVariation } = {}; - variations.forEach((variation) => { - const variablesMap = OptimizelyConfig.mergeFeatureVariables( - featureIdVariableMap, - variableIdMap, - featureId, - variation.variables, - variation.featureEnabled - ); - variationsMap[variation.key] = { - id: variation.id, - key: variation.key, - featureEnabled: variation.featureEnabled, - variablesMap: variablesMap, - }; - }); + variationsMap = variations.reduce( + (variationsMap: {[key:string]: OptimizelyVariation}, variation) => { + const variablesMap = OptimizelyConfig.mergeFeatureVariables( + featureIdVariableMap, + variableIdMap, + featureId, + variation.variables, + variation.featureEnabled + ); + variationsMap[variation.key] = { + id: variation.id, + key: variation.key, + featureEnabled: variation.featureEnabled, + variablesMap: variablesMap, + }; + return variationsMap; + }, {}); + return variationsMap; } From dd5d2b063524ee6c963ac93d4797977f8259b1fd Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Mon, 9 Aug 2021 14:26:47 +0500 Subject: [PATCH 05/12] cleanup and fixes. --- .../lib/core/optimizely_config/index.ts | 341 +++++++++++------- .../lib/core/project_config/index.ts | 12 +- packages/optimizely-sdk/lib/shared_types.ts | 8 +- 3 files changed, 221 insertions(+), 140 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 175fb7db9..7eb36902e 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,23 +13,23 @@ * 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 { + Audience, + Experiment, + FeatureVariable, OptimizelyExperimentsMap, OptimizelyFeaturesMap, OptimizelyVariablesMap, - FeatureVariable, - VariationVariable, - Variation, - Rollout, - Experiment, - Audience, OptimizelyAttribute, OptimizelyAudience, OptimizelyEvent, OptimizelyExperiment, OptimizelyVariable, OptimizelyVariation, + Rollout, + VariationVariable, + Variation, } from '../../shared_types'; interface FeatureVariablesMap { @@ -68,7 +68,7 @@ export class OptimizelyConfig { const experimentsMapById = OptimizelyConfig.getExperimentsMapById(configObj, featureIdVariablesMap); this.experimentsMap = OptimizelyConfig.getExperimentsKeyMap(experimentsMapById); - this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj,featureIdVariablesMap, experimentsMapById); + this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, featureIdVariablesMap, experimentsMapById); this.datafile = datafile; } @@ -87,68 +87,87 @@ export class OptimizelyConfig { */ static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { var finalAudiences: OptimizelyAudience[] = []; - var typedAudienceIds: {[id: string]: boolean} = {}; + var typedAudienceIds: { [id: string]: boolean } = {}; (configObj.typedAudiences || []).forEach((typedAudience) => { - finalAudiences.push({id: typedAudience.id, conditions: JSON.stringify(typedAudience.conditions), name: typedAudience.name}); + finalAudiences.push({ + id: typedAudience.id, + conditions: JSON.stringify(typedAudience.conditions), + name: typedAudience.name, + }); typedAudienceIds[typedAudience.id] = true; }); (configObj.audiences || []).forEach((oldAudience) => { if (!typedAudienceIds[oldAudience.id] && oldAudience.id != '$opt_dummy_audience') { - finalAudiences.push({id: oldAudience.id, conditions: JSON.stringify(oldAudience.conditions), name: oldAudience.name}); + finalAudiences.push({ + id: oldAudience.id, + conditions: JSON.stringify(oldAudience.conditions), + name: oldAudience.name, + }); } }); return finalAudiences; } - static getExperimentAudiences(experiment: Experiment, configObj: ProjectConfig): string { - if (!experiment.audienceConditions || experiment.audienceConditions.length === 0) { - return ''; - } - return OptimizelyConfig.GetSerializedAudiences(experiment.audienceConditions,configObj.audiencesById); - } + /** + * 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 { + var serializedAudience = ''; - static GetSerializedAudiences(conditions: Array, audiencesById: {[id:string]:Audience}): string { - var serializedAudience = "" if (conditions) { - var cond = "" - conditions.forEach(item => { - var subAudience = "" + var cond = ''; + conditions.forEach((item) => { + var subAudience = ''; // Checks if item is list of conditions means if it is sub audience if (item instanceof Array) { - subAudience = OptimizelyConfig.GetSerializedAudiences(item,audiencesById) - subAudience = `(${subAudience})` + subAudience = OptimizelyConfig.GetSerializedAudiences(item, audiencesById); + subAudience = `(${subAudience})`; } else if (OptimizelyConfig.audienceConditionalOperators.includes(item)) { - cond = item.toString().toUpperCase() + cond = item.toUpperCase(); } else { // Checks if item is audience id - const itemStr = item.toString() - var audienceName = audiencesById[itemStr] ? audiencesById[itemStr].name : itemStr - // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in sAudience then append condition between saudience and item - if (serializedAudience || cond == "NOT") { - cond = cond ? cond : "OR" - if (serializedAudience) { - serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`) + var 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 = `"${audiencesById[itemStr].name}"` + serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`); } } else { - serializedAudience = `"${audienceName}"` + serializedAudience = `"${audienceName}"`; } - } + } // Checks if sub audience is empty or not - if(subAudience) { - if (serializedAudience || cond == "NOT") { - cond = cond ? cond : "OR" - if (serializedAudience) { - serializedAudience = ` ${cond} ${subAudience}` + if (subAudience !== '') { + if (serializedAudience !== '' || cond === 'NOT') { + cond = cond === '' ? 'OR' : cond; + if (serializedAudience === '') { + serializedAudience = `${cond} ${subAudience}`; } else { - serializedAudience = `${cond} ${subAudience}` + serializedAudience = serializedAudience.concat(` ${cond} ${subAudience}`); } } else { - serializedAudience = serializedAudience.concat(subAudience) + serializedAudience = serializedAudience.concat(subAudience); } } }); @@ -156,91 +175,28 @@ export class OptimizelyConfig { return serializedAudience; } - // /** - // * Get Experiment Ids which are part of rollout - // * @param {Rollout[]} rollouts - // * @returns {[key: string]: boolean} Map of experiment Ids to boolean - // */ - // static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { - // return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { - // rollout.experiments.forEach((e) => { - // experimentIds[e.id] = true; - // }); - - // return experimentIds; - // }, {}); - // } - - static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { - var experimentKeysMap: OptimizelyExperimentsMap = {}; - - for (let id in experimentsMapById) { - let experiment = experimentsMapById[id]; - experimentKeysMap[experiment.key] = experiment; + /** + * Get serialized audience condition string for experiment + * @param {Experiment} experiment + * @param {ProjectConfig} configObj + * @returns {string} Serialized audiences condition string + */ + static getExperimentAudiences(experiment: Experiment, configObj: ProjectConfig): string { + if (!experiment.audienceConditions) { + return ''; } - return experimentKeysMap; - } - - static getExperimentsMapById( - configObj: ProjectConfig, - featureIdVariableMap: FeatureVariablesMap - ): {[id:string]: OptimizelyExperiment} { - const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); - - const experiments = configObj.experiments; - configObj.groups.forEach((group) => { - experiments.concat(group.experiments); - }); - let experimentsMap = experiments.reduce(( experimentsMap: { [id: string]: OptimizelyExperiment }, experiment) => { - var featureIds = configObj.experimentFeatureMap[experiment.id] || []; - var featureId = featureIds.length > 0 ? 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; - }, {}); - return experimentsMap; - } - - static GetVariationsMap( - variations: Variation[], - featureIdVariableMap: FeatureVariablesMap, - variableIdMap: { [id: string]: FeatureVariable }, - featureId: string - ): { [key: string]: Variation } { - var variationsMap: { [key: string]: OptimizelyVariation } = {}; - variationsMap = variations.reduce( - (variationsMap: {[key:string]: OptimizelyVariation}, variation) => { - const variablesMap = OptimizelyConfig.mergeFeatureVariables( - featureIdVariableMap, - variableIdMap, - featureId, - variation.variables, - variation.featureEnabled - ); - variationsMap[variation.key] = { - id: variation.id, - key: variation.key, - featureEnabled: variation.featureEnabled, - variablesMap: variablesMap, - }; - return variationsMap; - }, {}); - - return variationsMap; + 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 }, @@ -279,6 +235,46 @@ export class OptimizelyConfig { return variablesMap; } + /** + * 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 } { + var variationsMap: { [key: string]: OptimizelyVariation } = {}; + variationsMap = variations.reduce((variationsMap: { [key: string]: OptimizelyVariation }, variation) => { + const variablesMap = OptimizelyConfig.mergeFeatureVariables( + featureIdVariableMap, + variableIdMap, + featureId, + variation.variables, + variation.featureEnabled + ); + variationsMap[variation.key] = { + id: variation.id, + key: variation.key, + featureEnabled: variation.featureEnabled, + variablesMap: variablesMap, + }; + return variationsMap; + }, {}); + + return variationsMap; + } + + /** + * 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 } { var variablesIdMap: { [id: string]: FeatureVariable } = {}; variablesIdMap = (configObj.featureFlags || []).reduce((resultMap: { [id: string]: FeatureVariable }, feature) => { @@ -291,11 +287,19 @@ export class OptimizelyConfig { return variablesIdMap; } + /** + * Gets list of rollout experiments + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {string} featureId + * @param {Experiment[] | undefined} experiments + * @returns {OptimizelyExperiment[]} List of Optimizely rollout experiments + */ static getDeliveryRules( configObj: ProjectConfig, featureVariableIdMap: FeatureVariablesMap, featureId: string, - experiments: Experiment[] | undefined, + experiments: Experiment[] | undefined ): OptimizelyExperiment[] { var deliveryRules: OptimizelyExperiment[] = []; if (!experiments) { @@ -308,7 +312,7 @@ export class OptimizelyConfig { deliveryRules.push({ id: experiment.id, key: experiment.key, - audiences: OptimizelyConfig.getExperimentAudiences(experiment,configObj), + audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), variationsMap: OptimizelyConfig.GetVariationsMap( experiment.variations, featureVariableIdMap, @@ -320,10 +324,87 @@ export class OptimizelyConfig { return deliveryRules; } + /** + * Get Experiment Ids which are part of rollout + * @param {Rollout[]} rollouts + * @returns {[key: string]: boolean} Map of experiment Ids to boolean + */ + static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { + return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { + rollout.experiments.forEach((e) => { + experimentIds[e.id] = true; + }); + + 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 } { + var experimentsMap: { [id: string]: OptimizelyExperiment } = {}; + const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); + const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); + + const experiments = configObj.experiments; + + experiments.forEach((experiment) => { + if (!rolloutExperimentIds[experiment.id]) { + var featureIds = configObj.experimentFeatureMap[experiment.id]; + var 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 experiments mapped by their keys + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyExperimentsMap} Experiments mapped by key + */ + static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { + var experimentKeysMap: OptimizelyExperimentsMap = {}; + + for (let id in experimentsMapById) { + let experiment = experimentsMapById[id]; + experimentKeysMap[experiment.key] = experiment; + } + return experimentKeysMap; + } + + /** + * Gets Map of all FeatureFlags and associated experiment map inside it + * @param {ProjectConfig} configObj + * @param {FeatureVariablesMap} featureVariableIdMap + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyFeaturesMap} OptimizelyFeature mapped by key + */ static getFeaturesMap( configObj: ProjectConfig, featureVariableIdMap: FeatureVariablesMap, - experimentsMapById: OptimizelyExperimentsMap, + experimentsMapById: OptimizelyExperimentsMap ): OptimizelyFeaturesMap { var featuresMap: OptimizelyFeaturesMap = {}; configObj.featureFlags.forEach((featureFlag) => { @@ -347,14 +428,14 @@ export class OptimizelyConfig { }; return variables; }, {}); - var deliveryRules: OptimizelyExperiment[] = [] + var deliveryRules: OptimizelyExperiment[] = []; const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; if (rollout) { deliveryRules = OptimizelyConfig.getDeliveryRules( configObj, featureVariableIdMap, featureFlag.id, - rollout.experiments, + rollout.experiments ); } featuresMap[featureFlag.key] = { diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index 05da2935c..4feae1862 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -26,18 +26,18 @@ import configValidator from '../../utils/config_validator'; import { LogHandler } from '@optimizely/js-sdk-logging'; import { + Audience, Experiment, FeatureFlag, + FeatureVariable, Group, - Audience, + OptimizelyVariation, + OptimizelyAttribute, Rollout, TrafficAllocation, - FeatureVariable, Variation, - OptimizelyVariation, VariableType, VariationVariable, - OptimizelyAttribute } from '../../shared_types'; interface TryCreatingProjectConfigConfig { @@ -122,8 +122,8 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { return rolloutCopy; }); - datafileCopy.environmentKey = datafile.environmentKey ? datafile.environmentKey : "" - datafileCopy.sdkKey = datafile.sdkKey ? datafile.sdkKey : "" + datafileCopy.environmentKey = datafile.environmentKey ? datafile.environmentKey : ''; + datafileCopy.sdkKey = datafile.sdkKey ? datafile.sdkKey : ''; return datafileCopy; } diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 83f9befd7..471a87822 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -290,7 +290,7 @@ export interface SDKOptions { defaultDecideOptions?: OptimizelyDecideOption[]; } -export type OptimizelyExperimentsMap = { +export type OptimizelyExperimentsMap = { [experimentKey: string]: OptimizelyExperiment; } @@ -305,19 +305,19 @@ export type OptimizelyFeaturesMap = { export type OptimizelyAttribute = { id: string; key: string; -} +}; export type OptimizelyAudience = { id: string; name: string; conditions: unknown[] | string; -} +}; export type OptimizelyEvent = { id: string; key: string; experimentsIds: string[]; -} +}; export interface OptimizelyFeature { id: string; From 082e85b6081d080180d64a1a4c4760f7466a87a9 Mon Sep 17 00:00:00 2001 From: ozayr-zaviar Date: Tue, 10 Aug 2021 17:05:29 +0500 Subject: [PATCH 06/12] linting fixed and unit tests added --- .../lib/core/optimizely_config/index.tests.js | 752 +++++++++++++++++- .../lib/core/optimizely_config/index.ts | 56 +- .../optimizely-sdk/lib/tests/test_data.js | 342 ++++++++ 3 files changed, 1119 insertions(+), 31 deletions(-) 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..784d5a244 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 7eb36902e..5e72ae359 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -86,8 +86,8 @@ export class OptimizelyConfig { * @returns {OptimizelyAudience[]} Array of unique audiences */ static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { - var finalAudiences: OptimizelyAudience[] = []; - var typedAudienceIds: { [id: string]: boolean } = {}; + const finalAudiences: OptimizelyAudience[] = []; + const typedAudienceIds: { [id: string]: boolean } = {}; (configObj.typedAudiences || []).forEach((typedAudience) => { finalAudiences.push({ @@ -130,12 +130,12 @@ export class OptimizelyConfig { conditions: Array, audiencesById: { [id: string]: Audience } ): string { - var serializedAudience = ''; + let serializedAudience = ''; if (conditions) { - var cond = ''; + let cond = ''; conditions.forEach((item) => { - var subAudience = ''; + let subAudience = ''; // Checks if item is list of conditions means if it is sub audience if (item instanceof Array) { subAudience = OptimizelyConfig.GetSerializedAudiences(item, audiencesById); @@ -144,7 +144,7 @@ export class OptimizelyConfig { cond = item.toUpperCase(); } else { // Checks if item is audience id - var audienceName = audiencesById[item] ? audiencesById[item].name : item; + 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; @@ -204,20 +204,20 @@ export class OptimizelyConfig { featureVariableUsages: VariationVariable[] | undefined, isFeatureEnabled: boolean | undefined ): OptimizelyVariablesMap { - var variablesMap: OptimizelyVariablesMap = {}; + let variablesMap: OptimizelyVariablesMap = {}; if (!featureId) { return variablesMap; } variablesMap = (featureIdVariableMap[featureId] || []).reduce( - (variablesMap: OptimizelyVariablesMap, featureVariable) => { - variablesMap[featureVariable.key] = { + (OptlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { + OptlyVariablesMap[featureVariable.key] = { id: featureVariable.id, key: featureVariable.key, type: featureVariable.type, value: featureVariable.defaultValue, }; - return variablesMap; + return OptlyVariablesMap; }, {} ); @@ -249,8 +249,8 @@ export class OptimizelyConfig { variableIdMap: { [id: string]: FeatureVariable }, featureId: string ): { [key: string]: Variation } { - var variationsMap: { [key: string]: OptimizelyVariation } = {}; - variationsMap = variations.reduce((variationsMap: { [key: string]: OptimizelyVariation }, variation) => { + let variationsMap: { [key: string]: OptimizelyVariation } = {}; + variationsMap = variations.reduce((OptlyVariationsMap: { [key: string]: OptimizelyVariation }, variation) => { const variablesMap = OptimizelyConfig.mergeFeatureVariables( featureIdVariableMap, variableIdMap, @@ -258,13 +258,13 @@ export class OptimizelyConfig { variation.variables, variation.featureEnabled ); - variationsMap[variation.key] = { + OptlyVariationsMap[variation.key] = { id: variation.id, key: variation.key, featureEnabled: variation.featureEnabled, variablesMap: variablesMap, }; - return variationsMap; + return OptlyVariationsMap; }, {}); return variationsMap; @@ -276,7 +276,7 @@ export class OptimizelyConfig { * @returns {[id: string]: FeatureVariable }} FeatureVariables mapped by id */ static getVariableIdMap(configObj: ProjectConfig): { [id: string]: FeatureVariable } { - var variablesIdMap: { [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; @@ -301,7 +301,7 @@ export class OptimizelyConfig { featureId: string, experiments: Experiment[] | undefined ): OptimizelyExperiment[] { - var deliveryRules: OptimizelyExperiment[] = []; + const deliveryRules: OptimizelyExperiment[] = []; if (!experiments) { return deliveryRules; } @@ -349,7 +349,7 @@ export class OptimizelyConfig { configObj: ProjectConfig, featureIdVariableMap: FeatureVariablesMap ): { [id: string]: OptimizelyExperiment } { - var experimentsMap: { [id: string]: OptimizelyExperiment } = {}; + const experimentsMap: { [id: string]: OptimizelyExperiment } = {}; const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); @@ -357,8 +357,8 @@ export class OptimizelyConfig { experiments.forEach((experiment) => { if (!rolloutExperimentIds[experiment.id]) { - var featureIds = configObj.experimentFeatureMap[experiment.id]; - var featureId = ''; + const featureIds = configObj.experimentFeatureMap[experiment.id]; + let featureId = ''; if (featureIds && featureIds.length > 0) { featureId = featureIds[0]; } @@ -385,10 +385,10 @@ export class OptimizelyConfig { * @returns {OptimizelyExperimentsMap} Experiments mapped by key */ static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { - var experimentKeysMap: OptimizelyExperimentsMap = {}; + const experimentKeysMap: OptimizelyExperimentsMap = {}; - for (let id in experimentsMapById) { - let experiment = experimentsMapById[id]; + for (const id in experimentsMapById) { + const experiment = experimentsMapById[id]; experimentKeysMap[experiment.key] = experiment; } return experimentKeysMap; @@ -406,13 +406,13 @@ export class OptimizelyConfig { featureVariableIdMap: FeatureVariablesMap, experimentsMapById: OptimizelyExperimentsMap ): OptimizelyFeaturesMap { - var featuresMap: OptimizelyFeaturesMap = {}; + const featuresMap: OptimizelyFeaturesMap = {}; configObj.featureFlags.forEach((featureFlag) => { - var featureExperimentMap: OptimizelyExperimentsMap = {}; - var experimentRules: OptimizelyExperiment[] = []; - for (let key in experimentsMapById) { + const featureExperimentMap: OptimizelyExperimentsMap = {}; + const experimentRules: OptimizelyExperiment[] = []; + for (const key in experimentsMapById) { if (featureFlag.experimentIds.includes(key)) { - var experiment = experimentsMapById[key]; + const experiment = experimentsMapById[key]; if (experiment) { featureExperimentMap[experiment.key] = experiment; } @@ -428,7 +428,7 @@ export class OptimizelyConfig { }; return variables; }, {}); - var deliveryRules: OptimizelyExperiment[] = []; + let deliveryRules: OptimizelyExperiment[] = []; const rollout = configObj.rolloutIdMap[featureFlag.rolloutId]; if (rollout) { deliveryRules = OptimizelyConfig.getDeliveryRules( 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 }; From 6fcc79dd0d07a423d2e7986150c0a23f359c38ba Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Thu, 12 Aug 2021 15:35:23 +0500 Subject: [PATCH 07/12] Suggested changes made. --- .../core/condition_tree_evaluator/index.ts | 2 +- .../lib/core/optimizely_config/index.ts | 111 ++++++++---------- packages/optimizely-sdk/lib/shared_types.ts | 26 ++-- 3 files changed, 69 insertions(+), 70 deletions(-) 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..069caeae2 100644 --- a/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts +++ b/packages/optimizely-sdk/lib/core/condition_tree_evaluator/index.ts @@ -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.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 5e72ae359..8d58c4659 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -14,22 +14,23 @@ * limitations under the License. */ import { ProjectConfig } from '../project_config'; +import { DEFAULT_OPERATOR_TYPES } from '../condition_tree_evaluator'; import { Audience, Experiment, FeatureVariable, - OptimizelyExperimentsMap, - OptimizelyFeaturesMap, - OptimizelyVariablesMap, OptimizelyAttribute, OptimizelyAudience, OptimizelyEvent, OptimizelyExperiment, + OptimizelyExperimentsMap, + OptimizelyFeaturesMap, OptimizelyVariable, + OptimizelyVariablesMap, OptimizelyVariation, Rollout, - VariationVariable, Variation, + VariationVariable, } from '../../shared_types'; interface FeatureVariablesMap { @@ -51,11 +52,10 @@ export class OptimizelyConfig { public audiences: OptimizelyAudience[]; public events: OptimizelyEvent[]; private datafile: string; - private static audienceConditionalOperators: string[] = ['and', 'or', 'not']; constructor(configObj: ProjectConfig, datafile: string) { - this.sdkKey = configObj.sdkKey ? configObj.sdkKey : ''; - this.environmentKey = configObj.environmentKey ? configObj.environmentKey : ''; + this.sdkKey = configObj.sdkKey ?? ''; + this.environmentKey = configObj.environmentKey ?? ''; this.attributes = configObj.attributes; this.audiences = OptimizelyConfig.getAudiences(configObj); this.events = configObj.events; @@ -86,29 +86,29 @@ export class OptimizelyConfig { * @returns {OptimizelyAudience[]} Array of unique audiences */ static getAudiences(configObj: ProjectConfig): OptimizelyAudience[] { - const finalAudiences: OptimizelyAudience[] = []; - const typedAudienceIds: { [id: string]: boolean } = {}; + const audiences: OptimizelyAudience[] = []; + const typedAudienceIds: string[] = []; (configObj.typedAudiences || []).forEach((typedAudience) => { - finalAudiences.push({ + audiences.push({ id: typedAudience.id, conditions: JSON.stringify(typedAudience.conditions), name: typedAudience.name, }); - typedAudienceIds[typedAudience.id] = true; + typedAudienceIds.push(typedAudience.id); }); - (configObj.audiences || []).forEach((oldAudience) => { - if (!typedAudienceIds[oldAudience.id] && oldAudience.id != '$opt_dummy_audience') { - finalAudiences.push({ - id: oldAudience.id, - conditions: JSON.stringify(oldAudience.conditions), - name: oldAudience.name, + (configObj.audiences || []).forEach((audience) => { + if (!typedAudienceIds.includes(audience.id) && audience.id != '$opt_dummy_audience') { + audiences.push({ + id: audience.id, + conditions: JSON.stringify(audience.conditions), + name: audience.name, }); } }); - return finalAudiences; + return audiences; } /** @@ -126,7 +126,7 @@ export class OptimizelyConfig { * @param {[id: string]: Audience} audiencesById * @returns {string} Serialized audiences condition string */ - static GetSerializedAudiences( + static getSerializedAudiences( conditions: Array, audiencesById: { [id: string]: Audience } ): string { @@ -136,11 +136,11 @@ export class OptimizelyConfig { let cond = ''; conditions.forEach((item) => { let subAudience = ''; - // Checks if item is list of conditions means if it is sub audience + // Checks if item is list of conditions means it is sub audience if (item instanceof Array) { - subAudience = OptimizelyConfig.GetSerializedAudiences(item, audiencesById); + subAudience = OptimizelyConfig.getSerializedAudiences(item, audiencesById); subAudience = `(${subAudience})`; - } else if (OptimizelyConfig.audienceConditionalOperators.includes(item)) { + } else if (DEFAULT_OPERATOR_TYPES.includes(item)) { cond = item.toUpperCase(); } else { // Checks if item is audience id @@ -156,7 +156,7 @@ export class OptimizelyConfig { } else { serializedAudience = `"${audienceName}"`; } - } + } // Checks if sub audience is empty or not if (subAudience !== '') { if (serializedAudience !== '' || cond === 'NOT') { @@ -185,7 +185,7 @@ export class OptimizelyConfig { if (!experiment.audienceConditions) { return ''; } - return OptimizelyConfig.GetSerializedAudiences(experiment.audienceConditions, configObj.audiencesById); + return OptimizelyConfig.getSerializedAudiences(experiment.audienceConditions, configObj.audiencesById); } /** @@ -204,14 +204,9 @@ export class OptimizelyConfig { featureVariableUsages: VariationVariable[] | undefined, isFeatureEnabled: boolean | undefined ): OptimizelyVariablesMap { - let variablesMap: OptimizelyVariablesMap = {}; - if (!featureId) { - return variablesMap; - } - - variablesMap = (featureIdVariableMap[featureId] || []).reduce( + const variablesMap = (featureIdVariableMap[featureId] || []).reduce( (OptlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { - OptlyVariablesMap[featureVariable.key] = { + OptlyVariablesMap[featureVariable.key] = { id: featureVariable.id, key: featureVariable.key, type: featureVariable.type, @@ -243,7 +238,7 @@ export class OptimizelyConfig { * @param {string} featureId * @returns {[key: string]: Variation} Variations mapped by key */ - static GetVariationsMap( + static getVariationsMap( variations: Variation[], featureIdVariableMap: FeatureVariablesMap, variableIdMap: { [id: string]: FeatureVariable }, @@ -273,7 +268,7 @@ export class OptimizelyConfig { /** * Gets Map of FeatureVariable with respect to featureVariableId * @param {ProjectConfig} configObj - * @returns {[id: string]: FeatureVariable }} FeatureVariables mapped by id + * @returns {[id: string]: FeatureVariable} FeatureVariables mapped by id */ static getVariableIdMap(configObj: ProjectConfig): { [id: string]: FeatureVariable } { let variablesIdMap: { [id: string]: FeatureVariable } = {}; @@ -292,51 +287,44 @@ export class OptimizelyConfig { * @param {ProjectConfig} configObj * @param {FeatureVariablesMap} featureVariableIdMap * @param {string} featureId - * @param {Experiment[] | undefined} experiments + * @param {Experiment[]} experiments * @returns {OptimizelyExperiment[]} List of Optimizely rollout experiments */ static getDeliveryRules( configObj: ProjectConfig, featureVariableIdMap: FeatureVariablesMap, featureId: string, - experiments: Experiment[] | undefined + experiments: Experiment[] ): OptimizelyExperiment[] { - const deliveryRules: OptimizelyExperiment[] = []; - if (!experiments) { - return deliveryRules; - } - const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); - - experiments.forEach((experiment) => { - deliveryRules.push({ + return experiments.map((experiment) => { + return { id: experiment.id, key: experiment.key, audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), - variationsMap: OptimizelyConfig.GetVariationsMap( + variationsMap: OptimizelyConfig.getVariationsMap( experiment.variations, featureVariableIdMap, variableIdMap, featureId ), - }); + }; }); - return deliveryRules; } /** * Get Experiment Ids which are part of rollout - * @param {Rollout[]} rollouts - * @returns {[key: string]: boolean} Map of experiment Ids to boolean + * @param {Rollout[]} rollouts + * @returns {string[]} Array of experiment Ids */ - static getRolloutExperimentIds(rollouts: Rollout[]): { [key: string]: boolean } { - return (rollouts || []).reduce((experimentIds: { [key: string]: boolean }, rollout) => { + static getRolloutExperimentIds(rollouts: Rollout[]): string[] { + const experimentIds: string[] = []; + (rollouts || []).forEach((rollout) => { rollout.experiments.forEach((e) => { - experimentIds[e.id] = true; + experimentIds.push(e.id); }); - - return experimentIds; - }, {}); + }); + return experimentIds; } /** @@ -349,20 +337,19 @@ export class OptimizelyConfig { configObj: ProjectConfig, featureIdVariableMap: FeatureVariablesMap ): { [id: string]: OptimizelyExperiment } { - const experimentsMap: { [id: string]: OptimizelyExperiment } = {}; const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); const experiments = configObj.experiments; - experiments.forEach((experiment) => { - if (!rolloutExperimentIds[experiment.id]) { + return (experiments || []).reduce((experimentsMap: { [id: string]: OptimizelyExperiment }, experiment) => { + if (!rolloutExperimentIds.includes(experiment.id)) { const featureIds = configObj.experimentFeatureMap[experiment.id]; let featureId = ''; if (featureIds && featureIds.length > 0) { featureId = featureIds[0]; } - const variationsMap = OptimizelyConfig.GetVariationsMap( + const variationsMap = OptimizelyConfig.getVariationsMap( experiment.variations, featureIdVariableMap, variableIdMap, @@ -375,14 +362,14 @@ export class OptimizelyConfig { variationsMap: variationsMap, }; } - }); - return experimentsMap; + return experimentsMap; + }, {}); } /** * Get experiments mapped by their keys - * @param {OptimizelyExperimentsMap} experimentsMapById - * @returns {OptimizelyExperimentsMap} Experiments mapped by key + * @param {OptimizelyExperimentsMap} experimentsMapById + * @returns {OptimizelyExperimentsMap} Experiments mapped by key */ static getExperimentsKeyMap(experimentsMapById: OptimizelyExperimentsMap): OptimizelyExperimentsMap { const experimentKeysMap: OptimizelyExperimentsMap = {}; diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 471a87822..12921ac8e 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -249,7 +249,7 @@ export interface OptimizelyOptions { export interface OptimizelyExperiment { id: string; key: string; - audiences: unknown[] | string; + audiences: string; variationsMap: { [variationKey: string]: OptimizelyVariation; }; @@ -310,7 +310,7 @@ export type OptimizelyAttribute = { export type OptimizelyAudience = { id: string; name: string; - conditions: unknown[] | string; + conditions: string; }; export type OptimizelyEvent = { @@ -322,10 +322,14 @@ export type OptimizelyEvent = { export interface OptimizelyFeature { id: string; key: string; - experimentsMap: OptimizelyExperimentsMap; - variablesMap: OptimizelyVariablesMap; experimentRules: OptimizelyExperiment[]; deliveryRules: OptimizelyExperiment[]; + variablesMap: OptimizelyVariablesMap; + + /** + * @deprecated Use experimentRules and deliveryRules + */ + experimentsMap: OptimizelyExperimentsMap; } export interface OptimizelyVariation { @@ -336,14 +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; attributes: OptimizelyAttribute[]; audiences: OptimizelyAudience[]; events: OptimizelyEvent[]; - revision: string; - sdkKey: string; - environmentKey: string; getDatafile(): string; } From 16cc8952e860cac07f19787d221ab884c43e7ed5 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Thu, 12 Aug 2021 15:37:10 +0500 Subject: [PATCH 08/12] nits fixed. --- packages/optimizely-sdk/lib/core/project_config/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index 4feae1862..b7e497449 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -122,8 +122,8 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { return rolloutCopy; }); - datafileCopy.environmentKey = datafile.environmentKey ? datafile.environmentKey : ''; - datafileCopy.sdkKey = datafile.sdkKey ? datafile.sdkKey : ''; + datafileCopy.environmentKey = datafile.environmentKey ?? ''; + datafileCopy.sdkKey = datafile.sdkKey ?? ''; return datafileCopy; } From 5088dbda6ce23ecc1a4604dd5c0c0e355d575950 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Thu, 12 Aug 2021 17:31:03 +0500 Subject: [PATCH 09/12] nit fixed. --- .../optimizely-sdk/lib/core/optimizely_config/index.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 784d5a244..25ce515fe 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js @@ -856,7 +856,7 @@ describe('lib/core/optimizely_config', function() { ]; for (let testNo = 0; testNo < audienceConditions.length; testNo++) { - const serializedAudiences = OptimizelyConfig.GetSerializedAudiences(audienceConditions[testNo], audiencesById); + const serializedAudiences = OptimizelyConfig.getSerializedAudiences(audienceConditions[testNo], audiencesById); assert.equal(serializedAudiences, expectedAudienceOutputs[testNo]); } }); From b05d0496bbb1f85d4cd04e2b95a5a1af32d25a99 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Fri, 13 Aug 2021 10:11:11 +0500 Subject: [PATCH 10/12] suggested changes made. --- .../lib/core/condition_tree_evaluator/index.ts | 2 +- .../lib/core/optimizely_config/index.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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 069caeae2..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. * diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 8d58c4659..2c4715f41 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -205,14 +205,14 @@ export class OptimizelyConfig { isFeatureEnabled: boolean | undefined ): OptimizelyVariablesMap { const variablesMap = (featureIdVariableMap[featureId] || []).reduce( - (OptlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { - OptlyVariablesMap[featureVariable.key] = { + (optlyVariablesMap: OptimizelyVariablesMap, featureVariable) => { + optlyVariablesMap[featureVariable.key] = { id: featureVariable.id, key: featureVariable.key, type: featureVariable.type, value: featureVariable.defaultValue, }; - return OptlyVariablesMap; + return optlyVariablesMap; }, {} ); @@ -245,7 +245,7 @@ export class OptimizelyConfig { featureId: string ): { [key: string]: Variation } { let variationsMap: { [key: string]: OptimizelyVariation } = {}; - variationsMap = variations.reduce((OptlyVariationsMap: { [key: string]: OptimizelyVariation }, variation) => { + variationsMap = variations.reduce((optlyVariationsMap: { [key: string]: OptimizelyVariation }, variation) => { const variablesMap = OptimizelyConfig.mergeFeatureVariables( featureIdVariableMap, variableIdMap, @@ -253,13 +253,13 @@ export class OptimizelyConfig { variation.variables, variation.featureEnabled ); - OptlyVariationsMap[variation.key] = { + optlyVariationsMap[variation.key] = { id: variation.id, key: variation.key, featureEnabled: variation.featureEnabled, variablesMap: variablesMap, }; - return OptlyVariationsMap; + return optlyVariationsMap; }, {}); return variationsMap; From 76603f2dfc93175e77e07fb00c450ce8960f2128 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Fri, 13 Aug 2021 23:47:25 +0500 Subject: [PATCH 11/12] suggested changes made. --- .../lib/core/optimizely_config/index.ts | 22 +++++++++++++------ .../lib/core/project_config/index.ts | 5 ++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 2c4715f41..9b81500a7 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -43,11 +43,19 @@ 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[]; @@ -99,7 +107,7 @@ export class OptimizelyConfig { }); (configObj.audiences || []).forEach((audience) => { - if (!typedAudienceIds.includes(audience.id) && audience.id != '$opt_dummy_audience') { + if (typedAudienceIds.indexOf(audience.id) === -1 && audience.id != '$opt_dummy_audience') { audiences.push({ id: audience.id, conditions: JSON.stringify(audience.conditions), @@ -140,7 +148,7 @@ export class OptimizelyConfig { if (item instanceof Array) { subAudience = OptimizelyConfig.getSerializedAudiences(item, audiencesById); subAudience = `(${subAudience})`; - } else if (DEFAULT_OPERATOR_TYPES.includes(item)) { + } else if (DEFAULT_OPERATOR_TYPES.indexOf(item) > -1) { cond = item.toUpperCase(); } else { // Checks if item is audience id @@ -343,7 +351,7 @@ export class OptimizelyConfig { const experiments = configObj.experiments; return (experiments || []).reduce((experimentsMap: { [id: string]: OptimizelyExperiment }, experiment) => { - if (!rolloutExperimentIds.includes(experiment.id)) { + if (rolloutExperimentIds.indexOf(experiment.id) === -1) { const featureIds = configObj.experimentFeatureMap[experiment.id]; let featureId = ''; if (featureIds && featureIds.length > 0) { @@ -398,7 +406,7 @@ export class OptimizelyConfig { const featureExperimentMap: OptimizelyExperimentsMap = {}; const experimentRules: OptimizelyExperiment[] = []; for (const key in experimentsMapById) { - if (featureFlag.experimentIds.includes(key)) { + if (featureFlag.experimentIds.indexOf(key) > -1) { const experiment = experimentsMapById[key]; if (experiment) { featureExperimentMap[experiment.key] = experiment; diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index b7e497449..99cb0e6d8 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -32,7 +32,6 @@ import { FeatureVariable, Group, OptimizelyVariation, - OptimizelyAttribute, Rollout, TrafficAllocation, Variation, @@ -66,7 +65,7 @@ export interface ProjectConfig { sendFlagDecisions?: boolean; experimentKeyMap: { [key: string]: Experiment }; featureKeyMap: { - [key: string]: FeatureFlag + [key: string]: FeatureFlag; }; rollouts: Rollout[]; featureFlags: FeatureFlag[]; @@ -83,7 +82,7 @@ export interface ProjectConfig { groupIdMap: { [id: string]: Group }; groups: Group[]; events: Event[]; - attributes: OptimizelyAttribute[]; + attributes: Array<{ id: string; key: string }>; typedAudiences: Audience[]; rolloutIdMap: { [id: string]: Rollout }; anonymizeIP?: boolean | null; From 47706af6f7091c3c4d84a07c3a712f98ca572901 Mon Sep 17 00:00:00 2001 From: Sohail Hussain Date: Fri, 13 Aug 2021 15:21:23 -0700 Subject: [PATCH 12/12] added period --- packages/optimizely-sdk/lib/core/optimizely_config/index.ts | 2 +- packages/optimizely-sdk/lib/shared_types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 9b81500a7..ebed3da8f 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -48,7 +48,7 @@ export class OptimizelyConfig { public revision: string; /** - * This experimentsMap is for experiments of legacy projects only + * 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. diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 12921ac8e..f69abba55 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -345,7 +345,7 @@ export interface OptimizelyConfig { revision: string; /** - * This experimentsMap is for experiments of legacy projects only + * 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.