Skip to content

Commit 599b694

Browse files
authored
feat: Implement decideAll api (#635)
* Implement decideForKeys * Implememt decideAll and add comments * Add decideAll and decideForKeys unit tests to optimizely * Add unit tests for user context decideForKey and decideAll * Clean up * Fix decideAll * Add more ENABLED_FLAGS_ONLY tests * update test params * Update comments * Incorporate comments * Update comments
1 parent 132587e commit 599b694

File tree

4 files changed

+481
-17
lines changed

4 files changed

+481
-17
lines changed

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

Lines changed: 290 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4333,6 +4333,7 @@ describe('lib/optimizely', function() {
43334333
logLevel: LOG_LEVEL.INFO,
43344334
logToConsole: false,
43354335
});
4336+
43364337
describe('#createUserContext', function() {
43374338
beforeEach(function() {
43384339
optlyInstance = new Optimizely({
@@ -4911,16 +4912,10 @@ describe('lib/optimizely', function() {
49114912
});
49124913

49134914
sinon.stub(optlyInstance.notificationCenter, 'sendNotifications');
4914-
sinon.stub(errorHandler, 'handleError');
4915-
sinon.stub(createdLogger, 'log');
4916-
sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c');
49174915
});
49184916

49194917
afterEach(function() {
49204918
optlyInstance.notificationCenter.sendNotifications.restore();
4921-
errorHandler.handleError.restore();
4922-
createdLogger.log.restore();
4923-
fns.uuid.restore();
49244919
});
49254920

49264921
it('should make a decision and do not dispatch an event', function() {
@@ -4965,6 +4960,295 @@ describe('lib/optimizely', function() {
49654960
});
49664961
});
49674962
});
4963+
4964+
describe('#decideForKeys', function() {
4965+
var userId = 'tester';
4966+
beforeEach(function() {
4967+
optlyInstance = new Optimizely({
4968+
clientEngine: 'node-sdk',
4969+
datafile: testData.getTestDecideProjectConfig(),
4970+
errorHandler: errorHandler,
4971+
eventDispatcher: eventDispatcher,
4972+
jsonSchemaValidator: jsonSchemaValidator,
4973+
logger: createdLogger,
4974+
isValidInstance: true,
4975+
eventBatchSize: 1,
4976+
defaultDecideOptions: [],
4977+
});
4978+
4979+
sinon.stub(optlyInstance.notificationCenter, 'sendNotifications');
4980+
});
4981+
4982+
afterEach(function() {
4983+
optlyInstance.notificationCenter.sendNotifications.restore();
4984+
});
4985+
4986+
it('should return decision results map with single flag key provided for feature_test and dispatch an event', function() {
4987+
var flagKey = 'feature_2';
4988+
var user = optlyInstance.createUserContext(userId);
4989+
var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId);
4990+
var decisionsMap = optlyInstance.decideForKeys(user, [ flagKey ]);
4991+
var decision = decisionsMap[flagKey];
4992+
var expectedDecision = {
4993+
variationKey: 'variation_with_traffic',
4994+
enabled: true,
4995+
variables: expectedVariables,
4996+
ruleKey: 'exp_no_audience',
4997+
flagKey: flagKey,
4998+
userContext: user,
4999+
reasons: [],
5000+
}
5001+
assert.deepEqual(Object.values(decisionsMap).length, 1);
5002+
assert.deepEqual(decision, expectedDecision);
5003+
sinon.assert.calledOnce(optlyInstance.eventDispatcher.dispatchEvent);
5004+
sinon.assert.callCount(optlyInstance.notificationCenter.sendNotifications, 4)
5005+
var notificationCallArgs = optlyInstance.notificationCenter.sendNotifications.getCall(3).args;
5006+
var decisionEventDispatched = notificationCallArgs[1].decisionInfo.decisionEventDispatched;
5007+
assert.deepEqual(decisionEventDispatched, true);
5008+
});
5009+
5010+
it('should return decision results map with two flag keys provided and dispatch events', function() {
5011+
var flagKeysArray = ['feature_1', 'feature_2'];
5012+
var user = optlyInstance.createUserContext(userId);
5013+
var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKeysArray[0], userId);
5014+
var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKeysArray[1], userId);
5015+
var decisionsMap = optlyInstance.decideForKeys(user, flagKeysArray);
5016+
var decision1 = decisionsMap[flagKeysArray[0]];
5017+
var decision2 = decisionsMap[flagKeysArray[1]];
5018+
var expectedDecision1 = {
5019+
variationKey: '18257766532',
5020+
enabled: true,
5021+
variables: expectedVariables1,
5022+
ruleKey: '18322080788',
5023+
flagKey: flagKeysArray[0],
5024+
userContext: user,
5025+
reasons: [],
5026+
}
5027+
var expectedDecision2 = {
5028+
variationKey: 'variation_with_traffic',
5029+
enabled: true,
5030+
variables: expectedVariables2,
5031+
ruleKey: 'exp_no_audience',
5032+
flagKey: flagKeysArray[1],
5033+
userContext: user,
5034+
reasons: [],
5035+
}
5036+
assert.deepEqual(Object.values(decisionsMap).length, 2);
5037+
assert.deepEqual(decision1, expectedDecision1);
5038+
assert.deepEqual(decision2, expectedDecision2);
5039+
sinon.assert.calledTwice(optlyInstance.eventDispatcher.dispatchEvent);
5040+
});
5041+
5042+
it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() {
5043+
var flagKey1 = 'feature_2';
5044+
var flagKey2 = 'feature_3';
5045+
var user = optlyInstance.createUserContext(userId, {"gender": "female"});
5046+
var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey1, userId);
5047+
var decisionsMap = optlyInstance.decideForKeys(user, [ flagKey1, flagKey2 ], [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ]);
5048+
var decision = decisionsMap[flagKey1];
5049+
var expectedDecision = {
5050+
variationKey: 'variation_with_traffic',
5051+
enabled: true,
5052+
variables: expectedVariables,
5053+
ruleKey: 'exp_no_audience',
5054+
flagKey: flagKey1,
5055+
userContext: user,
5056+
reasons: [],
5057+
}
5058+
assert.deepEqual(Object.values(decisionsMap).length, 1);
5059+
assert.deepEqual(decision, expectedDecision);
5060+
sinon.assert.calledTwice(optlyInstance.eventDispatcher.dispatchEvent);
5061+
});
5062+
});
5063+
5064+
describe('#decideAll', function() {
5065+
var userId = 'tester';
5066+
describe('with empty default decide options', function() {
5067+
beforeEach(function() {
5068+
optlyInstance = new Optimizely({
5069+
clientEngine: 'node-sdk',
5070+
datafile: testData.getTestDecideProjectConfig(),
5071+
errorHandler: errorHandler,
5072+
eventDispatcher: eventDispatcher,
5073+
jsonSchemaValidator: jsonSchemaValidator,
5074+
logger: createdLogger,
5075+
isValidInstance: true,
5076+
eventBatchSize: 1,
5077+
defaultDecideOptions: [],
5078+
});
5079+
5080+
sinon.stub(optlyInstance.notificationCenter, 'sendNotifications');
5081+
});
5082+
5083+
afterEach(function() {
5084+
optlyInstance.notificationCenter.sendNotifications.restore();
5085+
});
5086+
5087+
it('should return decision results map with all flag keys provided and dispatch events', function() {
5088+
var configObj = optlyInstance.projectConfigManager.getConfig();
5089+
var allFlagKeysArray = Object.keys(configObj.featureKeyMap);
5090+
var user = optlyInstance.createUserContext(userId);
5091+
var expectedVariables1 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[0], userId);
5092+
var expectedVariables2 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[1], userId);
5093+
var expectedVariables3 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[2], userId);
5094+
var decisionsMap = user.decideAll(allFlagKeysArray);
5095+
var decision1 = decisionsMap[allFlagKeysArray[0]];
5096+
var decision2 = decisionsMap[allFlagKeysArray[1]];
5097+
var decision3 = decisionsMap[allFlagKeysArray[2]];
5098+
var expectedDecision1 = {
5099+
variationKey: '18257766532',
5100+
enabled: true,
5101+
variables: expectedVariables1,
5102+
ruleKey: '18322080788',
5103+
flagKey: allFlagKeysArray[0],
5104+
userContext: user,
5105+
reasons: [],
5106+
}
5107+
var expectedDecision2 = {
5108+
variationKey: 'variation_with_traffic',
5109+
enabled: true,
5110+
variables: expectedVariables2,
5111+
ruleKey: 'exp_no_audience',
5112+
flagKey: allFlagKeysArray[1],
5113+
userContext: user,
5114+
reasons: [],
5115+
}
5116+
var expectedDecision3 = {
5117+
variationKey: '',
5118+
enabled: false,
5119+
variables: expectedVariables3,
5120+
ruleKey: '',
5121+
flagKey: allFlagKeysArray[2],
5122+
userContext: user,
5123+
reasons: [],
5124+
}
5125+
assert.deepEqual(Object.values(decisionsMap).length, allFlagKeysArray.length);
5126+
assert.deepEqual(decision1, expectedDecision1);
5127+
assert.deepEqual(decision2, expectedDecision2);
5128+
assert.deepEqual(decision3, expectedDecision3);
5129+
sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent);
5130+
});
5131+
5132+
it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() {
5133+
var flagKey1 = 'feature_1';
5134+
var flagKey2 = 'feature_2';
5135+
var user = optlyInstance.createUserContext(userId, {"gender": "female"});
5136+
var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId);
5137+
var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId);
5138+
var decisionsMap = optlyInstance.decideAll(user, [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ]);
5139+
var decision1 = decisionsMap[flagKey1];
5140+
var decision2 = decisionsMap[flagKey2];
5141+
var expectedDecision1 = {
5142+
variationKey: '18257766532',
5143+
enabled: true,
5144+
variables: expectedVariables1,
5145+
ruleKey: '18322080788',
5146+
flagKey: flagKey1,
5147+
userContext: user,
5148+
reasons: [],
5149+
}
5150+
var expectedDecision2 = {
5151+
variationKey: 'variation_with_traffic',
5152+
enabled: true,
5153+
variables: expectedVariables2,
5154+
ruleKey: 'exp_no_audience',
5155+
flagKey: flagKey2,
5156+
userContext: user,
5157+
reasons: [],
5158+
}
5159+
assert.deepEqual(Object.values(decisionsMap).length, 2);
5160+
assert.deepEqual(decision1, expectedDecision1);
5161+
assert.deepEqual(decision2, expectedDecision2);
5162+
sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent);
5163+
});
5164+
});
5165+
5166+
describe('with ENABLED_FLAGS_ONLY flag in default decide options', function() {
5167+
beforeEach(function() {
5168+
optlyInstance = new Optimizely({
5169+
clientEngine: 'node-sdk',
5170+
datafile: testData.getTestDecideProjectConfig(),
5171+
errorHandler: errorHandler,
5172+
eventDispatcher: eventDispatcher,
5173+
jsonSchemaValidator: jsonSchemaValidator,
5174+
logger: createdLogger,
5175+
isValidInstance: true,
5176+
eventBatchSize: 1,
5177+
defaultDecideOptions: [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ],
5178+
});
5179+
5180+
sinon.stub(optlyInstance.notificationCenter, 'sendNotifications');
5181+
});
5182+
5183+
afterEach(function() {
5184+
optlyInstance.notificationCenter.sendNotifications.restore();
5185+
});
5186+
5187+
it('should return decision results map with only enabled flags and dispatch events', function() {
5188+
var flagKey1 = 'feature_1';
5189+
var flagKey2 = 'feature_2';
5190+
var user = optlyInstance.createUserContext(userId, {"gender": "female"});
5191+
var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId);
5192+
var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId);
5193+
var decisionsMap = optlyInstance.decideAll(user);
5194+
var decision1 = decisionsMap[flagKey1];
5195+
var decision2 = decisionsMap[flagKey2];
5196+
var expectedDecision1 = {
5197+
variationKey: '18257766532',
5198+
enabled: true,
5199+
variables: expectedVariables1,
5200+
ruleKey: '18322080788',
5201+
flagKey: flagKey1,
5202+
userContext: user,
5203+
reasons: [],
5204+
}
5205+
var expectedDecision2 = {
5206+
variationKey: 'variation_with_traffic',
5207+
enabled: true,
5208+
variables: expectedVariables2,
5209+
ruleKey: 'exp_no_audience',
5210+
flagKey: flagKey2,
5211+
userContext: user,
5212+
reasons: [],
5213+
}
5214+
assert.deepEqual(Object.values(decisionsMap).length, 2);
5215+
assert.deepEqual(decision1, expectedDecision1);
5216+
assert.deepEqual(decision2, expectedDecision2);
5217+
sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent);
5218+
});
5219+
5220+
it('should return decision results map with only enabled flags and excluded variables when EXCLUDE_VARIABLES_FLAG is passed in', function() {
5221+
var flagKey1 = 'feature_1';
5222+
var flagKey2 = 'feature_2';
5223+
var user = optlyInstance.createUserContext(userId, {"gender": "female"});
5224+
var decisionsMap = optlyInstance.decideAll(user, [ OptimizelyDecideOptions.EXCLUDE_VARIABLES ]);
5225+
var decision1 = decisionsMap[flagKey1];
5226+
var decision2 = decisionsMap[flagKey2];
5227+
var expectedDecision1 = {
5228+
variationKey: '18257766532',
5229+
enabled: true,
5230+
variables: {},
5231+
ruleKey: '18322080788',
5232+
flagKey: flagKey1,
5233+
userContext: user,
5234+
reasons: [],
5235+
}
5236+
var expectedDecision2 = {
5237+
variationKey: 'variation_with_traffic',
5238+
enabled: true,
5239+
variables: {},
5240+
ruleKey: 'exp_no_audience',
5241+
flagKey: flagKey2,
5242+
userContext: user,
5243+
reasons: [],
5244+
}
5245+
assert.deepEqual(Object.values(decisionsMap).length, 2);
5246+
assert.deepEqual(decision1, expectedDecision1);
5247+
assert.deepEqual(decision2, expectedDecision2);
5248+
sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent);
5249+
});
5250+
});
5251+
});
49685252
});
49695253

49705254
//tests separated out from APIs because of mock bucketing

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,4 +1593,61 @@ export default class Optimizely {
15931593

15941594
return allDecideOptions;
15951595
}
1596+
1597+
/**
1598+
* Returns an object of decision results for multiple flag keys and a user context.
1599+
* If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error.
1600+
* The SDK will always return an object of decisions. When it cannot process requests, it will return an empty object after logging the errors.
1601+
* @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient
1602+
* @param {string[]} keys An array of flag keys for which decisions will be made.
1603+
* @param {OptimizelyDecideOptions[]} options An array of options for decision-making.
1604+
* @return {[key: string]: OptimizelyDecision} An object of decision results mapped by flag keys.
1605+
*/
1606+
1607+
decideForKeys(
1608+
user: OptimizelyUserContext,
1609+
keys: string[],
1610+
options: OptimizelyDecideOptions[] = []
1611+
): { [key: string]: OptimizelyDecision } {
1612+
const decisionMap: { [key: string]: OptimizelyDecision } = {};
1613+
if (!this.isValidInstance()) {
1614+
this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'decideForKeys'));
1615+
return decisionMap;
1616+
}
1617+
if (keys.length === 0) {
1618+
return decisionMap;
1619+
}
1620+
1621+
const allDecideOptions = this.getAllDecideOptions(options);
1622+
keys.forEach(key => {
1623+
const optimizelyDecision: OptimizelyDecision = this.decide(user, key, options);
1624+
if (!allDecideOptions[OptimizelyDecideOptions.ENABLED_FLAGS_ONLY] || optimizelyDecision.enabled) {
1625+
decisionMap[key] = optimizelyDecision;
1626+
}
1627+
});
1628+
1629+
return decisionMap;
1630+
}
1631+
1632+
/**
1633+
* Returns an object of decision results for all active flag keys.
1634+
* @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient
1635+
* @param {OptimizelyDecideOptions[]} options An array of options for decision-making.
1636+
* @return {[key: string]: OptimizelyDecision} An object of all decision results mapped by flag keys.
1637+
*/
1638+
decideAll(
1639+
user: OptimizelyUserContext,
1640+
options: OptimizelyDecideOptions[] = []
1641+
): { [key: string]: OptimizelyDecision } {
1642+
const configObj = this.projectConfigManager.getConfig();
1643+
const decisionMap: { [key: string]: OptimizelyDecision } = {};
1644+
if (!this.isValidInstance() || !configObj) {
1645+
this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'decideAll'));
1646+
return decisionMap;
1647+
}
1648+
1649+
const allFlagKeys = Object.keys(configObj.featureKeyMap);
1650+
1651+
return this.decideForKeys(user, allFlagKeys, options);
1652+
}
15961653
}

0 commit comments

Comments
 (0)