@optimizely/optimizely-sdk
Version:
JavaScript SDK for Optimizely X Full Stack
1,277 lines (1,155 loc) • 80 kB
JavaScript
/****************************************************************************
* Copyright 2017-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. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/
import sinon from 'sinon';
import { assert } from 'chai';
import cloneDeep from 'lodash/cloneDeep';
import { sprintf } from '@optimizely/js-sdk-utils';
import DecisionService from './';
import bucketer from '../bucketer';
import {
LOG_LEVEL,
LOG_MESSAGES,
DECISION_SOURCES,
} from '../../utils/enums';
import logger from '../../plugins/logger';
import Optimizely from '../../optimizely';
import projectConfig from '../project_config';
import AudienceEvaluator from '../audience_evaluator';
import errorHandler from '../../plugins/error_handler';
import eventBuilder from '../../core/event_builder/index.js';
import eventDispatcher from '../../plugins/event_dispatcher/index.node';
import * as jsonSchemaValidator from '../../utils/json_schema_validator';
import {
getTestProjectConfig,
getTestProjectConfigWithFeatures,
} from '../../tests/test_data';
var testData = getTestProjectConfig();
var testDataWithFeatures = getTestProjectConfigWithFeatures();
describe('lib/core/decision_service', function() {
describe('APIs', function() {
var configObj = projectConfig.createProjectConfig(cloneDeep(testData));
var decisionServiceInstance;
var mockLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO });
var bucketerStub;
beforeEach(function() {
bucketerStub = sinon.stub(bucketer, 'bucket');
sinon.stub(mockLogger, 'log');
decisionServiceInstance = DecisionService.createDecisionService({
logger: mockLogger,
});
});
afterEach(function() {
bucketer.bucket.restore();
mockLogger.log.restore();
});
describe('#getVariation', function() {
it('should return the correct variation for the given experiment key and user ID for a running experiment', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation from `test_data`
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledOnce(bucketerStub);
});
it('should return the whitelisted variation if the user is whitelisted', function() {
assert.strictEqual(
'variationWithAudience',
decisionServiceInstance.getVariation(configObj, 'testExperimentWithAudiences', 'user2')
);
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(2, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User user2 is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: User user2 is forced in variation variationWithAudience.'
);
});
it('should return null if the user does not meet audience conditions', function() {
assert.isNull(
decisionServiceInstance.getVariation(configObj, 'testExperimentWithAudiences', 'user3', { foo: 'bar' })
);
assert.strictEqual(4, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User user3 is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'
);
assert.strictEqual(
mockLogger.log.args[2][1],
'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'
);
assert.strictEqual(
mockLogger.log.args[3][1],
'DECISION_SERVICE: User user3 does not meet conditions to be in experiment testExperimentWithAudiences.'
);
});
it('should return null if the experiment is not running', function() {
assert.isNull(decisionServiceInstance.getVariation(configObj, 'testExperimentNotRunning', 'user1'));
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(1, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: Experiment testExperimentNotRunning is not running.'
);
});
describe('when attributes.$opt_experiment_bucket_map is supplied', function() {
it('should respect the sticky bucketing information for attributes', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation from `test_data`
var attributes = {
$opt_experiment_bucket_map: {
'111127': {
variation_id: '111129', // ID of the 'variation' variation
},
},
};
assert.strictEqual(
'variation',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user', attributes)
);
sinon.assert.notCalled(bucketerStub);
});
});
describe('when a user profile service is provided', function() {
var userProfileServiceInstance = null;
var userProfileLookupStub;
var userProfileSaveStub;
beforeEach(function() {
userProfileServiceInstance = {
lookup: function() {},
save: function() {},
};
decisionServiceInstance = DecisionService.createDecisionService({
logger: mockLogger,
userProfileService: userProfileServiceInstance,
});
userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup');
userProfileSaveStub = sinon.stub(userProfileServiceInstance, 'save');
sinon.stub(decisionServiceInstance, '__getWhitelistedVariation').returns(null);
});
afterEach(function() {
userProfileServiceInstance.lookup.restore();
userProfileServiceInstance.save.restore();
decisionServiceInstance.__getWhitelistedVariation.restore();
});
it('should return the previously bucketed variation', function() {
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128', // ID of the 'control' variation
},
},
});
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Returning previously activated variation "control" of experiment "testExperiment" for user "decision_service_user" from user profile.'
);
});
it('should bucket if there was no prevously bucketed variation', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {},
});
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub);
// make sure we save the decision
sinon.assert.calledWith(userProfileSaveStub, {
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128',
},
},
});
});
it('should bucket if the user profile service returns null', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.returns(null);
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub);
// make sure we save the decision
sinon.assert.calledWith(userProfileSaveStub, {
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128',
},
},
});
});
it('should re-bucket if the stored variation is no longer valid', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: 'not valid variation',
},
},
});
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: User decision_service_user was previously bucketed into variation with ID not valid variation for experiment testExperiment, but no matching variation was found.'
);
// make sure we save the decision
sinon.assert.calledWith(userProfileSaveStub, {
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128',
},
},
});
});
it('should store the bucketed variation for the user', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {}, // no decisions for user
});
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub);
assert.strictEqual(5, mockLogger.log.callCount);
sinon.assert.calledWith(userProfileServiceInstance.save, {
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128',
},
},
});
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[4][1],
'DECISION_SERVICE: Saved variation "control" of experiment "testExperiment" for user "decision_service_user".'
);
});
it('should log an error message if "lookup" throws an error', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.throws(new Error('I am an error'));
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Error while looking up user profile for user ID "decision_service_user": I am an error.'
);
});
it('should log an error message if "save" throws an error', function() {
bucketerStub.returns('111128'); // ID of the 'control' variation
userProfileLookupStub.returns(null);
userProfileSaveStub.throws(new Error('I am an error'));
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user')
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.calledOnce(bucketerStub); // should still go through with bucketing
assert.strictEqual(5, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[4][1],
'DECISION_SERVICE: Error while saving user profile for user ID "decision_service_user": I am an error.'
);
// make sure that we save the decision
sinon.assert.calledWith(userProfileSaveStub, {
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128',
},
},
});
});
describe('when passing `attributes.$opt_experiment_bucket_map`', function() {
it('should respect attributes over the userProfileService for the matching experiment id', function() {
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128', // ID of the 'control' variation
},
},
});
var attributes = {
$opt_experiment_bucket_map: {
'111127': {
variation_id: '111129', // ID of the 'variation' variation
},
},
};
assert.strictEqual(
'variation',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user', attributes)
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Returning previously activated variation "variation" of experiment "testExperiment" for user "decision_service_user" from user profile.'
);
});
it('should ignore attributes for a different experiment id', function() {
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {
'111127': {
// 'testExperiment' ID
variation_id: '111128', // ID of the 'control' variation
},
},
});
var attributes = {
$opt_experiment_bucket_map: {
'122227': {
// other experiment ID
variation_id: '122229', // ID of the 'variationWithAudience' variation
},
},
};
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user', attributes)
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Returning previously activated variation "control" of experiment "testExperiment" for user "decision_service_user" from user profile.'
);
});
it('should use attributes when the userProfileLookup variations for other experiments', function() {
userProfileLookupStub.returns({
user_id: 'decision_service_user',
experiment_bucket_map: {
'122227': {
// other experiment ID
variation_id: '122229', // ID of the 'variationWithAudience' variation
},
},
});
var attributes = {
$opt_experiment_bucket_map: {
'111127': {
// 'testExperiment' ID
variation_id: '111129', // ID of the 'variation' variation
},
},
};
assert.strictEqual(
'variation',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user', attributes)
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Returning previously activated variation "variation" of experiment "testExperiment" for user "decision_service_user" from user profile.'
);
});
it('should use attributes when the userProfileLookup returns null', function() {
userProfileLookupStub.returns(null);
var attributes = {
$opt_experiment_bucket_map: {
'111127': {
variation_id: '111129', // ID of the 'variation' variation
},
},
};
assert.strictEqual(
'variation',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'decision_service_user', attributes)
);
sinon.assert.calledWith(userProfileLookupStub, 'decision_service_user');
sinon.assert.notCalled(bucketerStub);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: User decision_service_user is not in the forced variation map.'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Returning previously activated variation "variation" of experiment "testExperiment" for user "decision_service_user" from user profile.'
);
});
});
});
});
describe('__buildBucketerParams', function() {
it('should return params object with correct properties', function() {
var bucketerParams = decisionServiceInstance.__buildBucketerParams(
configObj,
'testExperiment',
'testUser',
'testUser'
);
var expectedParams = {
bucketingId: 'testUser',
experimentKey: 'testExperiment',
userId: 'testUser',
experimentId: '111127',
trafficAllocationConfig: [
{
entityId: '111128',
endOfRange: 4000,
},
{
entityId: '111129',
endOfRange: 9000,
},
],
variationIdMap: configObj.variationIdMap,
logger: mockLogger,
experimentKeyMap: configObj.experimentKeyMap,
groupIdMap: configObj.groupIdMap,
};
assert.deepEqual(bucketerParams, expectedParams);
sinon.assert.notCalled(mockLogger.log);
});
});
describe('__checkIfExperimentIsActive', function() {
it('should return true if experiment is running', function() {
assert.isTrue(decisionServiceInstance.__checkIfExperimentIsActive(configObj, 'testExperiment'));
sinon.assert.notCalled(mockLogger.log);
});
it('should return false when experiment is not running', function() {
assert.isFalse(decisionServiceInstance.__checkIfExperimentIsActive(configObj, 'testExperimentNotRunning'));
sinon.assert.calledOnce(mockLogger.log);
var logMessage = mockLogger.log.args[0][1];
assert.strictEqual(
logMessage,
sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')
);
});
});
describe('__checkIfUserIsInAudience', function() {
var __audienceEvaluateSpy;
beforeEach(function() {
__audienceEvaluateSpy = sinon.spy(AudienceEvaluator.prototype, 'evaluate');
});
afterEach(function() {
__audienceEvaluateSpy.restore();
});
it('should return true when audience conditions are met', function() {
assert.isTrue(
decisionServiceInstance.__checkIfUserIsInAudience(
configObj,
'testExperimentWithAudiences',
"experiment",
'testUser',
{ browser_type: 'firefox' },
''
)
);
assert.strictEqual(2, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to TRUE.'
);
});
it('should return true when experiment has no audience', function() {
assert.isTrue(
decisionServiceInstance.__checkIfUserIsInAudience(
configObj,
'testExperiment',
"experiment",
'testUser',
{},
''
)
);
assert.isTrue(__audienceEvaluateSpy.alwaysReturned(true));
assert.strictEqual(2, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: Evaluating audiences for experiment "testExperiment": [].'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Audiences for experiment testExperiment collectively evaluated to TRUE.'
);
});
it('should return false when audience conditions can not be evaluated', function() {
assert.isFalse(
decisionServiceInstance.__checkIfUserIsInAudience(
configObj,
'testExperimentWithAudiences',
"experiment",
'testUser',
{},
''
)
);
assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false));
assert.strictEqual(2, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'
);
});
it('should return false when audience conditions are not met', function() {
assert.isFalse(
decisionServiceInstance.__checkIfUserIsInAudience(
configObj,
'testExperimentWithAudiences',
"experiment",
'testUser',
{ browser_type: 'chrome' },
''
)
);
assert.isTrue(__audienceEvaluateSpy.alwaysReturned(false));
assert.strictEqual(2, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: Evaluating audiences for experiment "testExperimentWithAudiences": ["11154"].'
);
assert.strictEqual(
mockLogger.log.args[1][1],
'DECISION_SERVICE: Audiences for experiment testExperimentWithAudiences collectively evaluated to FALSE.'
);
});
});
describe('__getWhitelistedVariation', function() {
it('should return forced variation ID if forced variation is provided for the user ID', function() {
var testExperiment = configObj.experimentKeyMap['testExperiment'];
var expectedVariation = configObj.variationIdMap['111128'];
assert.strictEqual(
decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'user1'),
expectedVariation
);
});
it('should return null if forced variation is not provided for the user ID', function() {
var testExperiment = configObj.experimentKeyMap['testExperiment'];
assert.isNull(decisionServiceInstance.__getWhitelistedVariation(testExperiment, 'notInForcedVariations'));
});
});
describe('getForcedVariation', function() {
it('should return null for valid experimentKey, not set', function() {
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
assert.strictEqual(variation, null);
});
it('should return null for invalid experimentKey, not set', function() {
var variation = decisionServiceInstance.getForcedVariation(configObj, 'definitely_not_valid_exp_key', 'user1');
assert.strictEqual(variation, null);
});
it('should return null for invalid experimentKey when a variation was previously successfully forced on another experiment for the same user', function() {
decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control');
var variation = decisionServiceInstance.getForcedVariation(configObj, 'definitely_not_valid_exp_key', 'user1');
assert.strictEqual(variation, null);
});
it('should return null for valid experiment key, not set on this experiment key, but set on another experiment key', function() {
decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control');
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, null);
});
});
describe('#setForcedVariation', function() {
it('should return true for a valid forcedVariation in setForcedVariation', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
});
it('should return the same variation from getVariation as was set in setVariation', function() {
decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', 'control');
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
assert.strictEqual(variation, 'control');
});
it('should not set for an invalid variation key', function() {
decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'definitely_not_valid_variation_key'
);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
assert.strictEqual(variation, null);
});
it('should reset the forcedVariation if passed null', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
assert.strictEqual(variation, 'control');
var didSetVariationAgain = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
null
);
assert.strictEqual(didSetVariationAgain, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
assert.strictEqual(variation, null);
});
it('should be able to add variations for multiple experiments for one user', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var didSetVariation2 = decisionServiceInstance.setForcedVariation(
configObj,
'testExperimentLaunched',
'user1',
'controlLaunched'
);
assert.strictEqual(didSetVariation2, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, 'control');
assert.strictEqual(variation2, 'controlLaunched');
});
it('should be able to add experiments for multiple users', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user2',
'variation'
);
assert.strictEqual(didSetVariation, true);
var variationControl = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variationVariation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user2');
assert.strictEqual(variationControl, 'control');
assert.strictEqual(variationVariation, 'variation');
});
it('should be able to reset a variation for a user with multiple experiments', function() {
//set the first time
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var didSetVariation2 = decisionServiceInstance.setForcedVariation(
configObj,
'testExperimentLaunched',
'user1',
'controlLaunched'
);
assert.strictEqual(didSetVariation2, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, 'control');
assert.strictEqual(variation2, 'controlLaunched');
//reset for one of the experiments
var didSetVariationAgain = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'variation'
);
assert.strictEqual(didSetVariationAgain, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, 'variation');
assert.strictEqual(variation2, 'controlLaunched');
});
it('should be able to unset a variation for a user with multiple experiments', function() {
//set the first time
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var didSetVariation2 = decisionServiceInstance.setForcedVariation(
configObj,
'testExperimentLaunched',
'user1',
'controlLaunched'
);
assert.strictEqual(didSetVariation2, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, 'control');
assert.strictEqual(variation2, 'controlLaunched');
//reset for one of the experiments
decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', null);
assert.strictEqual(didSetVariation, true);
var variation = decisionServiceInstance.getForcedVariation(configObj, 'testExperiment', 'user1');
var variation2 = decisionServiceInstance.getForcedVariation(configObj, 'testExperimentLaunched', 'user1');
assert.strictEqual(variation, null);
assert.strictEqual(variation2, 'controlLaunched');
});
it('should return false for an empty variation key', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(configObj, 'testExperiment', 'user1', '');
assert.strictEqual(didSetVariation, false);
});
it('should return null when a variation was previously set, and that variation no longer exists on the config object', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var newDatafile = cloneDeep(testData);
// Remove 'control' variation from variations, traffic allocation, and datafile forcedVariations.
newDatafile.experiments[0].variations = [
{
key: 'variation',
id: '111129',
},
];
newDatafile.experiments[0].trafficAllocation = [
{
entityId: '111129',
endOfRange: 9000,
},
];
newDatafile.experiments[0].forcedVariations = {
user1: 'variation',
user2: 'variation',
};
// Now the only variation in testExperiment is 'variation'
var newConfigObj = projectConfig.createProjectConfig(newDatafile);
var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1');
assert.strictEqual(forcedVar, null);
});
it("should return null when a variation was previously set, and that variation's experiment no longer exists on the config object", function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'testExperiment',
'user1',
'control'
);
assert.strictEqual(didSetVariation, true);
var newConfigObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures));
var forcedVar = decisionServiceInstance.getForcedVariation(newConfigObj, 'testExperiment', 'user1');
assert.strictEqual(forcedVar, null);
});
it('should return false from setForcedVariation and not set for invalid experiment key', function() {
var didSetVariation = decisionServiceInstance.setForcedVariation(
configObj,
'definitelyNotAValidExperimentKey',
'user1',
'definitely_not_valid_variation_key'
);
assert.strictEqual(didSetVariation, false);
var variation = decisionServiceInstance.getForcedVariation(
configObj,
'definitelyNotAValidExperimentKey',
'user1'
);
assert.strictEqual(variation, null);
});
});
});
// TODO: Move tests that test methods of Optimizely to lib/optimizely/index.tests.js
describe('when a bucketingID is provided', function() {
var configObj = projectConfig.createProjectConfig(cloneDeep(testData));
var createdLogger = logger.createLogger({
logLevel: LOG_LEVEL.DEBUG,
logToConsole: false,
});
var optlyInstance;
beforeEach(function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: cloneDeep(testData),
jsonSchemaValidator: jsonSchemaValidator,
isValidInstance: true,
logger: createdLogger,
eventBuilder: eventBuilder,
eventDispatcher: eventDispatcher,
errorHandler: errorHandler,
});
sinon.stub(eventDispatcher, 'dispatchEvent');
sinon.stub(errorHandler, 'handleError');
});
afterEach(function() {
eventDispatcher.dispatchEvent.restore();
errorHandler.handleError.restore();
});
var testUserAttributes = {
browser_type: 'firefox',
};
var userAttributesWithBucketingId = {
browser_type: 'firefox',
$opt_bucketing_id: '123456789',
};
var invalidUserAttributesWithBucketingId = {
browser_type: 'safari',
$opt_bucketing_id: 'testBucketingIdControl!',
};
it('confirm normal bucketing occurs before setting bucketingId', function() {
assert.strictEqual('variation', optlyInstance.getVariation('testExperiment', 'test_user', testUserAttributes));
});
it('confirm valid bucketing with bucketing ID set in attributes', function() {
assert.strictEqual(
'variationWithAudience',
optlyInstance.getVariation('testExperimentWithAudiences', 'test_user', userAttributesWithBucketingId)
);
});
it('check invalid audience with bucketingId', function() {
assert.strictEqual(
null,
optlyInstance.getVariation('testExperimentWithAudiences', 'test_user', invalidUserAttributesWithBucketingId)
);
});
it('test that an experiment that is not running returns a null variation', function() {
assert.strictEqual(
null,
optlyInstance.getVariation('testExperimentNotRunning', 'test_user', userAttributesWithBucketingId)
);
});
it('test that an invalid experiment key gets a null variation', function() {
assert.strictEqual(
null,
optlyInstance.getVariation('invalidExperiment', 'test_user', userAttributesWithBucketingId)
);
});
it('check forced variation', function() {
assert.isTrue(
optlyInstance.setForcedVariation('testExperiment', 'test_user', 'control'),
sprintf('Set variation to "%s" failed', 'control')
);
assert.strictEqual(
'control',
optlyInstance.getVariation('testExperiment', 'test_user', userAttributesWithBucketingId)
);
});
it('check whitelisted variation', function() {
assert.strictEqual(
'control',
optlyInstance.getVariation('testExperiment', 'user1', userAttributesWithBucketingId)
);
});
it('check user profile', function() {
var userProfileLookupStub;
var userProfileServiceInstance = {
lookup: function() {},
};
userProfileLookupStub = sinon.stub(userProfileServiceInstance, 'lookup');
userProfileLookupStub.returns({
user_id: 'test_user',
experiment_bucket_map: {
'111127': {
variation_id: '111128', // ID of the 'control' variation
},
},
});
var decisionServiceInstance = DecisionService.createDecisionService({
logger: createdLogger,
userProfileService: userProfileServiceInstance,
});
assert.strictEqual(
'control',
decisionServiceInstance.getVariation(configObj, 'testExperiment', 'test_user', userAttributesWithBucketingId)
);
sinon.assert.calledWithExactly(userProfileLookupStub, 'test_user');
});
});
describe('_getBucketingId', function() {
var configObj;
var decisionService;
var mockLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO });
var userId = 'testUser1';
var userAttributesWithBucketingId = {
browser_type: 'firefox',
$opt_bucketing_id: '123456789',
};
var userAttributesWithInvalidBucketingId = {
browser_type: 'safari',
$opt_bucketing_id: 50,
};
beforeEach(function() {
sinon.stub(mockLogger, 'log');
configObj = projectConfig.createProjectConfig(cloneDeep(testData));
decisionService = DecisionService.createDecisionService({
logger: mockLogger,
});
});
afterEach(function() {
mockLogger.log.restore();
});
it('should return userId if bucketingId is not defined in user attributes', function() {
assert.strictEqual(userId, decisionService._getBucketingId(userId, null));
assert.strictEqual(userId, decisionService._getBucketingId(userId, { browser_type: 'safari' }));
});
it('should log warning in case of invalid bucketingId', function() {
assert.strictEqual(userId, decisionService._getBucketingId(userId, userAttributesWithInvalidBucketingId));
assert.strictEqual(1, mockLogger.log.callCount);
assert.strictEqual(
mockLogger.log.args[0][1],
'DECISION_SERVICE: BucketingID attribute is not a string. Defaulted to userId'
);
});
it('should return correct bucketingId when provided in attributes', function() {
assert.strictEqual('123456789', decisionService._getBucketingId(userId, userAttributesWithBucketingId));
assert.strictEqual(1, mockLogger.log.callCount);
assert.strictEqual(mockLogger.log.args[0][1], 'DECISION_SERVICE: BucketingId is valid: "123456789"');
});
});
describe('feature management', function() {
describe('#getVariationForFeature', function() {
var configObj;
var decisionServiceInstance;
var sandbox;
var mockLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO });
beforeEach(function() {
configObj = projectConfig.createProjectConfig(cloneDeep(testDataWithFeatures));
sandbox = sinon.sandbox.create();
sandbox.stub(mockLogger, 'log');
decisionServiceInstance = DecisionService.createDecisionService({
logger: mockLogger,
});
});
afterEach(function() {
sandbox.restore();
});
describe('feature attached to an experiment, and not attached to a rollout', function() {
var feature;
beforeEach(function() {
feature = configObj.featureKeyMap.test_feature_for_experiment;
});
describe('user bucketed into this experiment', function() {
var getVariationStub;
beforeEach(function() {
getVariationStub = sandbox.stub(decisionServiceInstance, 'getVariation');
getVariationStub.returns(null);
getVariationStub.withArgs(configObj, 'testing_my_feature', 'user1').returns('variation');
});
it('returns a decision with a variation in the experiment the feature is attached to', function() {
var decision = decisionServiceInstance.getVariationForFeature(configObj, feature, 'user1', {
test_attribute: 'test_value',
});
var expectedDecision = {
experiment: {
forcedVariations: {},
status: 'Running',
key: 'testing_my_feature',
id: '594098',
variations: [
{
id: '594096',
variables: [
{
id: '4792309476491264',
value: '2',
},
{
id: '5073784453201920',
value: 'true',
},
{
id: '5636734406623232',
value: 'Buy me NOW',
},
{
id: '6199684360044544',
value: '20.25',
},
{
id: '1547854156498475',
value: '{ "num_buttons": 1, "text": "first variation"}',
},
],
featureEnabled: true,
key: 'variation',
},
{
id: '594097',
variables: [
{
id: '4792309476491264',
value: '10',
},
{
id: '5073784453201920',
value: 'false',
},
{
id: '5636734406623232',
value: 'Buy me',
},
{
id: '6199684360044544',
value: '50.55',
},
{
id: '1547854156498475',
value: '{ "num_buttons": 2, "text": "second variation"}',
},
],
featureEnabled: true,
key: 'control',
},
{
id: '594099',
variables: [
{
id: '4792309476491264',
value: '40',
},
{
id: '5073784453201920',
value: 'true',
},
{
id: '5636734406623232',
value: 'Buy me Later',
},
{
id: '6199684360044544',
value: '99.99',
},
{
id: '1547854156498475',
value: '{ "num_buttons": 3, "text": "third variation"}',
},
],
featureEnabled: false,
key: 'variation2',
},
],
audienceIds: [],
trafficAllocation: [
{ endOfRange: 5000, entityId: '594096' },
{ endOfRange: 10000, entityId: '594097' },
],
layerId: '594093',
variationKeyMap: {
control: {
id: '594097',
variables: [
{
id: '4792309476491264',
value: '10',
},
{
id: '5073784453201920',
value: 'false',
},
{
id: '5636734406623232',
value: 'Buy me',
},
{
id: '6199684360044544',
value: '50.55',
},
{
id: '1547854156498475',
value: '{ "num_buttons": 2, "text": "second variation"}',
},
],
featureEnabled: true,
key: 'control',
},
variation: {
id: '594096',
variables: [
{
id: '4792309476491264',
value: '2',
},
{
id: '5073784453201920',
value: 'true',
},
{
i