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..82d77692f 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.tests.js @@ -145,5 +145,13 @@ describe('lib/core/optimizely_config', function() { it('should return correct config environmentKey ', function() { assert.equal(optimizelyConfigObject.environmentKey, datafile.environmentKey); }); + + it('should return correct config attributes', function() { + assert.deepEqual(datafile.attributes, optimizelyConfigObject.attributes); + }); + + it('should return correct config events', function() { + assert.deepEqual(datafile.events, optimizelyConfigObject.events); + }); }); }); diff --git a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts index 788339d3a..d1a237623 100644 --- a/packages/optimizely-sdk/lib/core/optimizely_config/index.ts +++ b/packages/optimizely-sdk/lib/core/optimizely_config/index.ts @@ -22,6 +22,9 @@ import { VariationVariable, Variation, Rollout, + OptimizelyAttribute, + OptimizelyAudience, + OptimizelyEvents } from '../../shared_types'; interface FeatureVariablesMap { @@ -38,15 +41,51 @@ export class OptimizelyConfig { public featuresMap: OptimizelyFeaturesMap; public revision: string; public sdkKey?: string; + public attributes: OptimizelyAttribute[]; + public audiences: OptimizelyAudience[]; + public events: OptimizelyEvents[]; public environmentKey?: string; private datafile: string; constructor(configObj: ProjectConfig, datafile: string) { - this.experimentsMap = OptimizelyConfig.getExperimentsMap(configObj); - this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, this.experimentsMap); this.revision = configObj.revision; + this.attributes = configObj.attributes; + this.audiences = []; + this.events = configObj.events; this.datafile = datafile; + const audiences = configObj.typedAudiences || []; + configObj.audiences.forEach((oldAudience) => { + if ( + audiences.filter((newAudience) => { + newAudience == oldAudience; + }).length == 0 + ) { + if (oldAudience.id != '$opt_dummy_audience') { + audiences.push(oldAudience); + } + } + }); + + this.audiences = audiences; + const audienceMap: { [key: string]: string } = {}; + + for (const audience of this.audiences) { + audienceMap[audience.id] = audience.name; + } + const updatedExperiments = OptimizelyConfig.getExperimentsMap(configObj); + + Object.keys(updatedExperiments).map(function (key) { + const audiencesSerialized = serializeAudiences(configObj, key, audienceMap); + if (audiencesSerialized) { + updatedExperiments[key].audiences = audiencesSerialized; + } + }); + const updatedRollouts = OptimizelyConfig.updateRollouts(configObj, audienceMap); + + this.experimentsMap = updatedExperiments; + this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, updatedExperiments, updatedRollouts); + console.log("FEATURES MAP:", this.featuresMap) if (configObj.sdkKey && configObj.environmentKey) { this.sdkKey = configObj.sdkKey; this.environmentKey = configObj.environmentKey; @@ -69,13 +108,31 @@ 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; }, {}); } + /** + * Update rollouts by adding audiences keys in experiments + * @param {ProjectConfig} configObj + * @returns {audienceMap} Map of audiences + */ + static updateRollouts(configObj: ProjectConfig, audienceMap: { [key: string]: string }): Rollout[] { + return configObj.rollouts.map((rollout) => { + rollout.experiments = rollout.experiments.map((experiment) => { + const audiences = serializeAudiences(configObj, experiment.key, audienceMap); + if (audiences) { + experiment.audiences = audiences; + } + return experiment; + }); + return rollout; + }); + } + /** * Get Map of all experiments except rollouts * @param {ProjectConfig} configObj @@ -83,43 +140,35 @@ export class OptimizelyConfig { */ 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; - }, - {}, - ), - }; - } + 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 experiments; - }, - {}, - ) + return variations; + }, {}), + audiences: experiment.audiences, + }; + } + + return experiments; + }, {}); } /** @@ -134,7 +183,7 @@ export class OptimizelyConfig { configObj: ProjectConfig, variation: Variation, experimentId: string, - featureVariablesMap: FeatureVariablesMap, + featureVariablesMap: FeatureVariablesMap ): OptimizelyVariablesMap { const featureId = configObj.experimentFeatureMap[experimentId]; @@ -151,7 +200,7 @@ export class OptimizelyConfig { return variablesMap; }, - {}, + {} ); variablesObject = (experimentFeatureVariables || []).reduce( (variablesMap: OptimizelyVariablesMap, featureVariable) => { @@ -167,7 +216,7 @@ export class OptimizelyConfig { return variablesMap; }, - {}, + {} ); } @@ -182,33 +231,44 @@ export class OptimizelyConfig { */ static getFeaturesMap( configObj: ProjectConfig, - allExperiments: OptimizelyExperimentsMap + allExperiments: OptimizelyExperimentsMap, + rollouts: Rollout[] ): OptimizelyFeaturesMap { return (configObj.featureFlags || []).reduce((features: OptimizelyFeaturesMap, feature) => { + const filteredRollout = rollouts.filter((rollout) => { + console.log("Rollout:", rollout.id, "Feature:", feature.id) + return rollout.id == feature.id; + })[0]; + console.log("FILTERED ROLLOUTS ", filteredRollout) 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, - }; + 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 variables; + }, {}), + deliveryRules: Object.values(allExperiments), + experimentRules: filteredRollout + ? filteredRollout.experiments.map((experiment) => { + return { + id: experiment.id, + key: experiment.key, + audiences: experiment.audiences, + variationsMap: experiment.variationKeyMap, + }; + }) + : [], }; return features; @@ -216,6 +276,90 @@ export class OptimizelyConfig { } } +/** + * Serialize audienceConditions + * @param {Array} condition + * @returns {string} serialized audience condition + */ +function serialized(condition: Array) { + const operator = condition[0]; + let first = ''; + let second = ''; + if (condition[1]) { + first = Array.isArray(condition[1]) ? `(${serialized(condition[1])})` : `AUDIENCE(${condition[1]})`; + } + if (condition[2]) { + second = Array.isArray(condition[2]) ? `(${serialized(condition[2])})` : `AUDIENCE(${condition[2]})`; + } + if (condition[1] && condition[2]) { + return `${first} ${operator.toString().toUpperCase()} ${second}`; + } else { + return `${operator.toString().toUpperCase()} ${first}`; + } +} + +/** + * replace audience ids with name + * @param {string} condition + * @param {{[key: string]: string}} audiences + * @returns {string} Updated serialized audienceCondition + */ +function replaceAudienceIdsWithNames(condition: string, audiences: {[key: string]: string}) { + const beginWord = "AUDIENCE("; + const endWord = ")"; + let keyIdx = 0; + let audienceId = ""; + let collect = false; + + let replaced = ""; + for (const ch of condition) { + if (collect) { + if (ch == endWord) { + replaced += `"${audiences[audienceId] || audienceId}"`; + collect = false; + audienceId = ""; + } + else { + audienceId += ch; + } + continue; + } + + if (ch == beginWord[keyIdx]) { + keyIdx += 1; + if (keyIdx == beginWord.length) { + keyIdx = 0; + collect = true; + } + continue; + } + else { + if (keyIdx > 0) { + replaced += beginWord.substring(0, keyIdx); + } + keyIdx = 0; + } + + replaced += ch; + } + + return replaced; +} + +/** + * Return serialized audienceCondtion with replaced audienceIds with names + * @param {Array} condition + * @returns {string} serialized audience condition + */ +function serializeAudiences(configObj: ProjectConfig, experimentKey: string, audienceMap: { [key: string]: string }) { + const experiment = configObj.experimentKeyMap[experimentKey]; + if (experiment.audienceConditions) { + const condition = serialized(experiment.audienceConditions); + return replaceAudienceIdsWithNames(condition, audienceMap); + } + return ''; +} + /** * Create an instance of OptimizelyConfig * @param {ProjectConfig} configObj diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index a5c093b94..e53312685 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -49,6 +49,7 @@ interface TryCreatingProjectConfigConfig { interface Event { key: string; id: string; + experimentsIds: string; } interface VariableUsageMap { @@ -80,7 +81,7 @@ export interface ProjectConfig { groupIdMap: { [id: string]: Group }; groups: Group[]; events: Event[]; - attributes: Array<{ id: string }>; + attributes: Array<{ id: string, key:string, name:string }>; typedAudiences: Audience[]; rolloutIdMap: { [id: string]: Rollout }; anonymizeIP?: boolean | null; diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index f91cac9ab..886588aa0 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -121,6 +121,7 @@ export interface Experiment { audienceIds: string[]; trafficAllocation: TrafficAllocation[]; forcedVariations?: { [key: string]: string }; + audiences: unknown[] | string; } export interface FeatureVariable { @@ -149,6 +150,7 @@ export type Condition = { } export interface Audience { + id: string; name: string; conditions: unknown[] | string; } @@ -234,6 +236,7 @@ export interface OptimizelyOptions { export interface OptimizelyExperiment { id: string; key: string; + audiences: unknown[] | string; variationsMap: { [variationKey: string]: OptimizelyVariation; }; @@ -286,11 +289,30 @@ export type OptimizelyFeaturesMap = { [featureKey: string]: OptimizelyFeature; } +export type OptimizelyAttribute = { + name: string; + key: string; +} + +export type OptimizelyAudience = { + id: string; + name: string; + conditions: unknown[] | string; +} + +export type OptimizelyEvents = { + id: string; + key: string; + experimentsIds: string; +} + export interface OptimizelyFeature { id: string; key: string; experimentsMap: OptimizelyExperimentsMap; variablesMap: OptimizelyVariablesMap; + experimentRules: OptimizelyExperiment[]; + deliveryRules: OptimizelyExperiment[]; } export interface OptimizelyVariation { @@ -303,6 +325,9 @@ export interface OptimizelyVariation { export interface OptimizelyConfig { experimentsMap: OptimizelyExperimentsMap; featuresMap: OptimizelyFeaturesMap; + attributes: OptimizelyAttribute[]; + audiences: OptimizelyAudience[]; + events: [OptimizelyEvents]; revision: string; sdkKey?: string; environmentKey?: string;