@optimizely/optimizely-sdk
Version:
JavaScript SDK for Optimizely X Full Stack
1,364 lines (1,250 loc) • 344 kB
JavaScript
/****************************************************************************
* Copyright 2016-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 { assert } from 'chai';
import sinon from 'sinon';
import { sprintf } from '@optimizely/js-sdk-utils';
import eventProcessor from '../core/event_processor';
import * as logging from '@optimizely/js-sdk-logging';
import Optimizely from './';
import AudienceEvaluator from '../core/audience_evaluator';
import bluebird from 'bluebird';
import bucketer from '../core/bucketer';
import * as projectConfigManager from '../core/project_config/project_config_manager';
import * as enums from '../utils/enums';
import * as eventBuilder from '../core/event_builder';
import eventDispatcher from '../plugins/event_dispatcher/index.node';
import errorHandler from '../plugins/error_handler';
import fns from '../utils/fns';
import logger from '../plugins/logger';
import * as decisionService from '../core/decision_service';
import * as jsonSchemaValidator from '../utils/json_schema_validator';
import * as projectConfig from '../core/project_config';
import testData from '../tests/test_data';
var ERROR_MESSAGES = enums.ERROR_MESSAGES;
var LOG_LEVEL = enums.LOG_LEVEL;
var LOG_MESSAGES = enums.LOG_MESSAGES;
var DECISION_SOURCES = enums.DECISION_SOURCES;
var DECISION_NOTIFICATION_TYPES = enums.DECISION_NOTIFICATION_TYPES;
var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES;
describe('lib/optimizely', function() {
var ProjectConfigManagerStub;
var globalStubErrorHandler;
var stubLogHandler;
var clock;
beforeEach(function() {
logging.setLogLevel('notset');
stubLogHandler = {
log: sinon.stub(),
};
logging.setLogHandler(stubLogHandler);
globalStubErrorHandler = {
handleError: sinon.stub(),
};
logging.setErrorHandler(globalStubErrorHandler);
ProjectConfigManagerStub = sinon.stub(projectConfigManager, 'createProjectConfigManager').callsFake(function(config) {
var currentConfig = config.datafile ? projectConfig.createProjectConfig(config.datafile) : null;
return {
stop: sinon.stub(),
getConfig: sinon.stub().returns(currentConfig),
onUpdate: sinon.stub().returns(function() {}),
onReady: sinon.stub().returns({ then: function() {} }),
};
});
sinon.stub(eventDispatcher, 'dispatchEvent');
clock = sinon.useFakeTimers(new Date());
});
afterEach(function() {
ProjectConfigManagerStub.restore();
logging.resetErrorHandler();
logging.resetLogger();
eventDispatcher.dispatchEvent.restore();
clock.restore();
});
describe('constructor', function() {
var stubErrorHandler = { handleError: function() {} };
var stubEventDispatcher = {
dispatchEvent: function() {
return bluebird.resolve(null);
},
};
var createdLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO });
beforeEach(function() {
sinon.stub(stubErrorHandler, 'handleError');
sinon.stub(createdLogger, 'log');
});
afterEach(function() {
stubErrorHandler.handleError.restore();
createdLogger.log.restore();
});
describe('constructor', function() {
it('should construct an instance of the Optimizely class', function() {
var optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
});
assert.instanceOf(optlyInstance, Optimizely);
});
it('should construct an instance of the Optimizely class when datafile is JSON string', function() {
var optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: JSON.stringify(testData.getTestProjectConfig()),
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
});
assert.instanceOf(optlyInstance, Optimizely);
});
it('should log if the client engine passed in is invalid', function() {
new Optimizely({
datafile: testData.getTestProjectConfig(),
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
logger: createdLogger,
});
sinon.assert.called(createdLogger.log);
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_CLIENT_ENGINE, 'OPTIMIZELY', 'undefined'));
});
it('should allow passing `react-sdk` as the clientEngine', function() {
var instance = new Optimizely({
clientEngine: 'react-sdk',
datafile: testData.getTestProjectConfig(),
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
logger: createdLogger,
});
assert.strictEqual(instance.clientEngine, 'react-sdk');
});
describe('when a user profile service is provided', function() {
beforeEach(function() {
sinon.stub(decisionService, 'createDecisionService');
});
afterEach(function() {
decisionService.createDecisionService.restore();
});
it('should validate and pass the user profile service to the decision service', function() {
var userProfileServiceInstance = {
lookup: function() {},
save: function() {},
};
new Optimizely({
clientEngine: 'node-sdk',
logger: createdLogger,
datafile: testData.getTestProjectConfig(),
jsonSchemaValidator: jsonSchemaValidator,
userProfileService: userProfileServiceInstance,
});
sinon.assert.calledWith(decisionService.createDecisionService, {
userProfileService: userProfileServiceInstance,
logger: createdLogger,
UNSTABLE_conditionEvaluators: undefined,
});
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(logMessage, 'OPTIMIZELY: Valid user profile service provided.');
});
it('should pass in a null user profile to the decision service if the provided user profile is invalid', function() {
var invalidUserProfile = {
save: function() {},
};
new Optimizely({
clientEngine: 'node-sdk',
logger: createdLogger,
datafile: testData.getTestProjectConfig(),
jsonSchemaValidator: jsonSchemaValidator,
userProfileService: invalidUserProfile,
});
sinon.assert.calledWith(decisionService.createDecisionService, {
userProfileService: null,
logger: createdLogger,
UNSTABLE_conditionEvaluators: undefined,
});
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(
logMessage,
"USER_PROFILE_SERVICE_VALIDATOR: Provided user profile service instance is in an invalid format: Missing function 'lookup'."
);
});
});
describe('when an sdkKey is provided', function() {
it('should not log an error when sdkKey is provided and datafile is not provided', function() {
new Optimizely({
clientEngine: 'node-sdk',
eventBuilder: eventBuilder,
errorHandler: stubErrorHandler,
eventDispatcher: eventDispatcher,
isValidInstance: true,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
sdkKey: '12345',
});
sinon.assert.notCalled(stubErrorHandler.handleError);
});
it('passes datafile, datafileOptions, sdkKey, and other options to the project config manager', function() {
var config = testData.getTestProjectConfig();
new Optimizely({
clientEngine: 'node-sdk',
datafile: config,
datafileOptions: {
autoUpdate: true,
updateInterval: 2 * 60 * 1000,
},
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
isValidInstance: true,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
sdkKey: '12345',
});
sinon.assert.calledOnce(projectConfigManager.createProjectConfigManager);
sinon.assert.calledWithExactly(projectConfigManager.createProjectConfigManager, {
datafile: config,
datafileOptions: {
autoUpdate: true,
updateInterval: 2 * 60 * 1000,
},
jsonSchemaValidator: jsonSchemaValidator,
sdkKey: '12345',
});
});
});
it('should support constructing two instances using the same datafile object', function() {
var datafile = testData.getTypedAudiencesConfig();
var optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: datafile,
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
});
assert.instanceOf(optlyInstance, Optimizely);
var optlyInstance2 = new Optimizely({
clientEngine: 'node-sdk',
datafile: datafile,
errorHandler: stubErrorHandler,
eventDispatcher: stubEventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
});
assert.instanceOf(optlyInstance2, Optimizely);
});
});
});
describe('APIs', function() {
var optlyInstance;
var bucketStub;
var createdLogger = logger.createLogger({
logLevel: LOG_LEVEL.INFO,
logToConsole: false,
});
beforeEach(function() {
optlyInstance = new Optimizely({
clientEngine: 'node-sdk',
datafile: testData.getTestProjectConfig(),
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: createdLogger,
isValidInstance: true,
eventBatchSize: 1,
});
bucketStub = sinon.stub(bucketer, 'bucket');
sinon.stub(errorHandler, 'handleError');
sinon.stub(createdLogger, 'log');
sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c');
});
afterEach(function() {
bucketer.bucket.restore();
errorHandler.handleError.restore();
createdLogger.log.restore();
fns.uuid.restore();
});
describe('#activate', function() {
it('should call bucketer and dispatchEvent with proper args and return variation key', function() {
bucketStub.returns('111129');
var variation = optlyInstance.activate('testExperiment', 'testUser');
assert.strictEqual(variation, 'variation');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '4',
experiment_id: '111127',
variation_id: '111129',
metadata: {
flag_key: '',
rule_key: 'testExperiment',
rule_type: 'experiment',
variation_key: 'variation',
},
},
],
events: [
{
entity_id: '4',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should dispatch proper params for null value attributes', function() {
bucketStub.returns('122229');
var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', {
browser_type: 'firefox',
test_null_attribute: null,
});
assert.strictEqual(activate, 'variationWithAudience');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '5',
experiment_id: '122227',
variation_id: '122229',
metadata: {
flag_key: '',
rule_key: 'testExperimentWithAudiences',
rule_type: 'experiment',
variation_key: 'variationWithAudience',
},
},
],
events: [
{
entity_id: '5',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call bucketer and dispatchEvent with proper args and return variation key if user is in audience', function() {
bucketStub.returns('122229');
var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', { browser_type: 'firefox' });
assert.strictEqual(activate, 'variationWithAudience');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '5',
experiment_id: '122227',
variation_id: '122229',
metadata: {
flag_key: '',
rule_key: 'testExperimentWithAudiences',
rule_type: 'experiment',
variation_key: 'variationWithAudience',
},
},
],
events: [
{
entity_id: '5',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call activate and dispatchEvent with typed attributes and return variation key', function() {
bucketStub.returns('122229');
var activate = optlyInstance.activate('testExperimentWithAudiences', 'testUser', {
browser_type: 'firefox',
boolean_key: true,
integer_key: 10,
double_key: 3.14,
});
assert.strictEqual(activate, 'variationWithAudience');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '5',
experiment_id: '122227',
variation_id: '122229',
metadata: {
flag_key: '',
rule_key: 'testExperimentWithAudiences',
rule_type: 'experiment',
variation_key: 'variationWithAudience',
},
},
],
events: [
{
entity_id: '5',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
{
entity_id: '323434545',
key: 'boolean_key',
type: 'custom',
value: true,
},
{
entity_id: '616727838',
key: 'integer_key',
type: 'custom',
value: 10,
},
{
entity_id: '808797686',
key: 'double_key',
type: 'custom',
value: 3.14,
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
describe('when experiment_bucket_map attribute is present', function() {
it('should call activate and respect attribute experiment_bucket_map', function() {
bucketStub.returns('111128'); // id of "control" variation
var activate = optlyInstance.activate('testExperiment', 'testUser', {
$opt_experiment_bucket_map: {
'111127': {
variation_id: '111129', // id of "variation" variation
},
},
});
assert.strictEqual(activate, 'variation');
sinon.assert.notCalled(bucketer.bucket);
});
});
it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment', function() {
bucketStub.returns('662');
var activate = optlyInstance.activate('groupExperiment2', 'testUser');
assert.strictEqual(activate, 'var2exp2');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '2',
experiment_id: '443',
variation_id: '662',
metadata: {
flag_key: '',
rule_key: 'groupExperiment2',
rule_type: 'experiment',
variation_key: 'var2exp2',
},
},
],
events: [
{
entity_id: '2',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call bucketer and dispatchEvent with proper args and return variation key if user is in grouped experiment and is in audience', function() {
bucketStub.returns('552');
var activate = optlyInstance.activate('groupExperiment1', 'testUser', { browser_type: 'firefox' });
assert.strictEqual(activate, 'var2exp1');
sinon.assert.calledOnce(bucketer.bucket);
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '1',
experiment_id: '442',
variation_id: '552',
metadata: {
flag_key: '',
rule_key: 'groupExperiment1',
rule_type: 'experiment',
variation_key: 'var2exp1',
},
},
],
events: [
{
entity_id: '1',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should not make a dispatch event call if variation ID is null', function() {
bucketStub.returns(null);
assert.isNull(optlyInstance.activate('testExperiment', 'testUser'));
sinon.assert.notCalled(eventDispatcher.dispatchEvent);
sinon.assert.called(createdLogger.log);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.DEBUG,
sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'DECISION_SERVICE', 'testUser')
);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.INFO,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperiment')
);
});
it('should return null if user is not in audience and user is not in group', function() {
assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', { browser_type: 'chrome' }));
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.DEBUG,
sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'DECISION_SERVICE', 'testUser')
);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.INFO,
sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, 'DECISION_SERVICE', 'testUser', 'testExperimentWithAudiences')
);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.INFO,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentWithAudiences')
);
});
it('should return null if user is not in audience and user is in group', function() {
assert.isNull(optlyInstance.activate('groupExperiment1', 'testUser', { browser_type: 'chrome' }));
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.DEBUG,
sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'DECISION_SERVICE', 'testUser')
);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.INFO,
sprintf(LOG_MESSAGES.USER_NOT_IN_EXPERIMENT, 'DECISION_SERVICE', 'testUser', 'groupExperiment1')
);
sinon.assert.calledWithExactly(
createdLogger.log,
LOG_LEVEL.INFO,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'groupExperiment1')
);
});
it('should return null if experiment is not running', function() {
assert.isNull(optlyInstance.activate('testExperimentNotRunning', 'testUser'));
sinon.assert.calledTwice(createdLogger.log);
var logMessage1 = createdLogger.log.args[0][1];
assert.strictEqual(
logMessage1,
sprintf(LOG_MESSAGES.EXPERIMENT_NOT_RUNNING, 'DECISION_SERVICE', 'testExperimentNotRunning')
);
var logMessage2 = createdLogger.log.args[1][1];
assert.strictEqual(
logMessage2,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentNotRunning')
);
});
it('should throw an error for invalid user ID', function() {
assert.isNull(optlyInstance.activate('testExperiment', null));
sinon.assert.notCalled(eventDispatcher.dispatchEvent);
sinon.assert.calledOnce(errorHandler.handleError);
var errorMessage = errorHandler.handleError.lastCall.args[0].message;
assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
sinon.assert.calledTwice(createdLogger.log);
var logMessage1 = createdLogger.log.args[0][1];
assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_INPUT_FORMAT, 'OPTIMIZELY', 'user_id'));
var logMessage2 = createdLogger.log.args[1][1];
assert.strictEqual(
logMessage2,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'null', 'testExperiment')
);
});
it('should log an error for invalid experiment key', function() {
assert.isNull(optlyInstance.activate('invalidExperimentKey', 'testUser'));
sinon.assert.notCalled(eventDispatcher.dispatchEvent);
sinon.assert.calledTwice(createdLogger.log);
var logMessage1 = createdLogger.log.args[0][1];
assert.strictEqual(
logMessage1,
sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, 'OPTIMIZELY', 'invalidExperimentKey')
);
var logMessage2 = createdLogger.log.args[1][1];
assert.strictEqual(
logMessage2,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'invalidExperimentKey')
);
});
it('should throw an error for invalid attributes', function() {
assert.isNull(optlyInstance.activate('testExperimentWithAudiences', 'testUser', []));
sinon.assert.notCalled(eventDispatcher.dispatchEvent);
sinon.assert.calledOnce(errorHandler.handleError);
var errorMessage = errorHandler.handleError.lastCall.args[0].message;
assert.strictEqual(errorMessage, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR'));
sinon.assert.calledTwice(createdLogger.log);
var logMessage1 = createdLogger.log.args[0][1];
assert.strictEqual(logMessage1, sprintf(ERROR_MESSAGES.INVALID_ATTRIBUTES, 'ATTRIBUTES_VALIDATOR'));
var logMessage2 = createdLogger.log.args[1][1];
assert.strictEqual(
logMessage2,
sprintf(LOG_MESSAGES.NOT_ACTIVATING_USER, 'OPTIMIZELY', 'testUser', 'testExperimentWithAudiences')
);
});
it('should activate when logger is in DEBUG mode', function() {
bucketStub.returns('111129');
var instance = new Optimizely({
datafile: testData.getTestProjectConfig(),
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
jsonSchemaValidator: jsonSchemaValidator,
logger: logger.createLogger({
logLevel: enums.LOG_LEVEL.DEBUG,
logToConsole: false,
}),
isValidInstance: true,
eventBatchSize: 1,
});
var variation = instance.activate('testExperiment', 'testUser');
assert.strictEqual(variation, 'variation');
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
});
describe('whitelisting', function() {
beforeEach(function() {
sinon.spy(Optimizely.prototype, 'validateInputs');
});
afterEach(function() {
Optimizely.prototype.validateInputs.restore();
});
it('should return forced variation after experiment status check and before audience check', function() {
var activate = optlyInstance.activate('testExperiment', 'user1');
assert.strictEqual(activate, 'control');
sinon.assert.calledTwice(Optimizely.prototype.validateInputs);
var logMessage0 = createdLogger.log.args[0][1];
assert.strictEqual(
logMessage0,
sprintf(LOG_MESSAGES.USER_HAS_NO_FORCED_VARIATION, 'DECISION_SERVICE', 'user1')
);
var logMessage1 = createdLogger.log.args[1][1];
assert.strictEqual(
logMessage1,
sprintf(LOG_MESSAGES.USER_FORCED_IN_VARIATION, 'DECISION_SERVICE', 'user1', 'control')
);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
decisions: [
{
campaign_id: '4',
experiment_id: '111127',
variation_id: '111128',
},
],
events: [
{
entity_id: '4',
timestamp: Math.round(new Date().getTime()),
key: 'campaign_activated',
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
},
],
},
],
visitor_id: 'user1',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
});
});
it('should not activate when optimizely object is not a valid instance', function() {
var instance = new Optimizely({
datafile: {},
errorHandler: errorHandler,
eventDispatcher: eventDispatcher,
logger: createdLogger,
});
createdLogger.log.reset();
instance.activate('testExperiment', 'testUser');
sinon.assert.calledOnce(createdLogger.log);
var logMessage = createdLogger.log.args[0][1];
assert.strictEqual(logMessage, sprintf(LOG_MESSAGES.INVALID_OBJECT, 'OPTIMIZELY', 'activate'));
sinon.assert.notCalled(eventDispatcher.dispatchEvent);
});
});
describe('#track', function() {
it("should dispatch an event when no attributes are provided and the event's experiment is untargeted", function() {
optlyInstance.track('testEvent', 'testUser');
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111095',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEvent',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it("should dispatch an event when empty attributes are provided and the event's experiment is untargeted", function() {
optlyInstance.track('testEvent', 'testUser', {});
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111095',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEvent',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it("should dispatch an event when attributes are provided and the event's experiment is untargeted", function() {
optlyInstance.track('testEvent', 'testUser', { browser_type: 'safari' });
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111095',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEvent',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'safari',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it("should dispatch an event when no attributes are provided and the event's experiment is targeted", function() {
optlyInstance.track('testEventWithAudiences', 'testUser');
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111097',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEventWithAudiences',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it("should dispatch an event when empty attributes are provided and the event's experiment is targeted", function() {
optlyInstance.track('testEventWithAudiences', 'testUser');
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111097',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEventWithAudiences',
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call dispatchEvent with proper args when including null value attributes', function() {
optlyInstance.track('testEventWithAudiences', 'testUser', {
browser_type: 'firefox',
test_null_attribute: null,
});
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111097',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEventWithAudiences',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call dispatchEvent with proper args when including attributes', function() {
optlyInstance.track('testEventWithAudiences', 'testUser', { browser_type: 'firefox' });
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111097',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEventWithAudiences',
},
],
},
],
visitor_id: 'testUser',
attributes: [
{
entity_id: '111094',
key: 'browser_type',
type: 'custom',
value: 'firefox',
},
],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call bucketer and dispatchEvent with proper args when including event tags', function() {
optlyInstance.track('testEvent', 'testUser', undefined, { eventTag: 'chill' });
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111095',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEvent',
tags: {
eventTag: 'chill',
},
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call dispatchEvent with proper args when including event tags and revenue', function() {
optlyInstance.track('testEvent', 'testUser', undefined, { revenue: 4200, eventTag: 'chill' });
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',
visitors: [
{
snapshots: [
{
events: [
{
entity_id: '111095',
timestamp: Math.round(new Date().getTime()),
uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c',
key: 'testEvent',
revenue: 4200,
tags: {
revenue: 4200,
eventTag: 'chill',
},
},
],
},
],
visitor_id: 'testUser',
attributes: [],
},
],
revision: '42',
client_name: 'node-sdk',
client_version: enums.NODE_CLIENT_VERSION,
anonymize_ip: false,
enrich_decisions: true,
},
};
var eventDispatcherCall = eventDispatcher.dispatchEvent.args[0];
assert.deepEqual(eventDispatcherCall[0], expectedObj);
});
it('should call dispatchEvent with proper args when including event tags and null event tag values and revenue', function() {
optlyInstance.track('testEvent', 'testUser', undefined, {
revenue: 4200,
eventTag: 'chill',
testNullEventTag: null,
});
sinon.assert.calledOnce(eventDispatcher.dispatchEvent);
var expectedObj = {
url: 'https://logx.optimizely.com/v1/events',
httpVerb: 'POST',
params: {
account_id: '12001',
project_id: '111001',