UNPKG

@optimizely/optimizely-sdk

Version:
1,364 lines (1,250 loc) 344 kB
/**************************************************************************** * 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',