diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 89254c184..54951f7ee 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -4333,6 +4333,7 @@ describe('lib/optimizely', function() { logLevel: LOG_LEVEL.INFO, logToConsole: false, }); + describe('#createUserContext', function() { beforeEach(function() { optlyInstance = new Optimizely({ @@ -4911,16 +4912,10 @@ describe('lib/optimizely', function() { }); sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); - sinon.stub(errorHandler, 'handleError'); - sinon.stub(createdLogger, 'log'); - sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); }); afterEach(function() { optlyInstance.notificationCenter.sendNotifications.restore(); - errorHandler.handleError.restore(); - createdLogger.log.restore(); - fns.uuid.restore(); }); it('should make a decision and do not dispatch an event', function() { @@ -4965,6 +4960,295 @@ describe('lib/optimizely', function() { }); }); }); + + describe('#decideForKeys', function() { + var userId = 'tester'; + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [], + }); + + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should return decision results map with single flag key provided for feature_test and dispatch an event', function() { + var flagKey = 'feature_2'; + var user = optlyInstance.createUserContext(userId); + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey, userId); + var decisionsMap = optlyInstance.decideForKeys(user, [ flagKey ]); + var decision = decisionsMap[flagKey]; + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey, + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 1); + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledOnce(optlyInstance.eventDispatcher.dispatchEvent); + sinon.assert.callCount(optlyInstance.notificationCenter.sendNotifications, 4) + var notificationCallArgs = optlyInstance.notificationCenter.sendNotifications.getCall(3).args; + var decisionEventDispatched = notificationCallArgs[1].decisionInfo.decisionEventDispatched; + assert.deepEqual(decisionEventDispatched, true); + }); + + it('should return decision results map with two flag keys provided and dispatch events', function() { + var flagKeysArray = ['feature_1', 'feature_2']; + var user = optlyInstance.createUserContext(userId); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKeysArray[0], userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKeysArray[1], userId); + var decisionsMap = optlyInstance.decideForKeys(user, flagKeysArray); + var decision1 = decisionsMap[flagKeysArray[0]]; + var decision2 = decisionsMap[flagKeysArray[1]]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKeysArray[0], + userContext: user, + reasons: [], + } + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKeysArray[1], + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledTwice(optlyInstance.eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() { + var flagKey1 = 'feature_2'; + var flagKey2 = 'feature_3'; + var user = optlyInstance.createUserContext(userId, {"gender": "female"}); + var expectedVariables = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var decisionsMap = optlyInstance.decideForKeys(user, [ flagKey1, flagKey2 ], [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ]); + var decision = decisionsMap[flagKey1]; + var expectedDecision = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables, + ruleKey: 'exp_no_audience', + flagKey: flagKey1, + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 1); + assert.deepEqual(decision, expectedDecision); + sinon.assert.calledTwice(optlyInstance.eventDispatcher.dispatchEvent); + }); + }); + + describe('#decideAll', function() { + var userId = 'tester'; + describe('with empty default decide options', function() { + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [], + }); + + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should return decision results map with all flag keys provided and dispatch events', function() { + var configObj = optlyInstance.projectConfigManager.getConfig(); + var allFlagKeysArray = Object.keys(configObj.featureKeyMap); + var user = optlyInstance.createUserContext(userId); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[0], userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[1], userId); + var expectedVariables3 = optlyInstance.getAllFeatureVariables(allFlagKeysArray[2], userId); + var decisionsMap = user.decideAll(allFlagKeysArray); + var decision1 = decisionsMap[allFlagKeysArray[0]]; + var decision2 = decisionsMap[allFlagKeysArray[1]]; + var decision3 = decisionsMap[allFlagKeysArray[2]]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: allFlagKeysArray[0], + userContext: user, + reasons: [], + } + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: allFlagKeysArray[1], + userContext: user, + reasons: [], + } + var expectedDecision3 = { + variationKey: '', + enabled: false, + variables: expectedVariables3, + ruleKey: '', + flagKey: allFlagKeysArray[2], + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, allFlagKeysArray.length); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + assert.deepEqual(decision3, expectedDecision3); + sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags when ENABLED_FLAGS_ONLY flag is passed in and dispatch events', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var user = optlyInstance.createUserContext(userId, {"gender": "female"}); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId); + var decisionsMap = optlyInstance.decideAll(user, [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ]); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + } + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent); + }); + }); + + describe('with ENABLED_FLAGS_ONLY flag in default decide options', function() { + beforeEach(function() { + optlyInstance = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [ OptimizelyDecideOptions.ENABLED_FLAGS_ONLY ], + }); + + sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); + }); + + afterEach(function() { + optlyInstance.notificationCenter.sendNotifications.restore(); + }); + + it('should return decision results map with only enabled flags and dispatch events', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var user = optlyInstance.createUserContext(userId, {"gender": "female"}); + var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId); + var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId); + var decisionsMap = optlyInstance.decideAll(user); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: expectedVariables1, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + } + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: expectedVariables2, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent); + }); + + it('should return decision results map with only enabled flags and excluded variables when EXCLUDE_VARIABLES_FLAG is passed in', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var user = optlyInstance.createUserContext(userId, {"gender": "female"}); + var decisionsMap = optlyInstance.decideAll(user, [ OptimizelyDecideOptions.EXCLUDE_VARIABLES ]); + var decision1 = decisionsMap[flagKey1]; + var decision2 = decisionsMap[flagKey2]; + var expectedDecision1 = { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: user, + reasons: [], + } + var expectedDecision2 = { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: user, + reasons: [], + } + assert.deepEqual(Object.values(decisionsMap).length, 2); + assert.deepEqual(decision1, expectedDecision1); + assert.deepEqual(decision2, expectedDecision2); + sinon.assert.calledThrice(optlyInstance.eventDispatcher.dispatchEvent); + }); + }); + }); }); //tests separated out from APIs because of mock bucketing diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts index e743416cd..5c4c504f9 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.ts +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -1593,4 +1593,61 @@ export default class Optimizely { return allDecideOptions; } + + /** + * Returns an object of decision results for multiple flag keys and a user context. + * If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + * The SDK will always return an object of decisions. When it cannot process requests, it will return an empty object after logging the errors. + * @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient + * @param {string[]} keys An array of flag keys for which decisions will be made. + * @param {OptimizelyDecideOptions[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of decision results mapped by flag keys. + */ + + decideForKeys( + user: OptimizelyUserContext, + keys: string[], + options: OptimizelyDecideOptions[] = [] + ): { [key: string]: OptimizelyDecision } { + const decisionMap: { [key: string]: OptimizelyDecision } = {}; + if (!this.isValidInstance()) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'decideForKeys')); + return decisionMap; + } + if (keys.length === 0) { + return decisionMap; + } + + const allDecideOptions = this.getAllDecideOptions(options); + keys.forEach(key => { + const optimizelyDecision: OptimizelyDecision = this.decide(user, key, options); + if (!allDecideOptions[OptimizelyDecideOptions.ENABLED_FLAGS_ONLY] || optimizelyDecision.enabled) { + decisionMap[key] = optimizelyDecision; + } + }); + + return decisionMap; + } + + /** + * Returns an object of decision results for all active flag keys. + * @param {OptimizelyUserContext} user A user context associated with this OptimizelyClient + * @param {OptimizelyDecideOptions[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of all decision results mapped by flag keys. + */ + decideAll( + user: OptimizelyUserContext, + options: OptimizelyDecideOptions[] = [] + ): { [key: string]: OptimizelyDecision } { + const configObj = this.projectConfigManager.getConfig(); + const decisionMap: { [key: string]: OptimizelyDecision } = {}; + if (!this.isValidInstance() || !configObj) { + this.logger.log(LOG_LEVEL.ERROR, sprintf(LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'decideAll')); + return decisionMap; + } + + const allFlagKeys = Object.keys(configObj.featureKeyMap); + + return this.decideForKeys(user, allFlagKeys, options); + } } diff --git a/packages/optimizely-sdk/lib/optimizely_user_context/index.tests.js b/packages/optimizely-sdk/lib/optimizely_user_context/index.tests.js index c5e8de72b..7e62836a9 100644 --- a/packages/optimizely-sdk/lib/optimizely_user_context/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely_user_context/index.tests.js @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2020, Optimizely, Inc. and contributors * + * Copyright 2020, 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. * @@ -20,6 +20,8 @@ import OptimizelyUserContext from './'; describe('lib/optimizely_user_context', function() { describe('APIs', function() { var fakeOptimizely; + var userId = 'tester'; + var options = 'fakeOption'; describe('#setAttribute', function() { fakeOptimizely = { decide: sinon.stub().returns({}) @@ -134,7 +136,6 @@ describe('lib/optimizely_user_context', function() { }); describe('#decide', function() { - var userId = 'tester'; it('should return an expected decision object', function() { var flagKey = 'feature_1'; var fakeDecision = { @@ -143,7 +144,7 @@ describe('lib/optimizely_user_context', function() { variables: {}, ruleKey: 'exp_no_audience', flagKey: flagKey, - userContext: user, + userContext: 'fakeUserContext', reasons: [], }; fakeOptimizely = { @@ -153,9 +154,113 @@ describe('lib/optimizely_user_context', function() { optimizely: fakeOptimizely, userId, }); - var decision = user.decide(flagKey); + var decision = user.decide(flagKey, options); + sinon.assert.calledWithExactly( + fakeOptimizely.decide, + user, + flagKey, + options + ); assert.deepEqual(decision, fakeDecision); }); }); + + describe('#decideForKeys', function() { + it('should return an expected decision results object', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var fakeDecisionMap = { + flagKey1: + { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey2: + { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: 'fakeUserContext', + reasons: [], + }, + }; + fakeOptimizely = { + decideForKeys: sinon.stub().returns(fakeDecisionMap) + }; + var user = new OptimizelyUserContext({ + optimizely: fakeOptimizely, + userId, + }); + var decisionMap = user.decideForKeys([ flagKey1, flagKey2 ], options); + sinon.assert.calledWithExactly( + fakeOptimizely.decideForKeys, + user, + [ flagKey1, flagKey2 ], + options + ); + assert.deepEqual(decisionMap, fakeDecisionMap); + }); + }); + + describe('#decideAll', function() { + it('should return an expected decision results object', function() { + var flagKey1 = 'feature_1'; + var flagKey2 = 'feature_2'; + var flagKey3 = 'feature_3'; + var fakeDecisionMap = { + flagKey1: + { + variationKey: '18257766532', + enabled: true, + variables: {}, + ruleKey: '18322080788', + flagKey: flagKey1, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey2: + { + variationKey: 'variation_with_traffic', + enabled: true, + variables: {}, + ruleKey: 'exp_no_audience', + flagKey: flagKey2, + userContext: 'fakeUserContext', + reasons: [], + }, + flagKey3: + { + variationKey: '', + enabled: false, + variables: {}, + ruleKey: '', + flagKey: flagKey3, + userContext: user, + reasons: [], + }, + }; + fakeOptimizely = { + decideAll: sinon.stub().returns(fakeDecisionMap) + }; + var user = new OptimizelyUserContext({ + optimizely: fakeOptimizely, + userId, + }); + var decisionMap = user.decideAll(options); + sinon.assert.calledWithExactly( + fakeOptimizely.decideAll, + user, + options + ); + assert.deepEqual(decisionMap, fakeDecisionMap); + }); + }); }); }); diff --git a/packages/optimizely-sdk/lib/optimizely_user_context/index.ts b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts index cde19959d..adde48679 100644 --- a/packages/optimizely-sdk/lib/optimizely_user_context/index.ts +++ b/packages/optimizely-sdk/lib/optimizely_user_context/index.ts @@ -61,7 +61,7 @@ export default class OptimizelyUserContext { * Returns a decision result for a given flag key and a user context, which contains all data required to deliver the flag. * If the SDK finds an error, it will return a decision with null for variationKey. The decision will include an error message in reasons. * @param {string} key A flag key for which a decision will be made. - * @param {OptimizelyDecideOption} options A list of options for decision-making. + * @param {OptimizelyDecideOption} options An array of options for decision-making. * @return {OptimizelyDecision} A decision result. */ decide( @@ -72,13 +72,31 @@ export default class OptimizelyUserContext { return this.optimizely.decide(this, key, options); } - decideForKeys(): void { - //TODO: implement - return; + /** + * Returns an object of decision results for multiple flag keys and a user context. + * If the SDK finds an error for a key, the response will include a decision for the key showing reasons for the error. + * The SDK will always return key-mapped decisions. When it cannot process requests, it will return an empty map after logging the errors. + * @param {string[]} keys An array of flag keys for which decisions will be made. + * @param {OptimizelyDecideOptions[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of decision results mapped by flag keys. + */ + decideForKeys( + keys: string[], + options: OptimizelyDecideOptions[] = [], + ): { [key: string]: OptimizelyDecision } { + + return this.optimizely.decideForKeys(this, keys, options); } - decideAll(): void { - //TODO: implement - return; + /** + * Returns an object of decision results for all active flag keys. + * @param {OptimizelyDecideOptions[]} options An array of options for decision-making. + * @return {[key: string]: OptimizelyDecision} An object of all decision results mapped by flag keys. + */ + decideAll( + options: OptimizelyDecideOptions[] = [] + ): { [key: string]: OptimizelyDecision } { + + return this.optimizely.decideAll(this, options); } }