diff --git a/integrations/optimizely/HISTORY.md b/integrations/optimizely/HISTORY.md index ebdb0e517..cc53e4ad0 100644 --- a/integrations/optimizely/HISTORY.md +++ b/integrations/optimizely/HISTORY.md @@ -1,4 +1,13 @@ +4.0.0 / 2020-06-12 +================== + + * Don't attempt to interface with Optimizely Classic Web, since Classic is finally dead + * Prepare to support Optimizely Edge, an alternative to Optimizely Web + * Drop all references to customCampaignProperties. It seems to have been documented [here](https://segment.com/docs/connections/destinations/catalog/optimizely-web/#settings) but it couldn't possibly have worked. + * Generally refactor the code and tests. + * If there is an effective referrer for the current page load, include it when tracking future campaigns and not just when tracking current campaigns. + 3.5.0 / 2019-12-28 ================== diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 8c18d286d..6e626baae 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -22,9 +22,7 @@ var Optimizely = (module.exports = integration('Optimizely') .option('variations', false) // send data via `.identify()` .option('listen', true) // send data via `.track()` .option('nonInteraction', false) - .option('sendRevenueOnlyForOrderCompleted', true) - .option('customExperimentProperties', {}) - .option('customCampaignProperties', {})); + .option('sendRevenueOnlyForOrderCompleted', true)); /** * The name and version for this integration. @@ -38,9 +36,6 @@ var optimizelyContext = { /** * Initialize. * - * https://www.optimizely.com/docs/api#function-calls - * https://jsfiddle.net/ushmw723/ <- includes optimizely snippets for capturing campaign and experiment data - * * @api public */ @@ -51,38 +46,29 @@ Optimizely.prototype.initialize = function() { type: 'integration', OAuthClientId: '5360906403' }); - // Initialize listeners for both Classic and New Optimizely - // crazying binding because that's just how javascript works + // Initialize listeners for Optimizely Web decisions. // We're caling this on the next tick to be safe so we don't hold up // initializing the integration even though the function below is designed to be async, // just want to be extra safe tick(function() { - Optimizely.initOptimizelyIntegration({ - referrerOverride: self.setEffectiveReferrer.bind(self), - sendExperimentData: self.sendClassicDataToSegment.bind(self), - sendCampaignData: self.sendNewDataToSegment.bind(self) - }); + self.initWebIntegration(); }); this.ready(); }; /** - * Track. The Optimizely X Web event API accepts a single payload object. - * It works with Classic Optimizely as well. + * Track. * - * Optimizely X: https://developers.optimizely.com/x/solutions/javascript/reference/index.html#function_setevent + * If Optimizely Web or Optimizely Edge is implemented on the page, its JS API can accept a custom event. + * https://docs.developers.optimizely.com/web/docs/event + * https://docs.developers.optimizely.com/performance-edge/reference/event * - * The new-style X API is forward compatible from Optimizely Classic to Optimizely X. - * - Classic will correctly consume the tags object to identify the revenue - * - In bundled mode, it will be forwarded along to the X API with the entire payload - * - * If the Optimizely X Fullstack JavaScript SDK is being used we should pass along - * the event to it. Any properties in the track object will be passed along as event tags. + * If the Optimizely Full Stack JavaScript SDK is initialized on the page, its API can also accept a custom event. + * Any properties in the track object will be passed along as event tags. * If the userId is not passed into the options object of the track call, we'll * attempt to use the userId of the track event, which is set using the analytics.identify call. - * - * https://developers.optimizely.com/x/solutions/sdks/reference/?language=javascript#tracking + * https://docs.developers.optimizely.com/full-stack/docs/track-javascript * * @api public * @param {Track} track @@ -108,7 +94,6 @@ Optimizely.prototype.track = function(track) { eventProperties.revenue = Math.round(eventProperties.revenue * 100); } - // Use the new-style API (which is compatible with Classic and X) var eventName = track.event().replace(/:/g, '_'); // can't have colons so replacing with underscores var payload = { type: 'event', @@ -116,8 +101,10 @@ Optimizely.prototype.track = function(track) { tags: eventProperties }; + // Track via Optimizely Web push(payload); + // Track via Optimizely Full Stack var optimizelyClientInstance = window.optimizelyClientInstance; if (optimizelyClientInstance && optimizelyClientInstance.track) { var optimizelyOptions = track.options('Optimizely'); @@ -141,8 +128,6 @@ Optimizely.prototype.track = function(track) { /** * Page. * - * https://www.optimizely.com/docs/api#track-event - * * @api public * @param {Page} page */ @@ -164,145 +149,12 @@ Optimizely.prototype.page = function(page) { }; /** - * sendClassicDataToSegment (Optimizely Classic) - * - * This function is executed for each experiment created in Classic Optimizely that is running on the page. - * This function will also be executed for any experiments activated at a later stage since initOptimizelyIntegration - * attached listeners on the page - * - * @api private - * @param {Object} experimentState: contains all information regarding experiments - * @param {Object} experimentState.experiment: the experiment running on the page - * @param {String} experimentState.experiment.name: name of the experiment - * @param {String} experimentState.experiment.id: ID of the experiment - * @param {String} experimentState.experiment.referrer: available if effective referrer if experiment is a redirect - * @param {Array} experimentState.variations: the variations the current user on page is seeing - * @param {String} experimentState.variations[].name: the name of the variation - * @param {String} experimentState.variations[].id: the ID of the variation - * @param {Object} experimentState.sections: the sections for the experiment (only defined for multivariate experiments) keyed by sectionId - * @param {String} experimentState.sections[sectionId].name: the name of section - * @param {Array} experimentState.sections[sectionId].variation_ids: the IDs of the variations in the section - * - */ - -Optimizely.prototype.sendClassicDataToSegment = function(experimentState) { - var experiment = experimentState.experiment; - var variations = experimentState.variations; - var sections = experimentState.sections; - var context = { integration: optimizelyContext }; // backward compatibility - - // Reformatting this data structure into hash map so concatenating variation ids and names is easier later - var variationsMap = foldl( - function(results, variation) { - var res = results; - res[variation.id] = variation.name; - return res; - }, - {}, - variations - ); - - // Sorting for consistency across browsers - var variationIds = keys(variationsMap).sort(); - var variationNames = values(variationsMap).sort(); - - // Send data via `.track()` - if (this.options.listen) { - var props = { - experimentId: experiment.id, - experimentName: experiment.name, - variationId: variationIds.join(), // eg. '123' or '123,455' - variationName: variationNames.join(', ') // eg. 'Variation X' or 'Variation 1, Variation 2' - }; - - // If this was a redirect experiment and the effective referrer is different from document.referrer, - // this value is made available. So if a customer came in via google.com/ad -> tb12.com -> redirect experiment -> Belichickgoat.com - // `experiment.referrer` would be google.com/ad here NOT `tb12.com`. - if (experiment.referrer) { - props.referrer = experiment.referrer; - context.page = { referrer: experiment.referrer }; - } - - // When there is a multivariate experiment - if (sections) { - // Since `sections` include all the possible sections on the page, we need to find the names of the sections - // if any of its variations were used. Experiments could display variations from multiple sections. - // The global optimizely data object does not expose a mapping between which section(s) were involved within an experiment. - // So we will build our own mapping to quickly get the section name(s) and id(s) for any displayed variation. - var activeSections = {}; - var variationIdsToSectionsMap = foldl( - function(results, section, sectionId) { - var res = results; - each(function(variationId) { - res[variationId] = { id: sectionId, name: section.name }; - }, section.variation_ids); - return res; - }, - {}, - sections - ); - for (var j = 0; j < variationIds.length; j++) { - var activeVariation = variationIds[j]; - var activeSection = variationIdsToSectionsMap[activeVariation]; - if (activeSection) - activeSections[activeSection.id] = activeSection.name; - } - - // Sorting for consistency across browsers - props.sectionId = keys(activeSections) - .sort() - .join(); // Not adding space for backward compat/consistency reasons since all IDs we've never had spaces - props.sectionName = values(activeSections) - .sort() - .join(', '); - } - - // For Google's nonInteraction flag - if (this.options.nonInteraction) props.nonInteraction = 1; - - // If customExperimentProperties is provided overide the props with it. - // If valid customExperimentProperties present it will override existing props. - var customExperimentProperties = this.options.customExperimentProperties; - var customPropsKeys = Object.keys(customExperimentProperties); - var data = window.optimizely && window.optimizely.data; - - if (data && customPropsKeys.length) { - for (var index = 0; index < customPropsKeys.length; index++) { - var segmentProp = customPropsKeys[index]; - var optimizelyProp = customExperimentProperties[segmentProp]; - if (typeof data[optimizelyProp] !== 'undefined') { - props[segmentProp] = data[optimizelyProp]; - } - } - } - - // Send to Segment - this.analytics.track('Experiment Viewed', props, context); - } - - // Send data via `.identify()` (not recommended!) - // TODO: deprecate this feature - if (this.options.variations) { - // Note: The only "breaking" behavior is that now there will be an `.identify()` call per active experiment - // Legacy behavior was that we would look up all active experiments on the page after init and send one `.identify()` call - // with all experiment/variation data as traits. - // New behavior will call `.identify()` per active experiment with isolated experiment/variation data for that single experiment - // However, since traits are cached, subsequent experiments that trigger `.identify()` calls will likely contain previous experiment data - var traits = {}; - traits['Experiment: ' + experiment.name] = variationNames.join(', '); // eg. 'Variation X' or 'Variation 1, Variation 2' - - // Send to Segment - this.analytics.identify(traits); - } -}; - -/** - * sendNewDataToSegment (Optimizely X) + * sendWebDecisionToSegment (Optimizely X) * * This function is called for each experiment created in New Optimizely that are running on the page. * New Optimizely added a dimension called "Campaigns" that encapsulate over the Experiments. So a campaign can have multiple experiments. * Multivariate experiments are no longer supported in New Optimizely. - * This function will also be executed for any experiments activated at a later stage since initOptimizelyIntegration + * This function will also be executed for any experiments activated at a later stage since initWebIntegration * attached listeners on the page * * @api private @@ -321,8 +173,7 @@ Optimizely.prototype.sendClassicDataToSegment = function(experimentState) { * @param {String} campaignState.variation.name: the name of the variation * @param {String} campaignState.isInCampaignHoldback: whether the visitor is in the Campaign holdback */ - -Optimizely.prototype.sendNewDataToSegment = function(campaignState) { +Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { var experiment = campaignState.experiment; var variation = campaignState.variation; var context = { integration: optimizelyContext }; // backward compatibility @@ -360,32 +211,17 @@ Optimizely.prototype.sendNewDataToSegment = function(campaignState) { isInCampaignHoldback: campaignState.isInCampaignHoldback }; - // If this was a redirect experiment and the effective referrer is different from document.referrer, - // this value is made available. So if a customer came in via google.com/ad -> tb12.com -> redirect experiment -> Belichickgoat.com - // `experiment.referrer` would be google.com/ad here NOT `tb12.com`. - if (experiment.referrer) { - props.referrer = experiment.referrer; - context.page = { referrer: experiment.referrer }; + if (this.redirectInfo) { + // Legacy. It's more accurate to use context.page.referrer or window.optimizelyEffectiveReferrer. + // TODO: Maybe only set this if experiment.id matches this.redirectInfo.experimentId? + props.referrer = this.redirectInfo.referrer; + + context.page = { referrer: this.redirectInfo.referrer }; } // For Google's nonInteraction flag if (this.options.nonInteraction) props.nonInteraction = 1; - // If customCampaignProperties is provided overide the props with it. - // If valid customCampaignProperties present it will override existing props. - var customCampaignProperties = this.options.customCampaignProperties; - var customPropsKeys = Object.keys(customCampaignProperties); - var data = window.optimizely && window.optimizely.newMockData; - if (data && customPropsKeys.length) { - for (var index = 0; index < customPropsKeys.length; index++) { - var segmentProp = customPropsKeys[index]; - var optimizelyProp = customCampaignProperties[segmentProp]; - if (typeof data[optimizelyProp] !== 'undefined') { - props[segmentProp] = data[optimizelyProp]; - } - } - } - // Send to Segment this.analytics.track('Experiment Viewed', props, context); } @@ -407,7 +243,7 @@ Optimizely.prototype.sendNewDataToSegment = function(campaignState) { }; /** - * setEffectiveReferrer + * setRedirectInfo * * This function is called when a redirect experiment changed the effective referrer value where it is different from the `document.referrer`. * This is a documented caveat for any mutual customers that are using redirect experiments. @@ -415,216 +251,99 @@ Optimizely.prototype.sendNewDataToSegment = function(campaignState) { * their Segment snippet. * * @apr private - * @param {string} referrer + * @param {Object?} redirectInfo + * @param {String} redirectInfo.experimentId + * @param {String} redirectInfo.variationId + * @param {String} redirectInfo.referrer */ - -Optimizely.prototype.setEffectiveReferrer = function(referrer) { - if (referrer) { - window.optimizelyEffectiveReferrer = referrer; - return referrer; +Optimizely.prototype.setRedirectInfo = function(redirectInfo) { + if (redirectInfo) { + this.redirectInfo = redirectInfo; + window.optimizelyEffectiveReferrer = redirectInfo.referrer; } }; /** - * initOptimizelyIntegration(handlers) - * - * This function was provided by Optimizely's Engineering team. The function below once initialized can detect which version of - * Optimizely a customer is using and call the appropriate callback functions when an experiment runs on the page. - * Instead of Segment looking up the experiment data, we can now just bind Segment APIs to their experiment listener/handlers! + * This function fetches all active Optimizely Web campaigns and experiments, + * invoking the sendWebDecisionToSegment callback for each one. * * @api private - * @param {Object} handlers - * @param {Function} referrerOverride: called if the effective refferer value differs from the current `document.referrer` due to a - * invocation of a redirect experiment on the page - * @param {Function} sendExperimentData: called for every running experiment on the page (Classic) - * @param {Function} sendCampaignData: called for every running campaign on the page (New) */ +Optimizely.prototype.initWebIntegration = function() { + var self = this; -Optimizely.initOptimizelyIntegration = function(handlers) { /** - * `initClassicOptimizelyIntegration` fetches all the experiment data from the classic Optimizely client - * and calls the functions provided in the arguments with the data that needs to - * be used for sending information. It is recommended to leave this function as is - * and to create your own implementation of the functions referrerOverride and - * sendExperimentData. - * - * @param {Function} referrerOverride - This function is called if the effective referrer value differs from - * the current document.referrer value. The only argument provided is the effective referrer value. - * @param {Function} sendExperimentData - This function is called for every running experiment on the page. - * The function is called with all the relevant ids and names. + * If the visitor got to the current page via an Optimizely redirect variation, + * record the "effective referrer", i.e. the URL that referred the visitor to the + * _pre-redirect_ page. */ - var initClassicOptimizelyIntegration = function( - referrerOverride, - sendExperimentData - ) { - var data = window.optimizely && window.optimizely.data; - var state = data && data.state; + var checkReferrer = function() { + var state = window.optimizely.get && window.optimizely.get('state'); if (state) { - var activeExperiments = state.activeExperiments; - if (state.redirectExperiment) { - var redirectExperimentId = state.redirectExperiment.experimentId; - var index = -1; - for (var i = 0; i < state.activeExperiments.length; i++) { - if (state.activeExperiments[i] === redirectExperimentId) { - index = i; - break; + self.setRedirectInfo(state.getRedirectInfo()); + } else { + push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + state = window.optimizely.get && window.optimizely.get('state'); + if (state) { + self.setRedirectInfo(state.getRedirectInfo()); } } - if (index === -1) { - activeExperiments.push(redirectExperimentId); - } - referrerOverride(state.redirectExperiment.referrer); - } - - for (var k = 0; k < activeExperiments.length; k++) { - var currentExperimentId = activeExperiments[k]; - var activeExperimentState = { - experiment: { - id: currentExperimentId, - name: data.experiments[currentExperimentId].name - }, - variations: [], - /** Segment added code */ - // we need to send sectionName for multivariate experiments - sections: data.sections - /**/ - }; - - /** Segment added code */ - // for backward compatability since we send referrer with the experiment properties - if ( - state.redirectExperiment && - currentExperimentId === state.redirectExperiment.experimentId && - state.redirectExperiment.referrer - ) { - activeExperimentState.experiment.referrer = - state.redirectExperiment.referrer; - } - /**/ - - var variationIds = - state.variationIdsMap[activeExperimentState.experiment.id]; - for (var j = 0; j < variationIds.length; j++) { - var id = variationIds[j]; - var name = data.variations[id].name; - activeExperimentState.variations.push({ - id: id, - name: name - }); - } - sendExperimentData(activeExperimentState); - } + }); } }; /** - * This function fetches all the campaign data from the new Optimizely client - * and calls the functions provided in the arguments with the data that needs to - * be used for sending information. It is recommended to leave this function as is - * and to create your own implementation of the functions referrerOverride and - * sendCampaignData. - * - * @param {Function} referrerOverride - This function is called if the effective referrer value differs from - * the current document.referrer value. The only argument provided is the effective referrer value. - * @param {Function} sendCampaignData - This function is called for every running campaign on the page. - * The function is called with the campaignState for the activated campaign + * A campaign or experiment can be activated after we have initialized. + * This function registers a listener that listens to newly activated campaigns and + * handles them. */ - var initNewOptimizelyIntegration = function( - referrerOverride, - sendCampaignData - ) { - var newActiveCampaign = function(id, referrer) { - var state = window.optimizely.get && window.optimizely.get('state'); - if (state) { - var activeCampaigns = state.getCampaignStates({ - isActive: true - }); - var campaignState = activeCampaigns[id]; - // Segment added code: in case this is a redirect experiment - if (referrer) campaignState.experiment.referrer = referrer; - sendCampaignData(campaignState); - } - }; - - var checkReferrer = function() { - var state = window.optimizely.get && window.optimizely.get('state'); - if (state) { - var referrer = - state.getRedirectInfo() && state.getRedirectInfo().referrer; - - if (referrer) { - referrerOverride(referrer); - return referrer; // Segment added code: so I can pass this referrer value in cb + var registerFutureActiveCampaigns = function() { + push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: function(event) { + var campaignId = event.data.campaign.id; + var state = window.optimizely.get && window.optimizely.get('state'); + if (!state) { + return; + } + // Make sure the campaign actually activated (rather than producing a null decision) + var activeCampaigns = state.getCampaignStates({ isActive: true }); + if (!activeCampaigns[campaignId]) { + return; } + self.sendWebDecisionToSegment(activeCampaigns[campaignId]); } - }; + }); + }; - /** - * At any moment, a new campaign can be activated (manual or conditional activation). - * This function registers a listener that listens to newly activated campaigns and - * handles them. - */ - var registerFutureActiveCampaigns = function() { - window.optimizely = window.optimizely || []; - window.optimizely.push({ - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'campaignDecided' - }, - handler: function(event) { - var id = event.data.campaign.id; - newActiveCampaign(id); - } + /** + * If this code is running after Optimizely on the page, there might already be + * some campaigns or experiments active. This function retrieves and handlers them. + */ + var registerCurrentlyActiveCampaigns = function() { + window.optimizely = window.optimizely || []; + var state = window.optimizely.get && window.optimizely.get('state'); + if (state) { + var activeCampaigns = state.getCampaignStates({ + isActive: true }); - }; - - /** - * If this code is running after Optimizely on the page, there might already be - * some campaigns active. This function makes sure all those campaigns are - * handled. - */ - var registerCurrentlyActiveCampaigns = function() { - window.optimizely = window.optimizely || []; - var state = window.optimizely.get && window.optimizely.get('state'); - if (state) { - var referrer = checkReferrer(); - var activeCampaigns = state.getCampaignStates({ - isActive: true - }); - for (var id in activeCampaigns) { - if ({}.hasOwnProperty.call(activeCampaigns, id)) { - // Segment modified code: need to pass down referrer in the cb for backward compat reasons - if (referrer) { - newActiveCampaign(id, referrer); - } else { - newActiveCampaign(id); - } - } - } - } else { - window.optimizely.push({ - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'initialized' - }, - handler: function() { - checkReferrer(); - } - }); - } - }; - registerCurrentlyActiveCampaigns(); - registerFutureActiveCampaigns(); + each(function(campaignState) { + self.sendWebDecisionToSegment(campaignState); + }, activeCampaigns); + } }; - initClassicOptimizelyIntegration( - handlers.referrerOverride, - handlers.sendExperimentData - ); - initNewOptimizelyIntegration( - handlers.referrerOverride, - handlers.sendCampaignData - ); + checkReferrer(); + registerCurrentlyActiveCampaigns(); + registerFutureActiveCampaigns(); }; diff --git a/integrations/optimizely/package.json b/integrations/optimizely/package.json index 128b01bed..12e554a49 100644 --- a/integrations/optimizely/package.json +++ b/integrations/optimizely/package.json @@ -37,6 +37,7 @@ "@segment/analytics.js-integration-tester": "^3.1.1", "@segment/clear-env": "^2.1.1", "browserify": "^16.2.3", + "chai": "^4.2.0", "eslint": "^5.16.0", "karma": "^4.1.0", "karma-browserify": "^6.0.0", @@ -46,7 +47,9 @@ "karma-sauce-launcher": "^2.0.2", "karma-spec-reporter": "^0.0.32", "karma-summary-reporter": "^1.6.0", + "lodash": "^4.17.15", "mocha": "^6.1.4", + "sinon": "^9.0.2", "watchify": "^3.11.1" } } diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 81253a299..54670f4a3 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -1,167 +1,145 @@ 'use strict'; +var _ = require('lodash'); var Analytics = require('@segment/analytics.js-core').constructor; +var assert = require('chai').assert; var sandbox = require('@segment/clear-env'); +var sinon = require('sinon'); var tester = require('@segment/analytics.js-integration-tester'); var Optimizely = require('../lib/'); var tick = require('next-tick'); +/* + * execute AsyncTest + * + * Prevent tests from hanging if deepEqual fails inside `tick` + * @api private + * @param {Function} done cb + * @param {Function} function that runs test + */ +function executeAsyncTest(done, test) { + tick(function() { + try { + test(); + done(); + } catch (e) { + done(e); + } + }); +} + /** * Test account: han@segment.com - * - * Docs for Optimizely data object: https://developers.optimizely.com/javascript/personalization/index.html#reading-data-and-state */ -var mockOptimizelyClassicDataObject = function() { - // Classic - window.optimizely.data = { - experiments: { - 0: { name: 'Test' }, - 1: { name: 'MultiVariate Test' }, - 2: { name: 'Inactive Test' }, - 11: { name: 'Redirect Test' } - }, - variations: { - 22: { name: 'Redirect Variation', code: '' }, - 123: { name: 'Variation #123', code: '' }, - 789: { name: 'Var 789', code: '' }, - 44: { name: 'Var 44', code: '' } - }, - sections: undefined, // we'll set this during multivariate test since that's when this is set by Optimizely's API - state: { - activeExperiments: ['0', '11'], - variationNamesMap: { - 0: 'Variation #123', - 1: 'Variation #123, Redirect Variation, Var 789', // this is the data format - 2: 'Inactive Variation', - 11: 'Redirect Variation' +var mockWindowOptimizely = function() { + window.optimizely = { + newMockData: { + 2347102720: { + audiences: [ + { + name: 'Middle Class', + id: '7100568438' + } + ], + campaignName: 'Get Rich or Die Tryin', + id: '2347102720', + experiment: { + id: '7522212694', + name: 'Wells Fargo Scam' + }, + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + }, + isInCampaignHoldback: false, + // these are returned by real Optimizely API but will not be send to integrations + isActive: false, + reason: undefined, + visitorRedirected: true }, - variationIdsMap: { - 0: ['123'], - 1: ['123', '22', '789'], - 11: ['22'], - 2: ['44'] + 7547101713: { + audiences: [ + { + name: 'Trust Tree', + id: '7527565438' + } + ], + campaignName: 'URF', + id: '7547101713', + experiment: { + id: '7547682694', + name: 'Worlds Group Stage' + }, + variation: { + id: '7557950020', + name: 'Variation #1' + }, + isInCampaignHoldback: true, + // these are returned by real Optimizely API but will not be send to integrations + isActive: true, + reason: undefined, + visitorRedirected: false }, - redirectExperiment: { - variationId: '22', - experimentId: '11', - referrer: 'google.com' + 2542102702: { + audiences: [ + { + name: 'Penthouse 6', + id: '8888222438' + }, + { + name: 'Fam Yolo', + id: '1234567890' + } + ], + campaignName: 'Coding Bootcamp', // Experiments created in Optimizely X will set this the same as experiment name + id: '7222777766', // but this will be different than experiment id + experiment: { + id: '1111182111', + name: 'Coding Bootcamp' + }, + variation: { + id: '7333333333', + name: 'Variation DBC' + }, + isInCampaignHoldback: false, + // these are returned by real Optimizely API but will not be send to integrations + isActive: true, + reason: undefined, + visitorRedirected: false } - } - }; -}; - -// Optimizely X -var mockOptimizelyXDataObject = function() { - // remove Classic data object - delete window.optimizely.data; - - window.optimizely.newMockData = { - 2347102720: { - audiences: [ - { - name: 'Middle Class', - id: '7100568438' - } - ], - campaignName: 'Get Rich or Die Tryin', - id: '2347102720', - experiment: { - id: '7522212694', - name: 'Wells Fargo Scam' - }, - variation: { - id: '7551111120', - name: 'Variation Corruption #1884' - }, - isInCampaignHoldback: false, - // these are returned by real Optimizely API but will not be send to integrations - isActive: false, - reason: undefined, - visitorRedirected: true - }, - 7547101713: { - audiences: [ - { - name: 'Trust Tree', - id: '7527565438' - } - ], - campaignName: 'URF', - id: '7547101713', - experiment: { - id: '7547682694', - name: 'Worlds Group Stage' - }, - variation: { - id: '7557950020', - name: 'Variation #1' - }, - isInCampaignHoldback: true, - // these are returned by real Optimizely API but will not be send to integrations - isActive: true, - reason: undefined, - visitorRedirected: false }, - 2542102702: { - audiences: [ - { - name: 'Penthouse 6', - id: '8888222438' + + get: function() { + return { + getCampaignStates: function(options) { + if (!options.isActive) { + throw new Error('Incorrect call to getCampaignStates'); + } + return _.pickBy(window.optimizely.newMockData, { + isActive: options.isActive + }); }, - { - name: 'Fam Yolo', - id: '1234567890' - } - ], - campaignName: 'Coding Bootcamp', // Experiments created in Optimizely X will set this the same as experiment name - id: '7222777766', // but this will be different than experiment id - experiment: { - id: '1111182111', - name: 'Coding Bootcamp' - }, - variation: { - id: '7333333333', - name: 'Variation DBC' - }, - isInCampaignHoldback: false, - // these are returned by real Optimizely API but will not be send to integrations - isActive: true, - reason: undefined, - visitorRedirected: false - } - }; - // Optimizely init snippet uses new API methods below to access data rather than the global optimizely.data object - window.optimizely.get = function() { - return { - getCampaignStates: function(options) { - if (!('isActive' in options)) return window.optimizely.newMockData; - // returns all campaigns with option to return just active ones (which is what we do in the snippet) - var ret = {}; - for (var campaign in window.optimizely.newMockData) { - if ( - window.optimizely.newMockData[campaign].isActive === - options.isActive - ) { - ret[campaign] = window.optimizely.newMockData[campaign]; + getRedirectInfo: function() { + var campaigns = this.getCampaignStates({ isActive: true }); + var campaignIds = Object.keys(campaigns); + for (var i = 0; i < campaignIds.length; i++) { + var id = campaignIds[i]; + if (campaigns[id].visitorRedirected) { + return { + experimentId: campaigns[id].experiment.id, + variationId: campaigns[id].variation.id, + referrer: 'barstools.com' + }; + } } + return null; } - return ret; - }, - getRedirectInfo: function() { - var campaigns = this.getCampaignStates({ isActive: true }); - for (var id in campaigns) { - if (campaigns[id].visitorRedirected) - return { referrer: 'barstools.com' }; - } - } - }; - }; -}; + }; + }, -var mockBothOptimizelyDataObjects = function() { - mockOptimizelyXDataObject(); - mockOptimizelyClassicDataObject(); + push: sinon.stub() + }; }; // passed into context.integration (not context.integrations!) for all track calls for some reason @@ -171,6 +149,8 @@ var optimizelyContext = { }; describe('Optimizely', function() { + this.timeout(0); + var analytics; var optimizely; var options = { @@ -195,606 +175,429 @@ describe('Optimizely', function() { analytics.reset(); optimizely.reset(); sandbox(); + sinon.restore(); }); - describe('before loading', function() { - beforeEach(function() { - analytics.stub( - Optimizely, - 'initOptimizelyIntegration', - Optimizely.initOptimizelyIntegration - ); // Reference to constructor intentionally - analytics.stub(optimizely, 'load'); - analytics.stub(optimizely, 'sendClassicDataToSegment'); - analytics.stub(optimizely, 'sendNewDataToSegment'); - analytics.stub(optimizely, 'setEffectiveReferrer'); + describe('#initialize', function() { + beforeEach(function(done) { + sinon.stub(Optimizely.prototype, 'initWebIntegration'); + sinon.stub(window.optimizely, 'push'); + analytics.once('ready', done); + analytics.initialize(); + analytics.page(); }); - describe('#initialize', function() { - beforeEach(function(done) { - analytics.stub(window.optimizely, 'push'); - analytics.once('ready', done); - analytics.initialize(); - analytics.page(); + it('should call initWebIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledWith(optimizely.initWebIntegration); }); + }); - it('should call initOptimizelyIntegration', function(done) { - executeAsyncTest(done, function() { - analytics.called(Optimizely.initOptimizelyIntegration); - }); + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizely.push, { + type: 'integration', + OAuthClientId: '5360906403' }); + }); + }); - it('should flag source of integration', function() { - analytics.called(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); + describe('#setRedirectInfo', function() { + beforeEach(function(done) { + analytics.initialize(); + tick(done); + }); + + context('when no redirect info was captured', function() { + beforeEach(function() { + optimizely.setRedirectInfo(null); + }); + + it('does not set redirect info', function() { + assert.isUndefined(optimizely.redirectInfo); + assert.isUndefined(window.optimizelyEffectiveReferrer); }); }); - describe('#initOptimizelyIntegration', function() { - // Testing the behavior of the Optimizely provided private init function - // to ensure that proper callback functions were executed with expected params - // given each of the possible Optimizely snippet you could have on the page (Classic, X, Both) - describe('Classic', function() { - beforeEach(function(done) { - mockOptimizelyClassicDataObject(); - analytics.initialize(); - tick(done); + context('when redirect info was captured', function() { + beforeEach(function() { + optimizely.setRedirectInfo({ + experimentId: 'x', + variationId: 'v', + referrer: 'r' }); + }); - it('should call setEffectiveReferrer for redirect experiments', function() { - analytics.called(optimizely.setEffectiveReferrer, 'google.com'); + it('sets redirect info', function() { + assert.deepEqual(optimizely.redirectInfo, { + experimentId: 'x', + variationId: 'v', + referrer: 'r' }); + assert.equal(window.optimizelyEffectiveReferrer, 'r'); + }); + }); + }); - it('should call sendClassicDataToSegment for active Classic experiments', function() { - // we have two active experiments running in the mock data object - analytics.calledTwice(optimizely.sendClassicDataToSegment); - analytics.deepEqual(optimizely.sendClassicDataToSegment.args[0], [ - { - experiment: { - id: '0', - name: 'Test' - }, - variations: [ - { - id: '123', - name: 'Variation #123' - } - ], - sections: undefined - } - ]); - analytics.deepEqual(optimizely.sendClassicDataToSegment.args[1], [ - { - experiment: { - id: '11', - name: 'Redirect Test', - referrer: 'google.com' - }, - variations: [ - { - id: '22', - name: 'Redirect Variation' - } - ], - sections: undefined - } - ]); - }); + describe('#initWebIntegration', function() { + beforeEach(function() { + sinon.stub(optimizely, 'sendWebDecisionToSegment'); + sinon.stub(optimizely, 'setRedirectInfo'); + }); + + context('before the Optimizely snippet has loaded', function() { + var prePushStub; + + beforeEach(function() { + prePushStub = sinon.stub(); + window.optimizely = { + push: prePushStub + }; + + optimizely.initWebIntegration(); }); - describe('New', function() { + context('if a redirect experiment has executed', function() { beforeEach(function() { - mockOptimizelyXDataObject(); + mockWindowOptimizely(); + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; }); - it('should not call setEffectiveReferrer for non redirect experiments', function(done) { - // by default mock data has no redirect experiments active - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.didNotCall(optimizely.setEffectiveReferrer); + it('eventually captures redirect info', function() { + sinon.assert.notCalled(optimizely.setRedirectInfo); + + var initializedCalls = _.filter(prePushStub.getCalls(), { + args: [ + { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + } + } + ] + }); + assert.equal(initializedCalls.length, 1); + // Actually simulate an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', + referrer: 'barstools.com' }); }); + }); - it('should call setEffectiveReferrer for redirect experiments', function(done) { - // enable redirect experiment - window.optimizely.newMockData[2347102720].isActive = true; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.called(optimizely.setEffectiveReferrer, 'barstools.com'); - }); + context("if a redirect experiment hasn't executed", function() { + beforeEach(function() { + // by default mock data has no redirect experiments active + mockWindowOptimizely(); }); - it('should call sendNewDataToSegment for active Optimizely X campaigns', function(done) { - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.calledTwice(optimizely.sendNewDataToSegment); - analytics.deepEqual(optimizely.sendNewDataToSegment.args[0], [ - { - audiences: [ - { - name: 'Penthouse 6', - id: '8888222438' - }, - { - name: 'Fam Yolo', - id: '1234567890' - } - ], - campaignName: 'Coding Bootcamp', - id: '7222777766', - experiment: { - id: '1111182111', - name: 'Coding Bootcamp' - }, - variation: { - id: '7333333333', - name: 'Variation DBC' - }, - isActive: true, - isInCampaignHoldback: false, - reason: undefined, - visitorRedirected: false - } - ]); - analytics.deepEqual(optimizely.sendNewDataToSegment.args[1], [ + it('does not capture redirect info', function() { + sinon.assert.notCalled(optimizely.setRedirectInfo); + + var initializedCalls = _.filter(prePushStub.getCalls(), { + args: [ { - audiences: [ - { - name: 'Trust Tree', - id: '7527565438' - } - ], - campaignName: 'URF', - id: '7547101713', - experiment: { - id: '7547682694', - name: 'Worlds Group Stage' - }, - variation: { - id: '7557950020', - name: 'Variation #1' - }, - isActive: true, - isInCampaignHoldback: true, - reason: undefined, - visitorRedirected: false + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + } } - ]); + ] }); + assert.equal(initializedCalls.length, 1); + // Actually simulate an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); }); }); - describe('Both', function() { - beforeEach(function() { - mockBothOptimizelyDataObjects(); - analytics.initialize(); - }); + it('does not immediately call sendWebDecisionToSegment', function() { + optimizely.initWebIntegration(); + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); - // Note: we're not testing setEffectiveReferrer here since you can only have one version - // or the other, not both. And each one has been tested in the above unit tests + context('when a campaign is finally decided', function() { + var handler; - it('should call both sendClassicDataToSegment and sendNewDataToSegment', function(done) { - // we have two active experiments running in the mock data object for both versions - executeAsyncTest(done, function() { - analytics.calledTwice(optimizely.sendClassicDataToSegment); - analytics.calledTwice(optimizely.sendNewDataToSegment); - analytics.deepEqual(optimizely.sendClassicDataToSegment.args[0], [ + beforeEach(function() { + // Make sure the code is actually listening for campaign decisions + var campaignDecidedCalls = _.filter(prePushStub.getCalls(), { + args: [ { - experiment: { - id: '0', - name: 'Test' - }, - variations: [ - { - id: '123', - name: 'Variation #123' - } - ], - sections: undefined + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + } } - ]); - analytics.deepEqual(optimizely.sendClassicDataToSegment.args[1], [ - { - experiment: { - id: '11', - name: 'Redirect Test', - referrer: 'google.com' - }, - variations: [ - { - id: '22', - name: 'Redirect Variation' - } - ], - sections: undefined + ] + }); + assert.equal(campaignDecidedCalls.length, 1); + // We'll call this later in order to simulate the 'campaignDecided' event. + handler = campaignDecidedCalls[0].args[0].handler; + }); + + context('and the campaign is active', function() { + beforeEach(function() { + mockWindowOptimizely(); + window.optimizely.newMockData[2347102720].isActive = true; + + handler({ + data: { + campaign: { + id: '2347102720' + } } - ]); - analytics.deepEqual(optimizely.sendNewDataToSegment.args[0], [ - { - audiences: [ - { - name: 'Penthouse 6', - id: '8888222438' - }, - { - name: 'Fam Yolo', - id: '1234567890' - } - ], - campaignName: 'Coding Bootcamp', - id: '7222777766', + }); + }); + + it('calls #sendWebDecisionToSegment', function() { + sinon.assert.calledWithExactly( + optimizely.sendWebDecisionToSegment, + sinon.match({ + id: '2347102720', + campaignName: 'Get Rich or Die Tryin', experiment: { - id: '1111182111', - name: 'Coding Bootcamp' + id: '7522212694', + name: 'Wells Fargo Scam' }, variation: { - id: '7333333333', - name: 'Variation DBC' + id: '7551111120', + name: 'Variation Corruption #1884' }, - isActive: true, isInCampaignHoldback: false, - reason: undefined, - visitorRedirected: false - } - ]); - analytics.deepEqual(optimizely.sendNewDataToSegment.args[1], [ - { audiences: [ { - name: 'Trust Tree', - id: '7527565438' + name: 'Middle Class', + id: '7100568438' } - ], - campaignName: 'URF', - id: '7547101713', - experiment: { - id: '7547682694', - name: 'Worlds Group Stage' - }, - variation: { - id: '7557950020', - name: 'Variation #1' - }, - isActive: true, - isInCampaignHoldback: true, - reason: undefined, - visitorRedirected: false - } - ]); + ] + }) + ); }); }); - }); - }); - }); - describe('#setEffectiveReferrer', function() { - describe('Classic', function() { - beforeEach(function(done) { - mockOptimizelyClassicDataObject(); - analytics.initialize(); - tick(done); - }); + context('and the campaign is inactive', function() { + beforeEach(function() { + mockWindowOptimizely(); - it('should set a global variable `window.optimizelyEffectiveReferrer`', function() { - analytics.equal(window.optimizelyEffectiveReferrer, 'google.com'); - }); - }); - - describe('New', function() { - beforeEach(function() { - mockOptimizelyXDataObject(); - // enable redirect experiment - window.optimizely.newMockData[2347102720].isActive = true; - analytics.initialize(); - }); + handler({ + data: { + campaign: { + id: '2347102720' + } + } + }); + }); - it('should set a global variable `window.optimizelyEffectiveReferrer`', function(done) { - executeAsyncTest(done, function() { - analytics.equal(window.optimizelyEffectiveReferrer, 'barstools.com'); + it('does not call #sendWebDecisionToSegment', function() { + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); }); }); }); - // Again -- we're not testing for both since there is no point. - // You can't have this function execute twice each with different referrer value - // It will always either just call one or the other - }); - - describe('#sendClassicDataToSegment', function() { - beforeEach(function() { - mockOptimizelyClassicDataObject(); - }); - - describe('#options.variations', function() { - beforeEach(function(done) { - optimizely.options.variations = true; - analytics.stub(analytics, 'identify'); - analytics.initialize(); - tick(done); - }); - - it('should send each experiment via `.identify()`', function() { - // Since we have two experiments in `window.optimizely.data.state.activeExperiments` - // This test proves the breaking changes for the option (it used to send both experiment data in one - // `.identify()` call) - analytics.calledTwice(analytics.identify); - analytics.deepEqual(analytics.identify.args[0], [ - { - 'Experiment: Test': 'Variation #123' - } - ]); - analytics.deepEqual(analytics.identify.args[1], [ - { - 'Experiment: Redirect Test': 'Redirect Variation' - } - ]); - }); - }); - - describe('#options.sendRevenueOnlyForOrderCompleted', function() { + context('after the Optimizely snippet has loaded', function() { beforeEach(function() { - analytics.stub(window.optimizely, 'push'); + mockWindowOptimizely(); }); - it('should not include revenue on a non Order Completed event if `onlySendRevenueOnOrderCompleted` is enabled', function() { - analytics.initialize(); - analytics.track('Order Updated', { - revenue: 25 - }); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'Order Updated', - tags: {} - }); - }); + context('if a redirect experiment has executed', function() { + beforeEach(function() { + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; - it('should send revenue only on Order Completed if `onlySendRevenueOnOrderCompleted` is enabled', function() { - analytics.initialize(); - analytics.track('Order Completed', { - revenue: 9.99 + optimizely.initWebIntegration(); }); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'Order Completed', - tags: { - revenue: 999 - } - }); - }); - it('should send revenue on all events with properties.revenue if `onlySendRevenueOnOrderCompleted` is disabled', function() { - optimizely.options.sendRevenueOnlyForOrderCompleted = false; - analytics.initialize(); - analytics.track('Checkout Started', { - revenue: 9.99 - }); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'Checkout Started', - tags: { - revenue: 999 - } + it('immediately captures redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', + referrer: 'barstools.com' + }); }); - }); - }); - - describe('#options.listen', function() { - beforeEach(function() { - optimizely.options.listen = true; - analytics.stub(analytics, 'track'); - }); - it('should send each standard active experiment data via `.track()`', function(done) { - // activate standard experiment - window.optimizely.data.state.activeExperiments = ['0']; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '0', - experimentName: 'Test', - variationId: '123', - variationName: 'Variation #123' - }, - { integration: optimizelyContext } - ]); + it('captures redirect info _before_ tracking decisions', function() { + sinon.assert.callOrder( + optimizely.setRedirectInfo, + optimizely.sendWebDecisionToSegment + ); }); }); - it('should map custom properties and send each standard active experiment data via `.track()`', function(done) { - optimizely.options.customExperimentProperties = { - experimentId: 'experiment_id', - experimentName: 'experiment_name', - variationId: 'variation_id', - variationName: 'variation_name' - }; - - window.optimizely.data.experiment_id = '124'; - window.optimizely.data.experiment_name = 'custom experiment name'; - window.optimizely.data.variation_id = '421'; - window.optimizely.data.variation_name = 'custom variation name'; + context("if a redirect experiment hasn't executed", function() { + beforeEach(function() { + optimizely.initWebIntegration(); + }); - window.optimizely.data.state.activeExperiments = ['0']; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '124', - experimentName: 'custom experiment name', - variationId: '421', - variationName: 'custom variation name' - }, - { integration: optimizelyContext } - ]); + it('does not capture redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); }); }); - it('should not map existing properties if custom properties not specified`', function(done) { - optimizely.options.customExperimentProperties = { - variationId: 'variation_id', - variationName: 'variation_name' - }; - - window.optimizely.data.experiment_id = '124'; - window.optimizely.data.experiment_name = 'custom experiment name'; - window.optimizely.data.variation_id = '421'; - window.optimizely.data.variation_name = 'custom variation name'; + it('calls sendWebDecisionToSegment for active Optimizely X campaigns', function() { + optimizely.initWebIntegration(); - window.optimizely.data.state.activeExperiments = ['0']; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', + sinon.assert.calledTwice(optimizely.sendWebDecisionToSegment); + sinon.assert.calledWithExactly(optimizely.sendWebDecisionToSegment, { + audiences: [ { - experimentId: '0', - experimentName: 'Test', - variationId: '421', - variationName: 'custom variation name' + name: 'Penthouse 6', + id: '8888222438' }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should send multivariate active experiment data via `.track()`', function(done) { - // activate multivariate experiment and set section info - window.optimizely.data.state.activeExperiments = ['0']; - window.optimizely.data.sections = { - 123409: { name: 'Section 1', variation_ids: ['123'] } - }; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', { - experimentId: '0', - experimentName: 'Test', - variationId: '123', - variationName: 'Variation #123', - sectionName: 'Section 1', - sectionId: '123409' - }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should dedupe sectionNames for multi section multivariate active experiment data via `.track()`', function(done) { - // activate multivariate experiment and set section info - window.optimizely.data.state.activeExperiments = ['1']; - window.optimizely.data.sections = { - 123409: { name: 'Section 1', variation_ids: ['123', '22', '789'] } - }; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', + name: 'Fam Yolo', + id: '1234567890' + } + ], + campaignName: 'Coding Bootcamp', + id: '7222777766', + experiment: { + id: '1111182111', + name: 'Coding Bootcamp' + }, + variation: { + id: '7333333333', + name: 'Variation DBC' + }, + isActive: true, + isInCampaignHoldback: false, + reason: undefined, + visitorRedirected: false + }); + sinon.assert.calledWithExactly(optimizely.sendWebDecisionToSegment, { + audiences: [ { - experimentId: '1', - experimentName: 'MultiVariate Test', - variationId: '123,22,789', - variationName: 'Redirect Variation, Var 789, Variation #123', - sectionName: 'Section 1', - sectionId: '123409' - }, - { integration: optimizelyContext } - ]); - }); - }); + name: 'Trust Tree', + id: '7527565438' + } + ], + campaignName: 'URF', + id: '7547101713', + experiment: { + id: '7547682694', + name: 'Worlds Group Stage' + }, + variation: { + id: '7557950020', + name: 'Variation #1' + }, + isActive: true, + isInCampaignHoldback: true, + reason: undefined, + visitorRedirected: false + }); + }); + + context('when a future campaign is decided', function() { + var handler; - it('should send multivariate active experiment with multiple section data via `.track()`', function(done) { - // activate multivariate experiment and set section info - window.optimizely.data.state.activeExperiments = ['1']; - window.optimizely.data.sections = { - 112309: { name: 'Section 1', variation_ids: ['123'] }, - 111111: { name: 'Section 2', variation_ids: ['22', '789'] } - }; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '1', - experimentName: 'MultiVariate Test', - variationId: '123,22,789', - variationName: 'Redirect Variation, Var 789, Variation #123', - sectionName: 'Section 1, Section 2', - sectionId: '111111,112309' - }, - { integration: optimizelyContext } - ]); - }); - }); + beforeEach(function() { + optimizely.initWebIntegration(); + // Forget about the initial campaigns that were tracked. + optimizely.sendWebDecisionToSegment.resetHistory(); - it('should send redirect active experiment data via `.track()`', function(done) { - // activate redirect experiment - window.optimizely.data.state.activeExperiments = []; - var context = { - integration: optimizelyContext, - page: { referrer: 'google.com' } - }; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', + // Make sure the code is actually listening for campaign decisions + var campaignDecidedCalls = _.filter( + window.optimizely.push.getCalls(), { - experimentId: '11', - experimentName: 'Redirect Test', - referrer: 'google.com', - variationId: '22', - variationName: 'Redirect Variation' - }, - context - ]); + args: [ + { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + } + } + ] + } + ); + assert.equal(campaignDecidedCalls.length, 1); + // We'll call this later in order to simulate the 'campaignDecided' event. + handler = campaignDecidedCalls[0].args[0].handler; }); - }); - it("should send Google's nonInteraction flag via `.track()`", function(done) { - // flip the nonInteraction option on and activate standard experiment - optimizely.options.nonInteraction = true; - window.optimizely.data.state.activeExperiments = ['0']; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '0', - experimentName: 'Test', - variationId: '123', - variationName: 'Variation #123', - nonInteraction: 1 - }, - { integration: optimizelyContext } - ]); + context('and the campaign is active', function() { + beforeEach(function() { + window.optimizely.newMockData[2347102720].isActive = true; + + handler({ + data: { + campaign: { + id: '2347102720' + } + } + }); + }); + + it('calls #sendWebDecisionToSegment', function() { + sinon.assert.calledWithExactly( + optimizely.sendWebDecisionToSegment, + sinon.match({ + id: '2347102720', + campaignName: 'Get Rich or Die Tryin', + experiment: { + id: '7522212694', + name: 'Wells Fargo Scam' + }, + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + }, + isInCampaignHoldback: false, + audiences: [ + { + name: 'Middle Class', + id: '7100568438' + } + ] + }) + ); + }); }); - }); - it('should not send inactive experiments', function(done) { - // clear out the redirect experiment - window.optimizely.data.state.redirectExperiment = undefined; - // disable all active experiments - window.optimizely.data.state.activeExperiments = []; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.didNotCall(analytics.track); + context('and the campaign is inactive', function() { + beforeEach(function() { + handler({ + data: { + campaign: { + id: '2347102720' + } + } + }); + }); + + it('does not call #sendWebDecisionToSegment', function() { + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); }); }); }); }); - describe('#sendNewDataToSegment', function() { + describe('#sendWebDecisionToSegment', function() { + // TODO: Turn these into proper _unit_ tests. + // * Directly call sendWebDecisionToSegment (after calling setRedirectInfo in cases where + // that precondition is relevant) instead of calling analytics.initialize. + // * For some of these tests (e.g. the ones that reference "personalized" campaigns), + // move the tests to #initWebIntegration and assert that #sendWebDecisionToSegment is called + // with particular arguments. + beforeEach(function() { - mockOptimizelyXDataObject(); + mockWindowOptimizely(); }); - describe('#options.variations', function() { + context('options.variations', function() { beforeEach(function(done) { optimizely.options.variations = true; - analytics.stub(analytics, 'identify'); + sinon.stub(analytics, 'identify'); analytics.initialize(); tick(done); }); @@ -803,13 +606,13 @@ describe('Optimizely', function() { // Since we have two experiments in `window.optimizely.data.state.activeExperiments` // This test proves the breaking changes for the option (it used to send both experiment data in one // `.identify()` call) - analytics.calledTwice(analytics.identify); - analytics.deepEqual(analytics.identify.args[0], [ + sinon.assert.calledTwice(analytics.identify); + assert.deepEqual(analytics.identify.args[0], [ { 'Experiment: Coding Bootcamp': 'Variation DBC' } ]); - analytics.deepEqual(analytics.identify.args[1], [ + assert.deepEqual(analytics.identify.args[1], [ { 'Experiment: Worlds Group Stage': 'Variation #1' } @@ -817,30 +620,31 @@ describe('Optimizely', function() { }); }); - describe('#options.sendRevenueOnlyForOrderCompleted', function() { - beforeEach(function() { - analytics.stub(window.optimizely, 'push'); - }); - - it('should not include revenue on a non Order Completed event if `onlySendRevenueOnOrderCompleted` is enabled', function() { + context('options.sendRevenueOnlyForOrderCompleted', function() { + it('should not include revenue on a non Order Completed event if `onlySendRevenueOnOrderCompleted` is enabled', function(done) { analytics.initialize(); + tick(done); + analytics.track('Order Updated', { revenue: 25 }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Order Updated', tags: {} }); }); - it('should send revenue only on Order Completed if `onlySendRevenueOnOrderCompleted` is enabled', function() { + it('should send revenue only on Order Completed if `onlySendRevenueOnOrderCompleted` is enabled', function(done) { optimizely.options.sendRevenueOnlyForOrderCompleted = true; + analytics.initialize(); + tick(done); + analytics.track('Order Completed', { revenue: 9.99 }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Order Completed', tags: { @@ -849,13 +653,16 @@ describe('Optimizely', function() { }); }); - it('should send revenue on all events with properties.revenue if `onlySendRevenueOnOrderCompleted` is disabled', function() { + it('should send revenue on all events with properties.revenue if `onlySendRevenueOnOrderCompleted` is disabled', function(done) { optimizely.options.sendRevenueOnlyForOrderCompleted = false; + analytics.initialize(); + tick(done); + analytics.track('Checkout Started', { revenue: 9.99 }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Checkout Started', tags: { @@ -865,10 +672,10 @@ describe('Optimizely', function() { }); }); - describe('#options.listen', function() { + context('options.listen', function() { beforeEach(function() { optimizely.options.listen = true; - analytics.stub(analytics, 'track'); + sinon.stub(analytics, 'track'); }); it('should send standard active campaign data via `.track()`', function(done) { @@ -878,7 +685,7 @@ describe('Optimizely', function() { window.optimizely.newMockData[7547101713].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'Coding Bootcamp', @@ -902,7 +709,8 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'URF', @@ -916,75 +724,7 @@ describe('Optimizely', function() { isInCampaignHoldback: true }, { integration: optimizelyContext } - ]); - }); - }); - - it('should map custom properties and send campaign data via `.track()`', function(done) { - optimizely.options.customCampaignProperties = { - campaignName: 'campaign_name', - campaignId: 'campaign_id', - experimentId: 'experiment_id', - experimentName: 'experiment_name' - }; - - window.optimizely.newMockData.experiment_id = '124'; - window.optimizely.newMockData.experiment_name = - 'custom experiment name'; - window.optimizely.newMockData.campaign_id = '421'; - window.optimizely.newMockData.campaign_name = 'custom campaign name'; - - window.optimizely.newMockData[2542102702].isActive = false; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - campaignName: 'custom campaign name', - campaignId: '421', - experimentId: '124', - experimentName: 'custom experiment name', - variationId: '7557950020', - variationName: 'Variation #1', - audienceId: '7527565438', - audienceName: 'Trust Tree', - isInCampaignHoldback: true - }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should not map existing properties if custom properties not specified`', function(done) { - optimizely.options.customCampaignProperties = { - campaignName: 'campaign_name', - campaignId: 'campaign_id' - }; - - window.optimizely.newMockData.experiment_id = '124'; - window.optimizely.newMockData.experiment_name = - 'custom experiment name'; - window.optimizely.newMockData.campaign_id = '421'; - window.optimizely.newMockData.campaign_name = 'custom campaign name'; - - window.optimizely.newMockData[2542102702].isActive = false; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - campaignName: 'custom campaign name', - campaignId: '421', - experimentId: '7547682694', - experimentName: 'Worlds Group Stage', - variationId: '7557950020', - variationName: 'Variation #1', - audienceId: '7527565438', - audienceName: 'Trust Tree', - isInCampaignHoldback: true - }, - { integration: optimizelyContext } - ]); + ); }); }); @@ -999,7 +739,8 @@ describe('Optimizely', function() { }; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'Get Rich or Die Tryin', @@ -1014,7 +755,7 @@ describe('Optimizely', function() { isInCampaignHoldback: false }, context - ]); + ); }); }); @@ -1025,7 +766,8 @@ describe('Optimizely', function() { optimizely.options.nonInteraction = true; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'URF', @@ -1040,7 +782,7 @@ describe('Optimizely', function() { isInCampaignHoldback: true }, { integration: optimizelyContext } - ]); + ); }); }); @@ -1051,37 +793,36 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.didNotCall(analytics.track); + sinon.assert.notCalled(analytics.track); }); }); }); }); - describe('after loading', function() { - beforeEach(function(done) { - analytics.once('ready', done); + describe('#track', function() { + beforeEach(function() { analytics.initialize(); - mockBothOptimizelyDataObjects(); - analytics.page(); }); - describe('#track', function() { + context('when Optimizely Web is implemented', function() { beforeEach(function() { - analytics.stub(window.optimizely, 'push'); + window.optimizely = { + push: sinon.stub() + }; }); it('should send an event', function() { analytics.track('event'); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'event', tags: {} }); }); - it('should repace colons with underscore in eventName', function() { + it('should replace colons with underscore in eventName', function() { analytics.track('event:foo:bar'); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'event_foo_bar', tags: {} @@ -1090,7 +831,7 @@ describe('Optimizely', function() { it('should send all additional properties along as tags', function() { analytics.track('event', { id: 'c00lHa$h', name: 'jerry' }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'event', tags: { id: 'c00lHa$h', name: 'jerry' } @@ -1099,7 +840,7 @@ describe('Optimizely', function() { it('should change revenue to cents and include in tags', function() { analytics.track('Order Completed', { revenue: 9.99 }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Order Completed', tags: { revenue: 999 } @@ -1108,119 +849,132 @@ describe('Optimizely', function() { it('should round the revenue value to an integer value if passed in as a floating point number', function() { analytics.track('Order Completed', { revenue: 534.3099999999999 }); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Order Completed', tags: { revenue: 53431 } }); }); + }); - describe('the Optimizely X Fullstack JavaScript client is present', function() { - beforeEach(function() { - window.optimizelyClientInstance = {}; - analytics.stub(window.optimizelyClientInstance, 'track'); - }); - - afterEach(function() { - window.optimizelyClientInstance.track.restore(); - }); - - it('should send an event through the Optimizely X Fullstack JS SDK using the logged in user', function() { - analytics.identify('user1'); - analytics.track('event', { purchasePrice: 9.99, property: 'foo' }); - analytics.called( - window.optimizelyClientInstance.track, - 'event', - 'user1', - {}, - { purchasePrice: 9.99, property: 'foo' } - ); - }); - - it('should replace colons with underscores for event names', function() { - analytics.identify('user1'); - analytics.track('foo:bar:baz'); - analytics.called( - window.optimizelyClientInstance.track, - 'foo_bar_baz', - 'user1', - {}, - {} - ); - }); - - it('should send an event through the Optimizely X Fullstack JS SDK using the user provider user id', function() { - analytics.track( - 'event', - { purchasePrice: 9.99, property: 'foo' }, - { Optimizely: { userId: 'user1', attributes: { country: 'usa' } } } - ); - analytics.called( - window.optimizelyClientInstance.track, - 'event', - 'user1', - { country: 'usa' }, - { property: 'foo', purchasePrice: 9.99 } - ); - }); - - it('should send revenue on `Order Completed` through the Optimizely X Fullstack JS SDK and `properites.revenue` is passed', function() { - analytics.track( - 'Order Completed', - { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, - { Optimizely: { userId: 'user1', attributes: { country: 'usa' } } } - ); - analytics.called( - window.optimizelyClientInstance.track, - 'Order Completed', - 'user1', - { country: 'usa' }, - { property: 'foo', purchasePrice: 9.99, revenue: 199 } - ); - }); + context('when Optimizely Full Stack is implemented', function() { + beforeEach(function() { + window.optimizelyClientInstance = { + track: sinon.stub() + }; + }); - it('should not default to sending revenue through the Optimizely X Fullstack JS SDK on non `Order Completed` events and `properites.revenue` is passed', function() { - analytics.track( - 'event', - { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, - { Optimizely: { userId: 'user1', attributes: { country: 'usa' } } } - ); - analytics.called( - window.optimizelyClientInstance.track, - 'event', - 'user1', - { country: 'usa' }, - { property: 'foo', purchasePrice: 9.99 } - ); - }); + it('should send an event through the Optimizely X Full Stack JS SDK using the logged in user', function() { + analytics.identify('user1'); + analytics.track('event', { purchasePrice: 9.99, property: 'foo' }); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'event', + 'user1', + {}, + { purchasePrice: 9.99, property: 'foo' } + ); + }); + + it('should replace colons with underscores for event names', function() { + analytics.identify('user1'); + analytics.track('foo:bar:baz'); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'foo_bar_baz', + 'user1', + {}, + {} + ); + }); + + it('should send an event through the Optimizely X Fullstack JS SDK using the user provider user id', function() { + analytics.track( + 'event', + { purchasePrice: 9.99, property: 'foo' }, + { + Optimizely: { userId: 'user1', attributes: { country: 'usa' } } + } + ); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'event', + 'user1', + { country: 'usa' }, + { property: 'foo', purchasePrice: 9.99 } + ); + }); + + it('should send revenue on `Order Completed` through the Optimizely X Fullstack JS SDK and `properites.revenue` is passed', function() { + analytics.track( + 'Order Completed', + { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, + { + Optimizely: { userId: 'user1', attributes: { country: 'usa' } } + } + ); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'Order Completed', + 'user1', + { country: 'usa' }, + { property: 'foo', purchasePrice: 9.99, revenue: 199 } + ); + }); + + it('should not default to sending revenue through the Optimizely X Fullstack JS SDK on non `Order Completed` events and `properites.revenue` is passed', function() { + analytics.track( + 'event', + { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, + { + Optimizely: { userId: 'user1', attributes: { country: 'usa' } } + } + ); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'event', + 'user1', + { country: 'usa' }, + { property: 'foo', purchasePrice: 9.99 } + ); + }); - it('should send revenue through the Optimizely X Fullstack JS SDK on all events if `sendRevenueOnlyForOrderCompleted` is disabled and `properites.revenue` is passed', function() { - optimizely.options.sendRevenueOnlyForOrderCompleted = false; - analytics.track( - 'event', - { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, - { Optimizely: { userId: 'user1', attributes: { country: 'usa' } } } - ); - analytics.called( - window.optimizelyClientInstance.track, - 'event', - 'user1', - { country: 'usa' }, - { property: 'foo', purchasePrice: 9.99, revenue: 199 } - ); - }); + it('should send revenue through the Optimizely X Fullstack JS SDK on all events if `sendRevenueOnlyForOrderCompleted` is disabled and `properites.revenue` is passed', function() { + optimizely.options.sendRevenueOnlyForOrderCompleted = false; + analytics.track( + 'event', + { purchasePrice: 9.99, property: 'foo', revenue: 1.99 }, + { + Optimizely: { userId: 'user1', attributes: { country: 'usa' } } + } + ); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'event', + 'user1', + { country: 'usa' }, + { property: 'foo', purchasePrice: 9.99, revenue: 199 } + ); }); }); + }); - describe('#page', function() { + describe('#page', function() { + beforeEach(function() { + analytics.initialize(); + }); + + context('when Optimizely Web is implemented', function() { beforeEach(function() { - analytics.stub(window.optimizely, 'push'); + window.optimizely = { + push: sinon.stub() + }; }); it('should send an event for a named page', function() { var referrer = window.document.referrer; analytics.page('Home'); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Viewed Home Page', tags: { @@ -1237,7 +991,7 @@ describe('Optimizely', function() { it('should send an event for a named and categorized page', function() { var referrer = window.document.referrer; analytics.page('Blog', 'New Integration'); - analytics.called(window.optimizely.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'event', eventName: 'Viewed Blog New Integration Page', tags: { @@ -1254,22 +1008,3 @@ describe('Optimizely', function() { }); }); }); - -/* - * execute AsyncTest - * - * Prevent tests from hanging if deepEqual fails inside `tick` - * @api private - * @param {Function} done cb - * @param {Function} function that runs test - */ -function executeAsyncTest(done, test) { - tick(function() { - try { - test(); - done(); - } catch (e) { - done(e); - } - }); -} diff --git a/yarn.lock b/yarn.lock index c0759b622..ac84a095e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1457,10 +1457,10 @@ component-cookie "^1.1.2" component-url "^0.2.1" -"@segment/tracktor@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@segment/tracktor/-/tracktor-0.12.0.tgz#2df0a1f8dad87e13ca4afac51655d6bac7c0c95f" - integrity sha512-yOGcYD33y0Wo1qHIA+IFIHcxk0GoRrQwCjpuaKZf2rnz0puZoseSGPdbIX47BgMLSSgEYnBoW3s5aUpCRdkEkw== +"@segment/tracktor@0.12.1": + version "0.12.1" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/@segment/tracktor/-/tracktor-0.12.1.tgz#ca3e868f8b51c7da585764a482874addc31ea3e1" + integrity sha1-yj6Gj4tRx9pYV2SkgodK3cMeo+E= dependencies: element-matches-polyfill "^1.0.0" whatwg-fetch "^3.0.0" @@ -1485,6 +1485,20 @@ dependencies: type-detect "4.0.8" +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.7.2": + version "1.8.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha1-yNaIIahUxVW7oXLzsGlZoAObI20= + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha1-KTZ0/MsyYqx4LHqt/eyoaxDHXEA= + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/formatio@^3.1.0", "@sinonjs/formatio@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e" @@ -1493,6 +1507,14 @@ "@sinonjs/commons" "^1" "@sinonjs/samsam" "^3.1.0" +"@sinonjs/formatio@^5.0.1": + version "5.0.1" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha1-8T5xPLMxOxq5ZZAbAbCCjqa3cIk= + dependencies: + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^5.0.2" + "@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.1.tgz#e88c53fbd9d91ad9f0f2b0140c16c7c107fe0d07" @@ -1502,6 +1524,15 @@ array-from "^2.1.1" lodash "^4.17.11" +"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.0.3": + version "5.0.3" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/@sinonjs/samsam/-/samsam-5.0.3.tgz#86f21bdb3d52480faf0892a480c9906aa5a52938" + integrity sha1-hvIb2z1SSA+vCJKkgMmQaqWlKTg= + dependencies: + "@sinonjs/commons" "^1.6.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + "@sinonjs/text-encoding@^0.7.1": version "0.7.1" resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" @@ -1843,6 +1874,11 @@ assert@^1.4.0: object-assign "^4.1.1" util "0.10.3" +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha1-5gtrDo8wG9l+U3UhW9pAbIURjAs= + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -2514,6 +2550,18 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chai@^4.2.0: + version "4.2.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha1-dgqnLPION5XoSxKHfODoNzeqKeU= + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^3.0.1" + get-func-name "^2.0.0" + pathval "^1.1.0" + type-detect "^4.0.5" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2544,6 +2592,11 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +check-error@^1.0.2: + version "1.0.2" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + chokidar@^2.0.3, chokidar@^2.1.1: version "2.1.6" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" @@ -3298,6 +3351,13 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha1-38lARACtHI/gI+faHfHBR8S0RN8= + dependencies: + type-detect "^4.0.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -3481,6 +3541,11 @@ diff@3.5.0, diff@^3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== +diff@^4.0.2: + version "4.0.2" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha1-YPOuy4nV+uUgwRqhnvwruYKq3n0= + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -4419,6 +4484,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + get-own-enumerable-property-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" @@ -4770,6 +4840,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s= + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -6145,6 +6220,11 @@ lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.2.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.17.15: + version "4.17.15" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg= + log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -6751,6 +6831,17 @@ nise@^1.4.10: lolex "^4.1.0" path-to-regexp "^1.7.0" +nise@^4.0.1: + version "4.0.3" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/nise/-/nise-4.0.3.tgz#9f79ff02fa002ed5ffbc538ad58518fa011dc913" + integrity sha1-n3n/AvoALtX/vFOK1YUY+gEdyRM= + dependencies: + "@sinonjs/commons" "^1.7.0" + "@sinonjs/fake-timers" "^6.0.0" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-emoji@^1.0.3: version "1.10.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" @@ -7512,6 +7603,11 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" +pathval@^1.1.0: + version "1.1.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -8518,6 +8614,19 @@ sinon@^7.3.2: nise "^1.4.10" supports-color "^5.5.0" +sinon@^9.0.2: + version "9.0.2" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/sinon/-/sinon-9.0.2.tgz#b9017e24633f4b1c98dfb6e784a5f0509f5fd85d" + integrity sha1-uQF+JGM/SxyY37bnhKXwUJ9f2F0= + dependencies: + "@sinonjs/commons" "^1.7.2" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.0.3" + diff "^4.0.2" + nise "^4.0.1" + supports-color "^7.1.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -9011,6 +9120,13 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://optimizely.jfrog.io/optimizely/api/npm/npm/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha1-aOMlkd9z4lrRxLSRCKLsUHliv9E= + dependencies: + has-flag "^4.0.0" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -9313,7 +9429,7 @@ type-component@0.0.1: resolved "https://registry.yarnpkg.com/type-component/-/type-component-0.0.1.tgz#952a6c81c21efd24d13d811d0c8498cb860e1956" integrity sha1-lSpsgcIe/STRPYEdDISYy4YOGVY= -type-detect@4.0.8: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==