From 927eccb294fd48b0034b281a62c709924725055f Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Mon, 4 Jan 2021 14:19:38 -0800 Subject: [PATCH 1/7] * Check for sticky bucketing if decide options do not include shouldIgnoreUPS * Update getVariationForFeature method to include options * Fix existing unit test * Add unit test for IGNORE_USER_PROFILE_SERVICE flag in decide options --- .../lib/core/decision_service/index.d.ts | 18 ++--- .../lib/core/decision_service/index.js | 72 ++++++++++--------- .../lib/core/decision_service/index.tests.js | 12 +++- .../lib/optimizely/index.tests.js | 52 ++++++++++++-- .../optimizely-sdk/lib/optimizely/index.ts | 3 +- 5 files changed, 109 insertions(+), 48 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts index a219625d5..445054383 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts @@ -60,19 +60,21 @@ export interface DecisionService { * experiment properties (both objects), as well as a decisionSource property. * decisionSource indicates whether the decision was due to a rollout or an * experiment. - * @param {ProjectConfig} configObj The parsed project configuration object - * @param {FeatureFlag} feature A feature flag object from project configuration - * @param {string} userId A string identifying the user, for bucketing - * @param {unknown} attributes Optional user attributes - * @return {DecisionResponse} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource - * properties and the decide reasons. If the user was not bucketed into a variation, the - * variation property in decision object is null. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {FeatureFlag} feature A feature flag object from project configuration + * @param {string} userId A string identifying the user, for bucketing + * @param {unknown} attributes Optional user attributes + * @param {[key: string]: boolean} options Map of decide options + * @return {DecisionResponse} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource + * properties and the decide reasons. If the user was not bucketed into a variation, the + * variation property in decision object is null. */ getVariationForFeature( configObj: ProjectConfig, feature: FeatureFlag, userId: string, - attributes: unknown + attributes: unknown, + options?: { [key: string]: boolean } ): DecisionResponse; /** diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index 659e305e0..fecf84223 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -21,6 +21,7 @@ import * as enums from '../../utils/enums'; import projectConfig from '../project_config'; import AudienceEvaluator from '../audience_evaluator'; import * as stringValidator from '../../utils/string_value_validator'; +import { OptimizelyDecideOptions } from '../../shared_types'; var MODULE_NAME = 'DECISION_SERVICE'; var ERROR_MESSAGES = enums.ERROR_MESSAGES; @@ -55,14 +56,15 @@ function DecisionService(options) { /** * Gets variation where visitor will be bucketed. - * @param {Object} configObj The parsed project configuration object - * @param {string} experimentKey - * @param {string} userId - * @param {Object} attributes - * @return {Object} DecisionResonse DecisionResonse containing the variation the user is bucketed into - * and the decide reasons. + * @param {Object} configObj The parsed project configuration object + * @param {string} experimentKey + * @param {string} userId + * @param {Object} attributes + * @param {OptimizelyDecideOptions[]} options Decide options + * @return {Object} DecisionResonse DecisionResonse containing the variation the user is bucketed into + * and the decide reasons. */ -DecisionService.prototype.getVariation = function(configObj, experimentKey, userId, attributes) { +DecisionService.prototype.getVariation = function(configObj, experimentKey, userId, attributes, options = {}) { // by default, the bucketing ID should be the user ID var bucketingId = this._getBucketingId(userId, attributes); var decideReasons = []; @@ -98,26 +100,30 @@ DecisionService.prototype.getVariation = function(configObj, experimentKey, user }; } - // check for sticky bucketing - var experimentBucketMap = this.__resolveExperimentBucketMap(userId, attributes); - variation = this.__getStoredVariation(configObj, experiment, userId, experimentBucketMap); - if (variation) { - var returningStoredVariationMessage = sprintf( - LOG_MESSAGES.RETURNING_STORED_VARIATION, - MODULE_NAME, - variation.key, - experimentKey, - userId - ); - this.logger.log( - LOG_LEVEL.INFO, - returningStoredVariationMessage - ); - decideReasons.push(returningStoredVariationMessage); - return { - result: variation.key, - reasons: decideReasons, - }; + var shouldIgnoreUPS = options[OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE]; + + // check for sticky bucketing if decide options do not include shouldIgnoreUPS + if (!shouldIgnoreUPS) { + var experimentBucketMap = this.__resolveExperimentBucketMap(userId, attributes); + variation = this.__getStoredVariation(configObj, experiment, userId, experimentBucketMap); + if (variation) { + var returningStoredVariationMessage = sprintf( + LOG_MESSAGES.RETURNING_STORED_VARIATION, + MODULE_NAME, + variation.key, + experimentKey, + userId + ); + this.logger.log( + LOG_LEVEL.INFO, + returningStoredVariationMessage + ); + decideReasons.push(returningStoredVariationMessage); + return { + result: variation.key, + reasons: decideReasons, + }; + } } // Perform regular targeting and bucketing @@ -418,13 +424,14 @@ DecisionService.prototype.__saveUserProfile = function(experiment, variation, us * @param {Object} feature A feature flag object from project configuration * @param {String} userId A string identifying the user, for bucketing * @param {Object} attributes Optional user attributes + * @param {Object} options Map of decide options * @return {Object} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource * properties and decide reasons. If the user was not bucketed into a variation, the variation * property in decision object is null. */ -DecisionService.prototype.getVariationForFeature = function(configObj, feature, userId, attributes) { +DecisionService.prototype.getVariationForFeature = function(configObj, feature, userId, attributes, options = {}) { var decideReasons = []; - var decisionVariation = this._getVariationForFeatureExperiment(configObj, feature, userId, attributes); + var decisionVariation = this._getVariationForFeatureExperiment(configObj, feature, userId, attributes, options); decideReasons.push(...decisionVariation.reasons); var experimentDecision = decisionVariation.result; @@ -457,7 +464,8 @@ DecisionService.prototype.getVariationForFeature = function(configObj, feature, }; }; -DecisionService.prototype._getVariationForFeatureExperiment = function(configObj, feature, userId, attributes) { + +DecisionService.prototype._getVariationForFeatureExperiment = function(configObj, feature, userId, attributes, options) { var decideReasons = []; var experiment = null; var variationKey = null; @@ -468,7 +476,7 @@ DecisionService.prototype._getVariationForFeatureExperiment = function(configObj if (group) { experiment = this._getExperimentInGroup(configObj, group, userId); if (experiment && feature.experimentIds.indexOf(experiment.id) !== -1) { - decisionVariation = this.getVariation(configObj, experiment.key, userId, attributes); + decisionVariation = this.getVariation(configObj, experiment.key, userId, attributes, options); decideReasons.push(...decisionVariation.reasons); variationKey = decisionVariation.result; } @@ -478,7 +486,7 @@ DecisionService.prototype._getVariationForFeatureExperiment = function(configObj // with one experiment, so we look at the first experiment ID only experiment = projectConfig.getExperimentFromId(configObj, feature.experimentIds[0], this.logger); if (experiment) { - decisionVariation = this.getVariation(configObj, experiment.key, userId, attributes); + decisionVariation = this.getVariation(configObj, experiment.key, userId, attributes, options); decideReasons.push(...decisionVariation.reasons); variationKey = decisionVariation.result; } diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js index 4231745c6..eb742d8d0 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js @@ -1364,9 +1364,15 @@ describe('lib/core/decision_service', function() { decisionSource: DECISION_SOURCES.FEATURE_TEST, }; assert.deepEqual(decision, expectedDecision); - sinon.assert.calledWithExactly(getVariationStub, configObj, 'testing_my_feature', 'user1', { - test_attribute: 'test_value', - }); + sinon.assert.calledWithExactly( + getVariationStub, + configObj, + 'testing_my_feature', 'user1', + { + test_attribute: 'test_value', + }, + {} + ); }); }); diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 3c1876488..6eada8dba 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -5621,6 +5621,50 @@ describe('lib/optimizely', function() { ); }); }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId1 = '10418551353'; + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + var variationKey2 = 'variation_no_traffic'; + var mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { // 'exp_no_audience' + variation_id: variationId2, + }, + }, + }), + save: sinon.stub() + }; + var optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId + }); + var decision1 = optlyInstanceWithUserProfile.decide(user, flagKey); + // should return variationId2 set by UPS + assert.equal(variationKey2, decision1.variationKey); + var decision2 = optlyInstanceWithUserProfile.decide(user, flagKey, [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ]); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision2.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); + }); + }); }); describe('#decideForKeys', function() { @@ -5704,7 +5748,7 @@ describe('lib/optimizely', function() { 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 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]; @@ -5794,7 +5838,7 @@ describe('lib/optimizely', function() { 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 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 ]); @@ -5849,7 +5893,7 @@ describe('lib/optimizely', function() { 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 user = optlyInstance.createUserContext(userId, { gender: 'female' }); var expectedVariables1 = optlyInstance.getAllFeatureVariables(flagKey1, userId); var expectedVariables2 = optlyInstance.getAllFeatureVariables(flagKey2, userId); var decisionsMap = optlyInstance.decideAll(user); @@ -5882,7 +5926,7 @@ describe('lib/optimizely', function() { 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 user = optlyInstance.createUserContext(userId, { gender: 'female' }); var decisionsMap = optlyInstance.decideAll(user, [ OptimizelyDecideOptions.EXCLUDE_VARIABLES ]); var decision1 = decisionsMap[flagKey1]; var decision2 = decisionsMap[flagKey2]; diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts index a210ae12b..76572f54a 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.ts +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -1490,7 +1490,8 @@ export default class Optimizely { configObj, feature, userId, - attributes + attributes, + allDecideOptions, ); reasons.push(...decisionVariation.reasons); const decisionObj = decisionVariation.result; From 1fac8a5889fd30441594ace1b44c349242cf0536 Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 10:26:54 -0800 Subject: [PATCH 2/7] Fix comments --- .../lib/core/decision_service/index.d.ts | 18 ++++++++++-------- .../lib/core/decision_service/index.js | 18 +++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts index 445054383..87b46865e 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts @@ -39,18 +39,20 @@ export interface DecisionService { /** * Gets a decision response containing variation where visitor will be bucketed and the decide reasons. - * @param {ProjectConfig} configObj The parsed project configuration object - * @param {string} experimentKey - * @param {string} userId - * @param {UserAttributes} attributes - * @return {DecisionResponse} DecisionResponse DecisionResponse containing variation - * the user is bucketed into and the decide reasons. + * @param {ProjectConfig} configObj The parsed project configuration object + * @param {string} experimentKey + * @param {string} userId + * @param {UserAttributes} attributes + * @param {[key: string]: boolean} options Optional map of decide options + * @return {DecisionResponse} DecisionResponse DecisionResponse containing variation + * the user is bucketed into and the decide reasons. */ getVariation( configObj: ProjectConfig, experimentKey: string, userId: string, - attributes?: UserAttributes + attributes?: UserAttributes, + options?: { [key: string]: boolean }, ): DecisionResponse; /** @@ -64,7 +66,7 @@ export interface DecisionService { * @param {FeatureFlag} feature A feature flag object from project configuration * @param {string} userId A string identifying the user, for bucketing * @param {unknown} attributes Optional user attributes - * @param {[key: string]: boolean} options Map of decide options + * @param {[key: string]: boolean} options Optional map of decide options * @return {DecisionResponse} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource * properties and the decide reasons. If the user was not bucketed into a variation, the * variation property in decision object is null. diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index fecf84223..5f122bd83 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -420,14 +420,14 @@ DecisionService.prototype.__saveUserProfile = function(experiment, variation, us * experiment properties (both objects), as well as a decisionSource property. * decisionSource indicates whether the decision was due to a rollout or an * experiment. - * @param {Object} configObj The parsed project configuration object - * @param {Object} feature A feature flag object from project configuration - * @param {String} userId A string identifying the user, for bucketing - * @param {Object} attributes Optional user attributes - * @param {Object} options Map of decide options - * @return {Object} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource - * properties and decide reasons. If the user was not bucketed into a variation, the variation - * property in decision object is null. + * @param {Object} configObj The parsed project configuration object + * @param {Object} feature A feature flag object from project configuration + * @param {String} userId A string identifying the user, for bucketing + * @param {Object} attributes Optional user attributes + * @param {[key: string]: boolean} options Map of decide options + * @return {Object} DecisionResponse DecisionResponse containing an object with experiment, variation, and decisionSource + * properties and decide reasons. If the user was not bucketed into a variation, the variation + * property in decision object is null. */ DecisionService.prototype.getVariationForFeature = function(configObj, feature, userId, attributes, options = {}) { var decideReasons = []; @@ -465,7 +465,7 @@ DecisionService.prototype.getVariationForFeature = function(configObj, feature, }; -DecisionService.prototype._getVariationForFeatureExperiment = function(configObj, feature, userId, attributes, options) { +DecisionService.prototype._getVariationForFeatureExperiment = function(configObj, feature, userId, attributes, options = {}) { var decideReasons = []; var experiment = null; var variationKey = null; From 8896257c8ec56a35cc61dafcf26b42e537fd01ec Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 10:30:06 -0800 Subject: [PATCH 3/7] Clean up --- packages/optimizely-sdk/lib/core/decision_service/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts index 87b46865e..119f186eb 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.d.ts +++ b/packages/optimizely-sdk/lib/core/decision_service/index.d.ts @@ -52,7 +52,7 @@ export interface DecisionService { experimentKey: string, userId: string, attributes?: UserAttributes, - options?: { [key: string]: boolean }, + options?: { [key: string]: boolean } ): DecisionResponse; /** From 4b58a90560fff58bceecf89d543276a4341cf740 Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 13:42:22 -0800 Subject: [PATCH 4/7] Add unit test with IGNORE_USER_PROFILE_SERVICE in default decide options --- .../lib/core/decision_service/index.js | 2 +- .../lib/optimizely/index.tests.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index 5f122bd83..144f50a96 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -60,7 +60,7 @@ function DecisionService(options) { * @param {string} experimentKey * @param {string} userId * @param {Object} attributes - * @param {OptimizelyDecideOptions[]} options Decide options + * @param {[key: string]: boolean} options Optional map of decide options * @return {Object} DecisionResonse DecisionResonse containing the variation the user is bucketed into * and the decide reasons. */ diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index 6eada8dba..ec1f1555a 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -5665,6 +5665,46 @@ describe('lib/optimizely', function() { sinon.assert.notCalled(mockUserProfileServiceInstance.save); }); }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in default decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + var mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { // 'exp_no_audience' + variation_id: variationId2, + }, + }, + }), + save: sinon.stub() + }; + var optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ] + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId + }); + var decision = optlyInstanceWithUserProfile.decide(user, flagKey); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); + }); + }); }); describe('#decideForKeys', function() { From a098cec93c3b495acb02ff92989b4324aaa15a8a Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 15:03:30 -0800 Subject: [PATCH 5/7] Call __saveUserProfile only when shouldIgnoreUPS is true --- packages/optimizely-sdk/lib/core/decision_service/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.js b/packages/optimizely-sdk/lib/core/decision_service/index.js index 144f50a96..34bf80c8a 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.js @@ -180,8 +180,10 @@ DecisionService.prototype.getVariation = function(configObj, experimentKey, user ); this.logger.log(LOG_LEVEL.INFO, userInVariationLogMessage); decideReasons.push(userInVariationLogMessage); - // persist bucketing - this.__saveUserProfile(experiment, variation, userId, experimentBucketMap); + // persist bucketing if decide options do not include shouldIgnoreUPS + if (!shouldIgnoreUPS) { + this.__saveUserProfile(experiment, variation, userId, experimentBucketMap); + } return { result: variation.key, From 3032fdb81669a9b138b28b34ee8493030ee7ae78 Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 15:25:20 -0800 Subject: [PATCH 6/7] Clean up --- .../optimizely-sdk/lib/core/decision_service/index.tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js index eb742d8d0..5f61c11f0 100644 --- a/packages/optimizely-sdk/lib/core/decision_service/index.tests.js +++ b/packages/optimizely-sdk/lib/core/decision_service/index.tests.js @@ -155,8 +155,8 @@ describe('lib/core/decision_service', function() { var userProfileLookupStub; var userProfileSaveStub; var fakeDecisionWhitelistedVariation = { - getResult: sinon.stub().returns(null), - getReasons: sinon.stub().returns([]) + result: null, + reasons: [], } beforeEach(function() { userProfileServiceInstance = { From 396e0e2db17b88995fc71d320ae0a39eada12eab Mon Sep 17 00:00:00 2001 From: Polina Nguen Date: Wed, 13 Jan 2021 15:43:17 -0800 Subject: [PATCH 7/7] Add unit test when no stored bucketing exists --- .../lib/optimizely/index.tests.js | 149 +++++++++++------- 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/packages/optimizely-sdk/lib/optimizely/index.tests.js b/packages/optimizely-sdk/lib/optimizely/index.tests.js index ec1f1555a..0e85b0512 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.tests.js +++ b/packages/optimizely-sdk/lib/optimizely/index.tests.js @@ -5622,25 +5622,21 @@ describe('lib/optimizely', function() { }); }); - describe('with IGNORE_USER_PROFILE_SERVICE flag in decide options', function() { - it('should bypass user profile service', function() { + describe('when user profile service provided', function() { + var mockUserProfileServiceInstance; + var optlyInstanceWithUserProfile; + it('should bucket if there was no previously bucketed variation and save bucketing decision to the user profile', function() { var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' var variationId1 = '10418551353'; - var variationId2 = '10418510624'; var variationKey1 = 'variation_with_traffic'; - var variationKey2 = 'variation_no_traffic'; - var mockUserProfileServiceInstance = { + mockUserProfileServiceInstance = { lookup: sinon.stub().returns({ user_id: userId, - experiment_bucket_map: { - '10420810910': { // 'exp_no_audience' - variation_id: variationId2, - }, - }, + experiment_bucket_map: {}, }), save: sinon.stub() }; - var optlyInstanceWithUserProfile = new Optimizely({ + optlyInstanceWithUserProfile = new Optimizely({ clientEngine: 'node-sdk', datafile: testData.getTestDecideProjectConfig(), errorHandler: errorHandler, @@ -5656,53 +5652,94 @@ describe('lib/optimizely', function() { userId }); var decision1 = optlyInstanceWithUserProfile.decide(user, flagKey); - // should return variationId2 set by UPS - assert.equal(variationKey2, decision1.variationKey); - var decision2 = optlyInstanceWithUserProfile.decide(user, flagKey, [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ]); - // should ignore variationId2 set by UPS and return variationId1 - assert.equal(variationKey1, decision2.variationKey); - // also should not save either - sinon.assert.notCalled(mockUserProfileServiceInstance.save); - }); - }); - - describe('with IGNORE_USER_PROFILE_SERVICE flag in default decide options', function() { - it('should bypass user profile service', function() { - var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' - var variationId2 = '10418510624'; - var variationKey1 = 'variation_with_traffic'; - var mockUserProfileServiceInstance = { - lookup: sinon.stub().returns({ - user_id: userId, - experiment_bucket_map: { - '10420810910': { // 'exp_no_audience' - variation_id: variationId2, + // should return variationId1 as no stored variation exists + assert.equal(variationKey1, decision1.variationKey); + // also should call mockUserProfileServiceInstance.save to save bucketing decision + sinon.assert.calledOnce(mockUserProfileServiceInstance.save); + }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId1 = '10418551353'; + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + var variationKey2 = 'variation_no_traffic'; + mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { // 'exp_no_audience' + variation_id: variationId2, + }, }, - }, - }), - save: sinon.stub() - }; - var optlyInstanceWithUserProfile = new Optimizely({ - clientEngine: 'node-sdk', - datafile: testData.getTestDecideProjectConfig(), - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - userProfileService: mockUserProfileServiceInstance, - logger: createdLogger, - isValidInstance: true, - eventBatchSize: 1, - defaultDecideOptions: [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ] - }); - var user = new OptimizelyUserContext({ - optimizely: optlyInstanceWithUserProfile, - userId + }), + save: sinon.stub() + }; + optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId + }); + var decision1 = optlyInstanceWithUserProfile.decide(user, flagKey); + // should return variationId2 set by UPS + assert.equal(variationKey2, decision1.variationKey); + var decision2 = optlyInstanceWithUserProfile.decide(user, flagKey, [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ]); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision2.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); + }); + }); + + describe('with IGNORE_USER_PROFILE_SERVICE flag in default decide options', function() { + it('should bypass user profile service', function() { + var flagKey = 'feature_2'; // embedding experiment: 'exp_no_audience' + var variationId2 = '10418510624'; + var variationKey1 = 'variation_with_traffic'; + mockUserProfileServiceInstance = { + lookup: sinon.stub().returns({ + user_id: userId, + experiment_bucket_map: { + '10420810910': { // 'exp_no_audience' + variation_id: variationId2, + }, + }, + }), + save: sinon.stub() + }; + optlyInstanceWithUserProfile = new Optimizely({ + clientEngine: 'node-sdk', + datafile: testData.getTestDecideProjectConfig(), + errorHandler: errorHandler, + eventDispatcher: eventDispatcher, + jsonSchemaValidator: jsonSchemaValidator, + userProfileService: mockUserProfileServiceInstance, + logger: createdLogger, + isValidInstance: true, + eventBatchSize: 1, + defaultDecideOptions: [ OptimizelyDecideOptions.IGNORE_USER_PROFILE_SERVICE ] + }); + var user = new OptimizelyUserContext({ + optimizely: optlyInstanceWithUserProfile, + userId + }); + var decision = optlyInstanceWithUserProfile.decide(user, flagKey); + // should ignore variationId2 set by UPS and return variationId1 + assert.equal(variationKey1, decision.variationKey); + // also should not save either + sinon.assert.notCalled(mockUserProfileServiceInstance.save); }); - var decision = optlyInstanceWithUserProfile.decide(user, flagKey); - // should ignore variationId2 set by UPS and return variationId1 - assert.equal(variationKey1, decision.variationKey); - // also should not save either - sinon.assert.notCalled(mockUserProfileServiceInstance.save); }); }); });