Skip to content

Commit 986f6d5

Browse files
zashraf1985jaeopt
authored andcommitted
feat: getOptimizelyConfig API to get static experiments and features data (#362)
* first version of optimizely config * changed arrow functions to normal functions to make them work with old browsers * added unit tests * Changed all map keys to use key instead of id. Also added featureEnabled boolean to variation
1 parent 2070b6e commit 986f6d5

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Get Experiment Ids which are part of rollouts
18+
function getRolloutExperimentIds(rollouts) {
19+
return rollouts.reduce(function(experimentIds, rollout) {
20+
rollout.experiments.forEach(function(e) {
21+
experimentIds[e.id] = true;
22+
});
23+
return experimentIds;
24+
}, {});
25+
}
26+
27+
// Gets Map of all experiments except rollouts
28+
function getExperimentsMap(configObj) {
29+
var rolloutExperimentIds = getRolloutExperimentIds(configObj.rollouts);
30+
var featureVariablesMap = configObj.featureFlags.reduce(function(resultMap, feature){
31+
resultMap[feature.id] = feature.variables;
32+
return resultMap;
33+
}, {});
34+
return configObj.experiments.reduce(function(experiments, experiment) {
35+
// skip experiments that are part of a rollout
36+
if (!rolloutExperimentIds[experiment.id]) {
37+
experiments[experiment.key] = {
38+
id: experiment.id,
39+
key: experiment.key,
40+
variationsMap: experiment.variations.reduce(function(variations, variation) {
41+
var variablesMap = {};
42+
if (variation.featureEnabled) {
43+
variablesMap = variation.variables.reduce(function(variables, variable) {
44+
// developing a temporary map using variable ids. the entry with ids will be deleted after merging with featurevaribles
45+
variables[variable.id] = {
46+
id: variable.id,
47+
value: variable.value,
48+
};
49+
return variables;
50+
}, {});
51+
}
52+
variations[variation.key] = {
53+
id: variation.id,
54+
key: variation.key,
55+
featureEnabled: variation.featureEnabled,
56+
variablesMap,
57+
};
58+
return variations;
59+
}, {}),
60+
};
61+
var featureId = configObj.experimentFeatureMap[experiment.id];
62+
if (featureId) {
63+
mergeFeatureVariables(experiments[experiment.key], featureVariablesMap[featureId]);
64+
}
65+
}
66+
return experiments;
67+
}, {});
68+
}
69+
70+
// Merges feature varibles in variations of passed in experiment
71+
// Modifies experiment object.
72+
function mergeFeatureVariables(experiment, featureVariables) {
73+
var variationKeys = Object.keys(experiment.variationsMap);
74+
variationKeys.forEach(function(variationKey) {
75+
var variation = experiment.variationsMap[variationKey];
76+
featureVariables.forEach(function(featureVariable) {
77+
var variationVariable = variation.variablesMap[featureVariable.id];
78+
var variableValue = variationVariable ? variationVariable.value : featureVariable.defaultValue;
79+
variation.variablesMap[featureVariable.key] = {
80+
id: featureVariable.id,
81+
key: featureVariable.key,
82+
type: featureVariable.type,
83+
value: variableValue,
84+
};
85+
// deleting the temporary entry
86+
variationVariable && delete variation.variablesMap[featureVariable.id];
87+
})
88+
});
89+
};
90+
91+
// Gets map of all experiments
92+
function getFeaturesMap(configObj, allExperiments) {
93+
return configObj.featureFlags.reduce(function(features, feature) {
94+
features[feature.key] = {
95+
id: feature.id,
96+
key: feature.key,
97+
experimentsMap: feature.experimentIds.reduce(function(experiments, experimentId) {
98+
var experimentKey = configObj.experimentIdMap[experimentId].key;
99+
experiments[experimentKey] = allExperiments[experimentKey];
100+
return experiments;
101+
}, {}),
102+
variablesMap: feature.variables.reduce(function(variables, variable) {
103+
variables[variable.key] = {
104+
id: variable.id,
105+
key: variable.key,
106+
type: variable.type,
107+
value: variable.defaultValue,
108+
}
109+
return variables;
110+
}, {}),
111+
};
112+
return features;
113+
}, {});
114+
}
115+
116+
module.exports = {
117+
getOptimizelyConfig: function(configObj) {
118+
// Fetch all feature variables from feature flags to merge them with variation variables
119+
var experimentsMap = getExperimentsMap(configObj);
120+
return {
121+
experimentsMap,
122+
featuresMap: getFeaturesMap(configObj, experimentsMap),
123+
revision: configObj.revision,
124+
}
125+
},
126+
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
var assert = require('chai').assert;
17+
18+
var datafile = require('../../tests/test_data').getTestProjectConfigWithFeatures();
19+
var projectConfig = require('../project_config');
20+
var optimizelyConfig = require('./index');
21+
22+
var getAllExperimentsFromDatafile = function(datafile) {
23+
var allExperiments = [];
24+
datafile.groups.forEach(function(group) {
25+
group.experiments.forEach(function(experiment) {
26+
allExperiments.push(experiment);
27+
});
28+
});
29+
datafile.experiments.forEach(function(experiment) {
30+
allExperiments.push(experiment);
31+
});
32+
return allExperiments;
33+
};
34+
35+
describe('lib/core/optimizely_config', function() {
36+
describe('Optimizely Config', function() {
37+
var optimizelyConfigObject;
38+
var projectConfigObject;
39+
beforeEach(function() {
40+
projectConfigObject = projectConfig.createProjectConfig(datafile);
41+
optimizelyConfigObject = optimizelyConfig.getOptimizelyConfig(projectConfigObject);
42+
});
43+
44+
it('should return all experiments except rollouts', function() {
45+
var experimentsMap = optimizelyConfigObject.experimentsMap;
46+
var experimentsCount = Object.keys(optimizelyConfigObject.experimentsMap).length;
47+
assert.equal(experimentsCount, 6);
48+
49+
var allExperiments = getAllExperimentsFromDatafile(datafile);
50+
allExperiments.forEach(function(experiment) {
51+
assert.include(experimentsMap[experiment.key], {
52+
id: experiment.id,
53+
key: experiment.key,
54+
});
55+
var variationsMap = experimentsMap[experiment.key].variationsMap;
56+
experiment.variations.forEach(function(variation) {
57+
assert.include(variationsMap[variation.key], {
58+
id: variation.id,
59+
key: variation.key,
60+
})
61+
});
62+
});
63+
});
64+
65+
it('should return all the feature flags', function() {
66+
var featureFlagsCount = Object.keys(optimizelyConfigObject.featuresMap).length;
67+
assert.equal(featureFlagsCount, 7);
68+
69+
var featuresMap = optimizelyConfigObject.featuresMap;
70+
datafile.featureFlags.forEach(function(featureFlag) {
71+
assert.include(featuresMap[featureFlag.key], {
72+
id: featureFlag.id,
73+
key: featureFlag.key,
74+
});
75+
featureFlag.experimentIds.forEach(function(experimentId) {
76+
var experimentKey = projectConfigObject.experimentIdMap[experimentId].key;
77+
assert.isTrue(!!featuresMap[featureFlag.key].experimentsMap[experimentKey]);
78+
});
79+
var variablesMap = featuresMap[featureFlag.key].variablesMap;
80+
featureFlag.variables.forEach(function(variable) {
81+
assert.include(variablesMap[variable.key], {
82+
id: variable.id,
83+
key: variable.key,
84+
type: variable.type,
85+
value: variable.defaultValue,
86+
})
87+
});
88+
});
89+
});
90+
91+
it('should correctly merge all feature variables', function() {
92+
var featureFlags = datafile.featureFlags;
93+
var datafileExperimentsMap = getAllExperimentsFromDatafile(datafile)
94+
.reduce(function(experiments, experiment) {
95+
experiments[experiment.key] = experiment;
96+
return experiments;
97+
}, {});
98+
featureFlags.forEach(function(featureFlag) {
99+
var experimentIds = featureFlag.experimentIds;
100+
experimentIds.forEach(function(experimentId) {
101+
var experimentKey = projectConfigObject.experimentIdMap[experimentId].key;
102+
var experiment = optimizelyConfigObject.experimentsMap[experimentKey];
103+
var variations = datafileExperimentsMap[experimentKey].variations;
104+
var variationsMap = experiment.variationsMap;
105+
variations.forEach(function(variation) {
106+
featureFlag.variables.forEach(function(variable) {
107+
var variableToAssert = variationsMap[variation.key].variablesMap[variable.key];
108+
assert.include(variable, {
109+
id: variableToAssert.id,
110+
key: variableToAssert.key,
111+
type: variableToAssert.type,
112+
});
113+
if (!variation.featureEnabled) {
114+
assert.equal(variable.defaultValue, variableToAssert.value);
115+
}
116+
});
117+
})
118+
});
119+
});
120+
});
121+
122+
it('should return correct config revision', function() {
123+
assert.equal(optimizelyConfigObject.revision, datafile.revision);
124+
});
125+
});
126+
});

packages/optimizely-sdk/lib/optimizely/index.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var eventProcessor = require('@optimizely/js-sdk-event-processor');
2424
var eventTagsValidator = require('../utils/event_tags_validator');
2525
var notificationCenter = require('../core/notification_center');
2626
var projectConfig = require('../core/project_config');
27+
var optimizelyConfig = require('../core/optimizely_config');
2728
var sprintf = require('@optimizely/js-sdk-utils').sprintf;
2829
var userProfileServiceValidator = require('../utils/user_profile_service_validator');
2930
var stringValidator = require('../utils/string_value_validator');
@@ -877,6 +878,56 @@ Optimizely.prototype.getFeatureVariableString = function(featureKey, variableKey
877878
}
878879
};
879880

881+
/**
882+
* Returns OptimizelyConfig object containing experiments and features data
883+
* @return {Object}
884+
*
885+
* OptimizelyConfig Object Schema
886+
* {
887+
* 'experimentsMap': {
888+
* '111111': {
889+
* 'id': '111111',
890+
* 'key': 'my-fist-experiment'
891+
* 'variationsMap': {
892+
* '121212': {
893+
* 'id': '121212',
894+
* 'key': 'variation_1',
895+
* 'variablesMap': {
896+
* '222222': {
897+
* 'id': '222222',
898+
* 'key': 'age',
899+
* 'type': 'integer',
900+
* 'value': '0',
901+
* }
902+
* }
903+
* }
904+
* }
905+
* }
906+
* },
907+
* 'featuresMap': {
908+
* '333333': {
909+
* 'id': '333333',
910+
* 'key': 'awesome-feature',
911+
* 'experimentsMap': Object,
912+
* 'variationsMap': Object,
913+
* }
914+
* }
915+
* }
916+
*/
917+
Optimizely.prototype.getOptimizelyConfig = function() {
918+
try {
919+
var configObj = this.projectConfigManager.getConfig();
920+
if (!configObj) {
921+
return null;
922+
}
923+
return optimizelyConfig.getOptimizelyConfig(configObj);
924+
} catch (e) {
925+
this.logger.log(LOG_LEVEL.ERROR, e.message);
926+
this.errorHandler.handleError(e);
927+
return null;
928+
}
929+
}
930+
880931
/**
881932
* Stop background processes belonging to this instance, including:
882933
*

0 commit comments

Comments
 (0)