From e1fa89f409a52dc81651e9f04ba85b7362ce6156 Mon Sep 17 00:00:00 2001 From: nikhil Date: Fri, 12 Jun 2020 16:38:40 -0700 Subject: [PATCH 01/17] refactor: Clean up the Optimizely integration * 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. --- integrations/optimizely/HISTORY.md | 8 + integrations/optimizely/lib/index.js | 454 ++--------- integrations/optimizely/test/index.test.js | 862 +++++---------------- 3 files changed, 273 insertions(+), 1051 deletions(-) diff --git a/integrations/optimizely/HISTORY.md b/integrations/optimizely/HISTORY.md index ebdb0e517..7b7d52a32 100644 --- a/integrations/optimizely/HISTORY.md +++ b/integrations/optimizely/HISTORY.md @@ -1,4 +1,12 @@ +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. + 3.5.0 / 2019-12-28 ================== diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 8c18d286d..1f8a57842 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -23,8 +23,7 @@ var Optimizely = (module.exports = integration('Optimizely') .option('listen', true) // send data via `.track()` .option('nonInteraction', false) .option('sendRevenueOnlyForOrderCompleted', true) - .option('customExperimentProperties', {}) - .option('customCampaignProperties', {})); +); /** * The name and version for this integration. @@ -38,9 +37,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 +47,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) - }); + this.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 +95,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 +102,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 +129,6 @@ Optimizely.prototype.track = function(track) { /** * Page. * - * https://www.optimizely.com/docs/api#track-event - * * @api public * @param {Page} page */ @@ -164,165 +150,32 @@ 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 - * @param {Object} campaignState: contains all information regarding experiments and campaign - * @param {String} campaignState.id: the ID of the campaign - * @param {String} campaignState.campaignName: the name of the campaign - * @param {Array} campaignState.audiences: "Audiences" the visitor is considered part of related to this campaign - * @param {String} campaignState.audiences[].id: the id of the Audience - * @param {String} campaignState.audiences[].name: the name of the Audience - * @param {Object} campaignState.experiment: the experiment the visitor is seeing - * @param {String} campaignState.experiment.id: the id of the experiment - * @param {String} campaignState.experiment.name: the name of the experiment - * @param {String} campaignState.experiment.referrer: the effective referrer of the experiment (only defined for redirect) - * @param {Object} campaignState.variation: the variation the visitor is seeing - * @param {String} campaignState.variation.id: the id of the variation - * @param {String} campaignState.variation.name: the name of the variation - * @param {String} campaignState.isInCampaignHoldback: whether the visitor is in the Campaign holdback + * @param {String} id + * @param {String|undefined} referrer */ -Optimizely.prototype.sendNewDataToSegment = function(campaignState) { +Optimizely.prototype.sendWebDecisionToSegment = function(id, referrer) { + var state = window.optimizely.get && window.optimizely.get('state'); + if (!state) { + return; + } + + var activeCampaigns = state.getCampaignStates({ + isActive: true + }); + var campaignState = activeCampaigns[id]; + // Legacy. It's more accurate to use on context.page.referrer or window.optimizelyEffectiveReferrer. + if (referrer) campaignState.experiment.referrer = referrer; + var experiment = campaignState.experiment; var variation = campaignState.variation; var context = { integration: optimizelyContext }; // backward compatibility @@ -371,21 +224,6 @@ Optimizely.prototype.sendNewDataToSegment = function(campaignState) { // 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); } @@ -426,205 +264,85 @@ Optimizely.prototype.setEffectiveReferrer = function(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. - */ - 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; - } - } - if (index === -1) { - activeExperiments.push(redirectExperimentId); - } - referrerOverride(state.redirectExperiment.referrer); - } + var referrer = + state.getRedirectInfo() && state.getRedirectInfo().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); + if (referrer) { + self.setEffectiveReferrer(referrer); + return referrer; // Segment added code: so I can pass this referrer value in cb } } }; /** - * 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 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; + self.sendWebDecisionToSegment(id); } - }; - - 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 + /** + * 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. + * + * This function also checks for an effective referrer if the visitor got to the current + * page via an Optimizely redirect variation. + */ + 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) { + self.sendWebDecisionToSegment(id, referrer); + } else { + self.sendWebDecisionToSegment(id); + } } } - }; - - /** - * 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 || []; + } else { window.optimizely.push({ type: 'addListener', filter: { type: 'lifecycle', - name: 'campaignDecided' + name: 'initialized' }, - handler: function(event) { - var id = event.data.campaign.id; - newActiveCampaign(id); + handler: function() { + checkReferrer(); } }); - }; - - /** - * 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(); + } }; - - initClassicOptimizelyIntegration( - handlers.referrerOverride, - handlers.sendExperimentData - ); - initNewOptimizelyIntegration( - handlers.referrerOverride, - handlers.sendCampaignData - ); + registerCurrentlyActiveCampaigns(); + registerFutureActiveCampaigns(); }; diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 81253a299..a72f5c7d9 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -1,5 +1,6 @@ 'use strict'; +var _ = require('lodash'); var Analytics = require('@segment/analytics.js-core').constructor; var sandbox = require('@segment/clear-env'); var tester = require('@segment/analytics.js-integration-tester'); @@ -8,54 +9,9 @@ var tick = require('next-tick'); /** * 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' - }, - variationIdsMap: { - 0: ['123'], - 1: ['123', '22', '789'], - 11: ['22'], - 2: ['44'] - }, - redirectExperiment: { - variationId: '22', - experimentId: '11', - referrer: 'google.com' - } - } - }; -}; - -// Optimizely X -var mockOptimizelyXDataObject = function() { - // remove Classic data object - delete window.optimizely.data; - +var mockWindowOptimizely = function() { window.optimizely.newMockData = { 2347102720: { audiences: [ @@ -131,39 +87,28 @@ var mockOptimizelyXDataObject = function() { 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]; + + window.optimizely = { + get: function() { + return { + getCampaignStates: function(options) { + if (!options.isActive) { + throw new Error('Incorrect call to getCampaignStates'); + } + return _.filter(window.optimizely.newMockData, {isActive: options.isActive}); + }, + getRedirectInfo: function() { + var campaigns = this.getCampaignStates({ isActive: true }); + for (var id in campaigns) { + if (campaigns[id].visitorRedirected) + return { referrer: 'barstools.com' }; } } - 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(); -}; - // passed into context.integration (not context.integrations!) for all track calls for some reason var optimizelyContext = { name: 'optimizely', @@ -198,29 +143,22 @@ describe('Optimizely', function() { }); 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) { + analytics.stub(optimizely, 'initWebIntegration'); analytics.stub(window.optimizely, 'push'); analytics.once('ready', done); analytics.initialize(); analytics.page(); }); - it('should call initOptimizelyIntegration', function(done) { + afterEach(function() { + optimizely.initWebIntegration.restore(); + }); + + it('should call initWebIntegration', function(done) { executeAsyncTest(done, function() { - analytics.called(Optimizely.initOptimizelyIntegration); + analytics.called(Optimizely.initWebIntegration); }); }); @@ -232,260 +170,101 @@ describe('Optimizely', function() { }); }); - 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); + describe('#initWebIntegration', function() { + beforeEach(function() { + analytics.stub(optimizely, 'sendWebDecisionToSegment'); + analytics.stub(optimizely, 'setEffectiveReferrer'); + mockWindowOptimizely(); + }); + + afterEach(function() { + optimizely.sendWebDecisionToSegment.restore(); + optimizely.setEffectiveReferrer.restore(); + }); + + 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('should call setEffectiveReferrer for redirect experiments', function() { - analytics.called(optimizely.setEffectiveReferrer, 'google.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'); }); + }); - 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], [ + it('should call sendWebDecisionToSegment for active Optimizely X campaigns', function(done) { + analytics.initialize(); + executeAsyncTest(done, function() { + analytics.calledTwice(optimizely.sendWebDecisionToSegment); + analytics.deepEqual(optimizely.sendWebDecisionToSegment.args[0], [ { - experiment: { - id: '0', - name: 'Test' - }, - variations: [ + audiences: [ + { + name: 'Penthouse 6', + id: '8888222438' + }, { - id: '123', - name: 'Variation #123' + name: 'Fam Yolo', + id: '1234567890' } ], - sections: undefined + 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.sendClassicDataToSegment.args[1], [ + analytics.deepEqual(optimizely.sendWebDecisionToSegment.args[1], [ { - experiment: { - id: '11', - name: 'Redirect Test', - referrer: 'google.com' - }, - variations: [ + audiences: [ { - id: '22', - name: 'Redirect Variation' + name: 'Trust Tree', + id: '7527565438' } ], - sections: undefined + 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('New', function() { - beforeEach(function() { - mockOptimizelyXDataObject(); - }); - - 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('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'); - }); - }); - - 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], [ - { - 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 - } - ]); - }); - }); - }); - - describe('Both', function() { - beforeEach(function() { - mockBothOptimizelyDataObjects(); - analytics.initialize(); - }); - - // 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 - - 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], [ - { - 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 - } - ]); - 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], [ - { - 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 - } - ]); - }); - }); - }); }); }); describe('#setEffectiveReferrer', function() { - describe('Classic', function() { - beforeEach(function(done) { - mockOptimizelyClassicDataObject(); - analytics.initialize(); - tick(done); - }); - - it('should set a global variable `window.optimizelyEffectiveReferrer`', function() { - analytics.equal(window.optimizelyEffectiveReferrer, 'google.com'); - }); - }); - - describe('New', function() { + describe('Web', function() { beforeEach(function() { - mockOptimizelyXDataObject(); - // enable redirect experiment + mockWindowOptimizely(); + // Make sure window.optimizely.getRedirectInfo returns something window.optimizely.newMockData[2347102720].isActive = true; analytics.initialize(); }); @@ -496,299 +275,11 @@ describe('Optimizely', function() { }); }); }); - - // 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() { + describe('#sendWebDecisionToSegment', 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() { - beforeEach(function() { - analytics.stub(window.optimizely, 'push'); - }); - - 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: {} - }); - }); - - it('should send revenue only on Order Completed if `onlySendRevenueOnOrderCompleted` is enabled', function() { - analytics.initialize(); - analytics.track('Order Completed', { - revenue: 9.99 - }); - 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 - } - }); - }); - }); - - 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('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'; - - 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('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'; - - window.optimizely.data.state.activeExperiments = ['0']; - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '0', - experimentName: 'Test', - variationId: '421', - variationName: 'custom variation name' - }, - { 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', - { - experimentId: '1', - experimentName: 'MultiVariate Test', - variationId: '123,22,789', - variationName: 'Redirect Variation, Var 789, Variation #123', - sectionName: 'Section 1', - sectionId: '123409' - }, - { integration: optimizelyContext } - ]); - }); - }); - - 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 } - ]); - }); - }); - - 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', - { - experimentId: '11', - experimentName: 'Redirect Test', - referrer: 'google.com', - variationId: '22', - variationName: 'Redirect Variation' - }, - context - ]); - }); - }); - - 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 } - ]); - }); - }); - - 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); - }); - }); - }); - }); - - describe('#sendNewDataToSegment', function() { - beforeEach(function() { - mockOptimizelyXDataObject(); + mockWindowOptimizely(); }); describe('#options.variations', function() { @@ -1061,61 +552,63 @@ describe('Optimizely', function() { beforeEach(function(done) { analytics.once('ready', done); analytics.initialize(); - mockBothOptimizelyDataObjects(); analytics.page(); }); describe('#track', function() { - beforeEach(function() { - analytics.stub(window.optimizely, 'push'); - }); + context('when the Optimizely Web snippet has initialized', function() { + beforeEach(function() { + mockWindowOptimizely(); + analytics.stub(window.optimizely, 'push'); + }); - it('should send an event', function() { - analytics.track('event'); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'event', - tags: {} + it('should send an event', function() { + analytics.track('event'); + analytics.called(window.optimizely.push, { + type: 'event', + eventName: 'event', + tags: {} + }); }); - }); - it('should repace colons with underscore in eventName', function() { - analytics.track('event:foo:bar'); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'event_foo_bar', - tags: {} + it('should replace colons with underscore in eventName', function() { + analytics.track('event:foo:bar'); + analytics.called(window.optimizely.push, { + type: 'event', + eventName: 'event_foo_bar', + tags: {} + }); }); - }); - it('should send all additional properties along as tags', function() { - analytics.track('event', { id: 'c00lHa$h', name: 'jerry' }); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'event', - tags: { id: 'c00lHa$h', name: 'jerry' } + it('should send all additional properties along as tags', function() { + analytics.track('event', { id: 'c00lHa$h', name: 'jerry' }); + analytics.called(window.optimizely.push, { + type: 'event', + eventName: 'event', + tags: { id: 'c00lHa$h', name: 'jerry' } + }); }); - }); - it('should change revenue to cents and include in tags', function() { - analytics.track('Order Completed', { revenue: 9.99 }); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'Order Completed', - tags: { revenue: 999 } + it('should change revenue to cents and include in tags', function() { + analytics.track('Order Completed', { revenue: 9.99 }); + analytics.called(window.optimizely.push, { + type: 'event', + eventName: 'Order Completed', + tags: { revenue: 999 } + }); }); - }); - 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, { - type: 'event', - eventName: 'Order Completed', - tags: { revenue: 53431 } + 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, { + type: 'event', + eventName: 'Order Completed', + tags: { revenue: 53431 } + }); }); }); - describe('the Optimizely X Fullstack JavaScript client is present', function() { + context('when the Optimizely Full Stack JavaScript SDK has initialized', function() { beforeEach(function() { window.optimizelyClientInstance = {}; analytics.stub(window.optimizelyClientInstance, 'track'); @@ -1125,7 +618,7 @@ describe('Optimizely', function() { window.optimizelyClientInstance.track.restore(); }); - it('should send an event through the Optimizely X Fullstack JS SDK using the logged in user', function() { + 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' }); analytics.called( @@ -1213,42 +706,45 @@ describe('Optimizely', function() { }); describe('#page', function() { - beforeEach(function() { - analytics.stub(window.optimizely, 'push'); - }); - - it('should send an event for a named page', function() { - var referrer = window.document.referrer; - analytics.page('Home'); - analytics.called(window.optimizely.push, { - type: 'event', - eventName: 'Viewed Home Page', - tags: { - name: 'Home', - path: '/context.html', - referrer: referrer, - search: '', - title: '', - url: 'http://localhost:9876/context.html' - } + context('when the Optimizely Web snippet has initialized', function() { + beforeEach(function() { + mockWindowOptimizely(); + analytics.stub(window.optimizely, 'push'); + }); + + it('should send an event for a named page', function() { + var referrer = window.document.referrer; + analytics.page('Home'); + analytics.called(window.optimizely.push, { + type: 'event', + eventName: 'Viewed Home Page', + tags: { + name: 'Home', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } + }); }); - }); - 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, { - type: 'event', - eventName: 'Viewed Blog New Integration Page', - tags: { - name: 'New Integration', - category: 'Blog', - path: '/context.html', - referrer: referrer, - search: '', - title: '', - url: 'http://localhost:9876/context.html' - } + 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, { + type: 'event', + eventName: 'Viewed Blog New Integration Page', + tags: { + name: 'New Integration', + category: 'Blog', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } + }); }); }); }); From 8e556d58c054f9109fc8aff7632cec8c01d3be21 Mon Sep 17 00:00:00 2001 From: nikhil Date: Fri, 12 Jun 2020 23:31:45 -0700 Subject: [PATCH 02/17] Way more changes --- integrations/optimizely/lib/index.js | 139 ++-- integrations/optimizely/package.json | 2 + integrations/optimizely/test/index.test.js | 735 ++++++++++++--------- yarn.lock | 121 +++- 4 files changed, 622 insertions(+), 375 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 1f8a57842..b9904c579 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -22,8 +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('sendRevenueOnlyForOrderCompleted', true)); /** * The name and version for this integration. @@ -52,7 +51,7 @@ Optimizely.prototype.initialize = function() { // initializing the integration even though the function below is designed to be async, // just want to be extra safe tick(function() { - this.initWebIntegration(); + self.initWebIntegration(); }); this.ready(); @@ -159,23 +158,22 @@ Optimizely.prototype.page = function(page) { * attached listeners on the page * * @api private - * @param {String} id - * @param {String|undefined} referrer + * @param {Object} campaignState: contains all information regarding experiments and campaign + * @param {String} campaignState.id: the ID of the campaign + * @param {String} campaignState.campaignName: the name of the campaign + * @param {Array} campaignState.audiences: "Audiences" the visitor is considered part of related to this campaign + * @param {String} campaignState.audiences[].id: the id of the Audience + * @param {String} campaignState.audiences[].name: the name of the Audience + * @param {Object} campaignState.experiment: the experiment the visitor is seeing + * @param {String} campaignState.experiment.id: the id of the experiment + * @param {String} campaignState.experiment.name: the name of the experiment + * @param {String} campaignState.experiment.referrer: the effective referrer of the experiment (only defined for redirect) + * @param {Object} campaignState.variation: the variation the visitor is seeing + * @param {String} campaignState.variation.id: the id of the variation + * @param {String} campaignState.variation.name: the name of the variation + * @param {String} campaignState.isInCampaignHoldback: whether the visitor is in the Campaign holdback */ - -Optimizely.prototype.sendWebDecisionToSegment = function(id, referrer) { - var state = window.optimizely.get && window.optimizely.get('state'); - if (!state) { - return; - } - - var activeCampaigns = state.getCampaignStates({ - isActive: true - }); - var campaignState = activeCampaigns[id]; - // Legacy. It's more accurate to use on context.page.referrer or window.optimizelyEffectiveReferrer. - if (referrer) campaignState.experiment.referrer = referrer; - +Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { var experiment = campaignState.experiment; var variation = campaignState.variation; var context = { integration: optimizelyContext }; // backward compatibility @@ -213,12 +211,12 @@ Optimizely.prototype.sendWebDecisionToSegment = function(id, referrer) { 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 @@ -245,7 +243,9 @@ Optimizely.prototype.sendWebDecisionToSegment = function(id, referrer) { }; /** - * setEffectiveReferrer + * setRedirectInfo + * + * TODO: fix the tests! * * 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. @@ -253,13 +253,15 @@ Optimizely.prototype.sendWebDecisionToSegment = function(id, referrer) { * 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; } }; @@ -272,16 +274,29 @@ Optimizely.prototype.setEffectiveReferrer = function(referrer) { Optimizely.prototype.initWebIntegration = function() { var self = this; + /** + * 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 checkReferrer = function() { var state = window.optimizely.get && window.optimizely.get('state'); if (state) { - var referrer = - state.getRedirectInfo() && state.getRedirectInfo().referrer; - - if (referrer) { - self.setEffectiveReferrer(referrer); - return referrer; // Segment added code: so I can pass this referrer value in cb - } + self.setRedirectInfo(state.getRedirectInfo()); + } else { + push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + var state = window.optimizely.get && window.optimizely.get('state'); + if (state) { + self.setRedirectInfo(state.getRedirectInfo()); + } + } + }); } }; @@ -291,16 +306,24 @@ Optimizely.prototype.initWebIntegration = function() { * handles them. */ var registerFutureActiveCampaigns = function() { - window.optimizely = window.optimizely || []; - window.optimizely.push({ + push({ type: 'addListener', filter: { type: 'lifecycle', name: 'campaignDecided' }, handler: function(event) { - var id = event.data.campaign.id; - self.sendWebDecisionToSegment(id); + 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]); } }); }; @@ -308,41 +331,21 @@ Optimizely.prototype.initWebIntegration = function() { /** * 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. - * - * This function also checks for an effective referrer if the visitor got to the current - * page via an Optimizely redirect variation. */ 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) { - self.sendWebDecisionToSegment(id, referrer); - } else { - self.sendWebDecisionToSegment(id); - } - } - } - } else { - window.optimizely.push({ - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'initialized' - }, - handler: function() { - checkReferrer(); - } - }); + each(function(campaignState) { + self.sendWebDecisionToSegment(campaignState); + }, activeCampaigns); } }; + + checkReferrer(); registerCurrentlyActiveCampaigns(); registerFutureActiveCampaigns(); }; diff --git a/integrations/optimizely/package.json b/integrations/optimizely/package.json index 128b01bed..86ba169e5 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", @@ -47,6 +48,7 @@ "karma-spec-reporter": "^0.0.32", "karma-summary-reporter": "^1.6.0", "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 a72f5c7d9..6db47b964 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -2,7 +2,9 @@ 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').sandbox.create(); var tester = require('@segment/analytics.js-integration-tester'); var Optimizely = require('../lib/'); var tick = require('next-tick'); @@ -12,90 +14,92 @@ var tick = require('next-tick'); */ 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 - }, - 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' + 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' }, - { - 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: '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 }, - variation: { - id: '7333333333', - name: 'Variation DBC' + 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 }, - isInCampaignHoldback: false, - // 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' + }, + { + 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 + } + }, - window.optimizely = { get: function() { return { getCampaignStates: function(options) { if (!options.isActive) { throw new Error('Incorrect call to getCampaignStates'); } - return _.filter(window.optimizely.newMockData, {isActive: options.isActive}); + return _.filter(window.optimizely.newMockData, { + isActive: options.isActive + }); }, getRedirectInfo: function() { var campaigns = this.getCampaignStates({ isActive: true }); @@ -105,7 +109,9 @@ var mockWindowOptimizely = function() { } } }; - } + }, + + push: sinon.stub() }; }; @@ -116,6 +122,8 @@ var optimizelyContext = { }; describe('Optimizely', function() { + this.timeout(0); + var analytics; var optimizely; var options = { @@ -140,152 +148,258 @@ describe('Optimizely', function() { analytics.reset(); optimizely.reset(); sandbox(); + sinon.restore(); }); - describe('before loading', function() { - describe('#initialize', function() { - beforeEach(function(done) { - analytics.stub(optimizely, 'initWebIntegration'); - analytics.stub(window.optimizely, 'push'); - analytics.once('ready', done); - analytics.initialize(); - analytics.page(); + describe('#initialize', function() { + beforeEach(function(done) { + sinon.stub(Optimizely.prototype, 'initWebIntegration'); + sinon.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); }); + }); - afterEach(function() { - optimizely.initWebIntegration.restore(); + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizely.push, { + type: 'integration', + OAuthClientId: '5360906403' }); + }); + }); - it('should call initWebIntegration', function(done) { - executeAsyncTest(done, function() { - analytics.called(Optimizely.initWebIntegration); + 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); + }); + }); + + context('when redirect info was captured', function() { + beforeEach(function() { + optimizely.setRedirectInfo({ + experimentId: 'x', + variationId: 'v', + referrer: 'r' }); }); - it('should flag source of integration', function() { - analytics.called(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' + it('sets redirect info', function() { + assert.deepEqual(optimizely.redirectInfo, { + experimentId: 'x', + variationId: 'v', + referrer: 'r' }); + assert.equal(window.optimizelyEffectiveReferrer, 'r'); }); }); + }); + + describe('#initWebIntegration', function() { + beforeEach(function() { + sinon.stub(optimizely, 'sendWebDecisionToSegment'); + sinon.stub(optimizely, 'setRedirectInfo'); + }); - describe('#initWebIntegration', function() { + context('after the Optimizely snippet has loaded', function() { beforeEach(function() { - analytics.stub(optimizely, 'sendWebDecisionToSegment'); - analytics.stub(optimizely, 'setEffectiveReferrer'); mockWindowOptimizely(); }); - afterEach(function() { - optimizely.sendWebDecisionToSegment.restore(); - optimizely.setEffectiveReferrer.restore(); - }); - - 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); + context('if a redirect experiment has executed', function() { + it('captures redirect info', function() { + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; + optimizely.initWebIntegration(); + sinon.assert.calledOnce(optimizely.setRedirectInfo); + sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { + experimentId: 'TODO', + variationId: 'TODO', + 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() { + it('does not capture redirect info', function() { + // by default mock data has no redirect experiments active + optimizely.initWebIntegration(); + sinon.assert.notCalled(optimizely.setRedirectInfo); }); }); - it('should call sendWebDecisionToSegment for active Optimizely X campaigns', function(done) { - analytics.initialize(); - executeAsyncTest(done, function() { - analytics.calledTwice(optimizely.sendWebDecisionToSegment); - analytics.deepEqual(optimizely.sendWebDecisionToSegment.args[0], [ + it('calls sendWebDecisionToSegment for active Optimizely X campaigns', function() { + optimizely.initWebIntegration(); + sinon.assert.calledTwice(optimizely.sendWebDecisionToSegment); + sinon.assert.calledWith({ + audiences: [ { - 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 + name: 'Penthouse 6', + id: '8888222438' + }, + { + name: 'Fam Yolo', + id: '1234567890' } - ]); - analytics.deepEqual(optimizely.sendWebDecisionToSegment.args[1], [ + ], + 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.calledWith({ + audiences: [ { - 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 + 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 }); }); + + it('listens for future campaign activations', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: sinon.match.function + }); + }); + + // TODO: context('when a future campaign activation occurs') + + // TODO: context('when a future campaign decision occurs without activation') }); - }); - describe('#setEffectiveReferrer', function() { - describe('Web', function() { + context('before the Optimizely snippet has loaded', function() { beforeEach(function() { - mockWindowOptimizely(); - // Make sure window.optimizely.getRedirectInfo returns something - window.optimizely.newMockData[2347102720].isActive = true; - analytics.initialize(); + window.optimizely = { + push: sinon.stub() + }; + optimizely.initWebIntegration(); }); - it('should set a global variable `window.optimizelyEffectiveReferrer`', function(done) { - executeAsyncTest(done, function() { - analytics.equal(window.optimizelyEffectiveReferrer, 'barstools.com'); + it('defers the redirect check until snippet initialization', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: sinon.match.function }); }); + + context('once the snippet finally initializes', function() { + beforeEach(function() { + // by default mock data has no redirect experiments active + mockWindowOptimizely(); + }); + + context('if a redirect experiment has executed', function() { + beforeEach(function() { + mockWindowOptimizely(); + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; + }); + + it('captures redirect info', function() { + window.optimizely.push.firstCall.args[0].handler(); + sinon.assert.calledOnce(optimizely.setRedirectInfo); + sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { + experimentId: 'TODO', + variationId: 'TODO', + referrer: 'barstools.com' + }); + }); + }); + + context("if a redirect experiment hasn't executed", function() { + it('does not capture redirect info', function() { + window.optimizely.push.firstCall.args[0].handler(); + sinon.assert.notCalled(optimizely.setRedirectInfo); + }); + }); + }); + + it('does not immediately call sendWebDecisionToSegment', function() { + optimizely.initWebIntegration(); + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); + + it('listens for future campaign activations', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: sinon.match.function + }); + }); + + // TODO: context('when a future campaign activation occurs') + + // TODO: context('when a future campaign decision occurs without activation') }); }); describe('#sendWebDecisionToSegment', function() { + // TODO: call sendWebDecisionToSegment directly with a campaignId, maybe a referrer, and assert on the output + + // TODO: move some of these tests above, where we call initWebIntegration and assert a particular call to sendWebDecisionToSegment + // and a particular state in the API + beforeEach(function() { 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); }); @@ -294,13 +408,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' } @@ -308,30 +422,35 @@ describe('Optimizely', function() { }); }); - describe('#options.sendRevenueOnlyForOrderCompleted', function() { + context('options.sendRevenueOnlyForOrderCompleted', function() { beforeEach(function() { - analytics.stub(window.optimizely, 'push'); + sinon.stub(window.optimizely, 'push'); }); - it('should not include revenue on a non Order Completed event if `onlySendRevenueOnOrderCompleted` is enabled', 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: { @@ -340,13 +459,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: { @@ -356,10 +478,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) { @@ -367,9 +489,11 @@ describe('Optimizely', function() { // Going to leave just the one that was created as a standard // experiment inside Optimizely X (not campaign) window.optimizely.newMockData[7547101713].isActive = false; - analytics.initialize(); + + analytics.initialize(); // TODO + executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'Coding Bootcamp', @@ -393,7 +517,7 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'URF', @@ -428,7 +552,7 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'custom campaign name', @@ -461,7 +585,7 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'custom campaign name', @@ -490,7 +614,7 @@ describe('Optimizely', function() { }; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'Get Rich or Die Tryin', @@ -516,7 +640,7 @@ describe('Optimizely', function() { optimizely.options.nonInteraction = true; analytics.initialize(); executeAsyncTest(done, function() { - analytics.deepEqual(analytics.track.args[0], [ + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { campaignName: 'URF', @@ -542,7 +666,7 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - analytics.didNotCall(analytics.track); + sinon.assert.notCalled(analytics.track); }); }); }); @@ -550,21 +674,21 @@ describe('Optimizely', function() { describe('after loading', function() { beforeEach(function(done) { - analytics.once('ready', done); + mockWindowOptimizely(); analytics.initialize(); analytics.page(); + analytics.once('ready', done); }); describe('#track', function() { context('when the Optimizely Web snippet has initialized', function() { beforeEach(function() { - mockWindowOptimizely(); - analytics.stub(window.optimizely, 'push'); + sinon.stub(window.optimizely, 'push'); }); 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: {} @@ -573,7 +697,7 @@ describe('Optimizely', 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: {} @@ -582,7 +706,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' } @@ -591,7 +715,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 } @@ -600,7 +724,7 @@ 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 } @@ -608,114 +732,121 @@ describe('Optimizely', function() { }); }); - context('when the Optimizely Full Stack JavaScript SDK has initialized', 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 Full Stack 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 } - ); - }); - - 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 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 } - ); - }); - }); + context( + 'when the Optimizely Full Stack JavaScript SDK has initialized', + function() { + beforeEach(function() { + window.optimizelyClientInstance = {}; + sinon.stub(window.optimizelyClientInstance, 'track'); + }); + + 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' } } + } + ); + sinon.assert.calledWith( + window.optimizelyClientInstance.track, + 'event', + 'user1', + { country: 'usa' }, + { property: 'foo', purchasePrice: 9.99, revenue: 199 } + ); + }); + } + ); }); describe('#page', function() { context('when the Optimizely Web snippet has initialized', function() { beforeEach(function() { mockWindowOptimizely(); - analytics.stub(window.optimizely, 'push'); + sinon.stub(window.optimizely, 'push'); }); 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: { @@ -732,7 +863,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: { diff --git a/yarn.lock b/yarn.lock index c0759b622..e2b0c8221 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" @@ -6751,6 +6826,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 +7598,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 +8609,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 +9115,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 +9424,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== From e5475becd11a6e0088be8c4b8a129802359c02a2 Mon Sep 17 00:00:00 2001 From: nikhil Date: Fri, 12 Jun 2020 23:31:45 -0700 Subject: [PATCH 03/17] Finish fixing and refactoring tests --- integrations/optimizely/lib/index.js | 4 +- integrations/optimizely/test/index.test.js | 808 ++++++++++++--------- 2 files changed, 458 insertions(+), 354 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index b9904c579..63d00b000 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -245,15 +245,13 @@ Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { /** * setRedirectInfo * - * TODO: fix the tests! - * * 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. * We will set this global variable that Segment customers can lookup and pass down in their initial `.page()` call inside * their Segment snippet. * * @apr private - * @param {Object=} redirectInfo + * @param {Object?} redirectInfo * @param {String} redirectInfo.experimentId * @param {String} redirectInfo.variationId * @param {String} redirectInfo.referrer diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 6db47b964..347d63902 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -4,7 +4,7 @@ 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').sandbox.create(); +var sinon = require('sinon'); var tester = require('@segment/analytics.js-integration-tester'); var Optimizely = require('../lib/'); var tick = require('next-tick'); @@ -97,16 +97,22 @@ var mockWindowOptimizely = function() { if (!options.isActive) { throw new Error('Incorrect call to getCampaignStates'); } - return _.filter(window.optimizely.newMockData, { + return _.pickBy(window.optimizely.newMockData, { isActive: options.isActive }); }, getRedirectInfo: function() { var campaigns = this.getCampaignStates({ isActive: true }); for (var id in campaigns) { - if (campaigns[id].visitorRedirected) - return { referrer: 'barstools.com' }; + if (campaigns[id].visitorRedirected) { + return { + experimentId: campaigns[id].experiment.id, + variationId: campaigns[id].variation.id, + referrer: 'barstools.com' + }; + } } + return null; } }; }, @@ -217,37 +223,209 @@ describe('Optimizely', function() { 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(); + }); + + context('if a redirect experiment has executed', function() { + beforeEach(function() { + mockWindowOptimizely(); + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; + }); + + 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 simulated an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', + referrer: 'barstools.com' + }); + }); + }); + + context("if a redirect experiment hasn't executed", function() { + beforeEach(function() { + // by default mock data has no redirect experiments active + mockWindowOptimizely(); + }); + + it('does not capture 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 simulated an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); + }); + }); + + it('does not immediately call sendWebDecisionToSegment', function() { + optimizely.initWebIntegration(); + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); + + context('when a campaign is finally decided', function() { + var handler; + + beforeEach(function() { + // Make sure the code is actually listening for campaign decisions + var campaignDecidedCalls = _.filter(prePushStub.getCalls(), { + 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; + }); + + context('and the campaign is active', function() { + beforeEach(function() { + mockWindowOptimizely(); + 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' + } + ] + }) + ); + }); + }); + + context('and the campaign is inactive', function() { + beforeEach(function() { + mockWindowOptimizely(); + + handler({ + data: { + campaign: { + id: '2347102720' + } + } + }); + }); + + it('does not call #sendWebDecisionToSegment', function() { + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); + }); + }); + }); + context('after the Optimizely snippet has loaded', function() { beforeEach(function() { mockWindowOptimizely(); }); context('if a redirect experiment has executed', function() { - it('captures redirect info', function() { + beforeEach(function() { // Make sure window.optimizely.getRedirectInfo returns something window.optimizely.newMockData[2347102720].isActive = true; + optimizely.initWebIntegration(); - sinon.assert.calledOnce(optimizely.setRedirectInfo); - sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { - experimentId: 'TODO', - variationId: 'TODO', + }); + + it('immediately captures redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', referrer: 'barstools.com' }); }); + + it('captures redirect info _before_ tracking decisions', function() { + sinon.assert.callOrder( + optimizely.setRedirectInfo, + optimizely.sendWebDecisionToSegment + ); + }); }); context("if a redirect experiment hasn't executed", function() { - it('does not capture redirect info', function() { - // by default mock data has no redirect experiments active + beforeEach(function() { optimizely.initWebIntegration(); - sinon.assert.notCalled(optimizely.setRedirectInfo); + }); + + it('does not capture redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); }); }); it('calls sendWebDecisionToSegment for active Optimizely X campaigns', function() { optimizely.initWebIntegration(); + sinon.assert.calledTwice(optimizely.sendWebDecisionToSegment); - sinon.assert.calledWith({ + sinon.assert.calledWithExactly(optimizely.sendWebDecisionToSegment, { audiences: [ { name: 'Penthouse 6', @@ -273,7 +451,7 @@ describe('Optimizely', function() { reason: undefined, visitorRedirected: false }); - sinon.assert.calledWith({ + sinon.assert.calledWithExactly(optimizely.sendWebDecisionToSegment, { audiences: [ { name: 'Trust Tree', @@ -297,100 +475,99 @@ describe('Optimizely', function() { }); }); - it('listens for future campaign activations', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'campaignDecided' - }, - handler: sinon.match.function - }); - }); - - // TODO: context('when a future campaign activation occurs') - - // TODO: context('when a future campaign decision occurs without activation') - }); - - context('before the Optimizely snippet has loaded', function() { - beforeEach(function() { - window.optimizely = { - push: sinon.stub() - }; - optimizely.initWebIntegration(); - }); - - it('defers the redirect check until snippet initialization', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'initialized' - }, - handler: sinon.match.function - }); - }); + context('when a future campaign is decided', function() { + var handler; - context('once the snippet finally initializes', function() { beforeEach(function() { - // by default mock data has no redirect experiments active - mockWindowOptimizely(); + optimizely.initWebIntegration(); + // Forget about the initial campaigns that were tracked. + optimizely.sendWebDecisionToSegment.resetHistory(); + + // Make sure the code is actually listening for campaign decisions + var campaignDecidedCalls = _.filter( + window.optimizely.push.getCalls(), + { + 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; }); - context('if a redirect experiment has executed', function() { + context('and the campaign is active', function() { beforeEach(function() { - mockWindowOptimizely(); - // Make sure window.optimizely.getRedirectInfo returns something window.optimizely.newMockData[2347102720].isActive = true; - }); - it('captures redirect info', function() { - window.optimizely.push.firstCall.args[0].handler(); - sinon.assert.calledOnce(optimizely.setRedirectInfo); - sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { - experimentId: 'TODO', - variationId: 'TODO', - referrer: 'barstools.com' + handler({ + data: { + campaign: { + id: '2347102720' + } + } }); }); - }); - context("if a redirect experiment hasn't executed", function() { - it('does not capture redirect info', function() { - window.optimizely.push.firstCall.args[0].handler(); - sinon.assert.notCalled(optimizely.setRedirectInfo); + 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('does not immediately call sendWebDecisionToSegment', function() { - optimizely.initWebIntegration(); - sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); - }); + context('and the campaign is inactive', function() { + beforeEach(function() { + handler({ + data: { + campaign: { + id: '2347102720' + } + } + }); + }); - it('listens for future campaign activations', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'campaignDecided' - }, - handler: sinon.match.function + it('does not call #sendWebDecisionToSegment', function() { + sinon.assert.notCalled(optimizely.sendWebDecisionToSegment); + }); }); }); - - // TODO: context('when a future campaign activation occurs') - - // TODO: context('when a future campaign decision occurs without activation') }); }); describe('#sendWebDecisionToSegment', function() { - // TODO: call sendWebDecisionToSegment directly with a campaignId, maybe a referrer, and assert on the output - - // TODO: move some of these tests above, where we call initWebIntegration and assert a particular call to sendWebDecisionToSegment - // and a particular state in the API + // 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() { mockWindowOptimizely(); @@ -423,10 +600,6 @@ describe('Optimizely', function() { }); context('options.sendRevenueOnlyForOrderCompleted', function() { - beforeEach(function() { - sinon.stub(window.optimizely, 'push'); - }); - it('should not include revenue on a non Order Completed event if `onlySendRevenueOnOrderCompleted` is enabled', function(done) { analytics.initialize(); tick(done); @@ -489,9 +662,7 @@ describe('Optimizely', function() { // Going to leave just the one that was created as a standard // experiment inside Optimizely X (not campaign) window.optimizely.newMockData[7547101713].isActive = false; - - analytics.initialize(); // TODO - + analytics.initialize(); executeAsyncTest(done, function() { assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', @@ -517,7 +688,8 @@ describe('Optimizely', function() { window.optimizely.newMockData[2542102702].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'URF', @@ -531,75 +703,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() { - assert.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() { - assert.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 } - ]); + ); }); }); @@ -614,7 +718,8 @@ describe('Optimizely', function() { }; analytics.initialize(); executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'Get Rich or Die Tryin', @@ -629,7 +734,7 @@ describe('Optimizely', function() { isInCampaignHoldback: false }, context - ]); + ); }); }); @@ -640,7 +745,8 @@ describe('Optimizely', function() { optimizely.options.nonInteraction = true; analytics.initialize(); executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ + sinon.assert.calledWithExactly( + analytics.track, 'Experiment Viewed', { campaignName: 'URF', @@ -655,7 +761,7 @@ describe('Optimizely', function() { isInCampaignHoldback: true }, { integration: optimizelyContext } - ]); + ); }); }); @@ -672,210 +778,210 @@ describe('Optimizely', function() { }); }); - describe('after loading', function() { - beforeEach(function(done) { - mockWindowOptimizely(); + describe('#track', function() { + beforeEach(function() { analytics.initialize(); - analytics.page(); - analytics.once('ready', done); }); - describe('#track', function() { - context('when the Optimizely Web snippet has initialized', function() { - beforeEach(function() { - sinon.stub(window.optimizely, 'push'); - }); + context('when Optimizely Web is implemented', function() { + beforeEach(function() { + window.optimizely = { + push: sinon.stub() + }; + }); - it('should send an event', function() { - analytics.track('event'); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'event', - tags: {} - }); + it('should send an event', function() { + analytics.track('event'); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'event', + tags: {} }); + }); - it('should replace colons with underscore in eventName', function() { - analytics.track('event:foo:bar'); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'event_foo_bar', - tags: {} - }); + it('should replace colons with underscore in eventName', function() { + analytics.track('event:foo:bar'); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'event_foo_bar', + tags: {} }); + }); - it('should send all additional properties along as tags', function() { - analytics.track('event', { id: 'c00lHa$h', name: 'jerry' }); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'event', - tags: { id: 'c00lHa$h', name: 'jerry' } - }); + it('should send all additional properties along as tags', function() { + analytics.track('event', { id: 'c00lHa$h', name: 'jerry' }); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'event', + tags: { id: 'c00lHa$h', name: 'jerry' } }); + }); - it('should change revenue to cents and include in tags', function() { - analytics.track('Order Completed', { revenue: 9.99 }); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'Order Completed', - tags: { revenue: 999 } - }); + it('should change revenue to cents and include in tags', function() { + analytics.track('Order Completed', { revenue: 9.99 }); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'Order Completed', + tags: { revenue: 999 } }); + }); - 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 }); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'Order Completed', - tags: { revenue: 53431 } - }); + 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 }); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'Order Completed', + tags: { revenue: 53431 } }); }); + }); - context( - 'when the Optimizely Full Stack JavaScript SDK has initialized', - function() { - beforeEach(function() { - window.optimizelyClientInstance = {}; - sinon.stub(window.optimizelyClientInstance, 'track'); - }); - - 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 } - ); - }); + context('when Optimizely Full Stack is implemented', function() { + beforeEach(function() { + window.optimizelyClientInstance = { + track: sinon.stub() + }; + }); - 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 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 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' } } + } + ); + sinon.assert.calledWith( + 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() { + beforeEach(function() { + analytics.initialize(); }); - describe('#page', function() { - context('when the Optimizely Web snippet has initialized', function() { - beforeEach(function() { - mockWindowOptimizely(); - sinon.stub(window.optimizely, 'push'); - }); - - it('should send an event for a named page', function() { - var referrer = window.document.referrer; - analytics.page('Home'); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'Viewed Home Page', - tags: { - name: 'Home', - path: '/context.html', - referrer: referrer, - search: '', - title: '', - url: 'http://localhost:9876/context.html' - } - }); + context('when Optimizely Web is implemented', function() { + beforeEach(function() { + window.optimizely = { + push: sinon.stub() + }; + }); + + it('should send an event for a named page', function() { + var referrer = window.document.referrer; + analytics.page('Home'); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'Viewed Home Page', + tags: { + name: 'Home', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } }); + }); - it('should send an event for a named and categorized page', function() { - var referrer = window.document.referrer; - analytics.page('Blog', 'New Integration'); - sinon.assert.calledWith(window.optimizely.push, { - type: 'event', - eventName: 'Viewed Blog New Integration Page', - tags: { - name: 'New Integration', - category: 'Blog', - path: '/context.html', - referrer: referrer, - search: '', - title: '', - url: 'http://localhost:9876/context.html' - } - }); + it('should send an event for a named and categorized page', function() { + var referrer = window.document.referrer; + analytics.page('Blog', 'New Integration'); + sinon.assert.calledWith(window.optimizely.push, { + type: 'event', + eventName: 'Viewed Blog New Integration Page', + tags: { + name: 'New Integration', + category: 'Blog', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } }); }); }); From 099f98ba5b8bbb2e088c81baf746607280057c96 Mon Sep 17 00:00:00 2001 From: nikhil Date: Wed, 17 Jun 2020 16:05:19 -0700 Subject: [PATCH 04/17] yarn add lodash --dev --- integrations/optimizely/package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/integrations/optimizely/package.json b/integrations/optimizely/package.json index 86ba169e5..12e554a49 100644 --- a/integrations/optimizely/package.json +++ b/integrations/optimizely/package.json @@ -47,6 +47,7 @@ "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/yarn.lock b/yarn.lock index e2b0c8221..ac84a095e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6220,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" From ed636c0a23d189fe15926d24356c1ede323e400a Mon Sep 17 00:00:00 2001 From: nikhil Date: Wed, 17 Jun 2020 17:15:04 -0700 Subject: [PATCH 05/17] Update HISTORY.md --- integrations/optimizely/HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/integrations/optimizely/HISTORY.md b/integrations/optimizely/HISTORY.md index 7b7d52a32..cc53e4ad0 100644 --- a/integrations/optimizely/HISTORY.md +++ b/integrations/optimizely/HISTORY.md @@ -6,6 +6,7 @@ * 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 ================== From d2211d79789da1c18e17a5e075db2df816a65a3a Mon Sep 17 00:00:00 2001 From: nikhil Date: Wed, 12 Aug 2020 19:14:20 -0700 Subject: [PATCH 06/17] Fix lint --- integrations/optimizely/lib/index.js | 2 +- integrations/optimizely/test/index.test.js | 46 +++++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 63d00b000..6e626baae 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -289,7 +289,7 @@ Optimizely.prototype.initWebIntegration = function() { name: 'initialized' }, handler: function() { - var state = window.optimizely.get && window.optimizely.get('state'); + state = window.optimizely.get && window.optimizely.get('state'); if (state) { self.setRedirectInfo(state.getRedirectInfo()); } diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 347d63902..54670f4a3 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -9,6 +9,25 @@ 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 */ @@ -103,7 +122,9 @@ var mockWindowOptimizely = function() { }, getRedirectInfo: function() { var campaigns = this.getCampaignStates({ isActive: true }); - for (var id in campaigns) { + 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, @@ -257,7 +278,7 @@ describe('Optimizely', function() { ] }); assert.equal(initializedCalls.length, 1); - // Actually simulated an 'initialized' event. + // Actually simulate an 'initialized' event. initializedCalls[0].args[0].handler(); sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { @@ -289,7 +310,7 @@ describe('Optimizely', function() { ] }); assert.equal(initializedCalls.length, 1); - // Actually simulated an 'initialized' event. + // Actually simulate an 'initialized' event. initializedCalls[0].args[0].handler(); sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); @@ -987,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); - } - }); -} From baf72226ce9f350d95f4dcba936aa18b9e5bb4d5 Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 17 Jun 2020 14:00:18 -0700 Subject: [PATCH 07/17] edge implementation --- integrations/optimizely/HISTORY.md | 4 + integrations/optimizely/lib/index.js | 147 ++++++- integrations/optimizely/test/index.test.js | 477 +++++++++++++++++++++ 3 files changed, 615 insertions(+), 13 deletions(-) diff --git a/integrations/optimizely/HISTORY.md b/integrations/optimizely/HISTORY.md index cc53e4ad0..093d5b84f 100644 --- a/integrations/optimizely/HISTORY.md +++ b/integrations/optimizely/HISTORY.md @@ -1,3 +1,7 @@ +4.1.0 / 2020-07-13 +================== + + * Track conversion events using `window.optimizelyEdge` instead of `window.optimizely` if Optimizely Performance Edge is running on the page. 4.0.0 / 2020-06-12 ================== diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 6e626baae..2a655fcf8 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -10,6 +10,7 @@ var foldl = require('@ndhoule/foldl'); var each = require('@ndhoule/each'); var integration = require('@segment/analytics.js-integration'); var push = require('global-queue')('optimizely', { wrap: false }); +var edgePush = require('global-queue')('optimizelyEdge', { wrap: false }); var tick = require('next-tick'); /** @@ -42,17 +43,33 @@ var optimizelyContext = { Optimizely.prototype.initialize = function() { var self = this; // Flag source of integration (requested by Optimizely) - push({ - type: 'integration', - OAuthClientId: '5360906403' - }); - // 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() { - self.initWebIntegration(); - }); + if (window.optimizelyEdge) { + edgePush({ + type: 'integration', + OAuthClientId: '5360906403' + }); + + // Initialize listeners for Optimizely Edge decisions. + // We're calling 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() { + self.initEdgeIntegration(); + }); + } else { + push({ + type: 'integration', + OAuthClientId: '5360906403' + }); + + // Initialize listeners for Optimizely Web decisions. + // We're calling 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() { + self.initWebIntegration(); + }); + } this.ready(); }; @@ -101,8 +118,13 @@ Optimizely.prototype.track = function(track) { tags: eventProperties }; - // Track via Optimizely Web - push(payload); + if (window.optimizelyEdge) { + // Track via Optimizely Edge + edgePush(payload); + } else { + // Track via Optimizely Web + push(payload); + } // Track via Optimizely Full Stack var optimizelyClientInstance = window.optimizelyClientInstance; @@ -242,6 +264,31 @@ Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { } }; +/** + * TODO: description + * @param {Object} experimentState + */ +Optimizely.prototype.sendEdgeExperimentData = function(experimentState) { + var variation = experimentState.variation; + var context = { integration: optimizelyContext }; // backward compatibility + + // Send data via `.track()` + if (this.options.listen) { + var props = { + experimentId: experimentState.id, + experimentName: experimentState.name, + variationName: variation.name, + variationId: variation.id + }; + + // For Google's nonInteraction flag + if (this.options.nonInteraction) props.nonInteraction = 1; + + // Send to Segment + this.analytics.track('Experiment Viewed', props, context); + } +}; + /** * setRedirectInfo * @@ -347,3 +394,77 @@ Optimizely.prototype.initWebIntegration = function() { registerCurrentlyActiveCampaigns(); registerFutureActiveCampaigns(); }; + +/** + * TODO: description + */ +Optimizely.prototype.initEdgeIntegration = function() { + var edgeActiveExperiment = function(id) { + var state = + window.optimizelyEdge && + window.optimizelyEdge.get && + window.optimizelyEdge.get('state'); + if (state) { + var allActiveExperiments = state.getActiveExperiments(); + var experimentState = allActiveExperiments[id]; + + // TODO: referrer + self.sendEdgeExperimentData(experimentState); + } + }; + + /** + * If this code is running after Optimizely on the page, there might already be + * some experiments active. This function makes sure all those experiments are + * handled. + */ + var registerCurrentlyActiveEdgeExperiment = function() { + window.optimizelyEdge = window.optimizelyEdge || []; + var edgeState = + window.optimizelyEdge.get && window.optimizelyEdge.get('state'); + if (edgeState) { + var activeExperiments = edgeState.getActiveExperiments(); + for (var id in activeExperiments) { + if ({}.hasOwnProperty.call(activeExperiments, id)) { + edgeActiveExperiment(id); + } + } + } else { + window.optimizely.push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + // check referrer + // TODO: implement a way to get redirect info in Edge. + } + }); + } + }; + + /** + * At any moment, a new Edge experiment can be activated (manual or conditional activation). + * This function registers a listener that listens to newly activated Edge experiment and + * handles them. + */ + var registerFutureActiveEdgeExperiment = function() { + push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: function(event) { + var experimentId = event.data.decision.experimentId; + if (experimentId && event.data.decision.variationId) { + edgeActiveExperiment(experimentId); + } + } + }); + }; + + registerCurrentlyActiveEdgeExperiment(); + registerFutureActiveEdgeExperiment(); +}; diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 54670f4a3..a35712003 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -142,6 +142,83 @@ var mockWindowOptimizely = function() { }; }; +var mockWindowEdge = function() { + window.optimizelyEdge = { + edgeMockData: { + 7522212694: { + id: '7522212694', + name: 'Wells Fargo Scam', + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + }, + // these are returned by real Optimizely API but will not be send to integrations + isActive: false, + reason: undefined, + visitorRedirected: true + }, + 7547682694: { + id: '7547682694', + name: 'Worlds Group Stage', + variation: { + id: '7557950020', + name: 'Variation #1' + }, + // these are returned by real Optimizely API but will not be send to integrations + isActive: true, + reason: undefined, + visitorRedirected: false + }, + 1111182111: { + id: '1111182111', + name: 'Coding Bootcamp', + variation: { + id: '7333333333', + name: 'Variation DBC' + }, + // these are returned by real Optimizely API but will not be send to integrations + isActive: true, + reason: undefined, + visitorRedirected: false + } + }, + + get: function() { + return { + getActiveExperiments: function() { + var data = _.filter(window.optimizelyEdge.newMockData, { + isActive: true + }); + + data = Object.keys(data).map(function(key) { + return data[key]; + }); + var formatExperiment = function(experimentState) { + return { + id: experimentState.id, + name: experimentState.name, + variation: { + id: experimentState.variation.id, + name: experimentState.variation.name + } + }; + }; + + /* eslint-disable no-param-reassign */ + return data.reduce(function(activeExperiments, experiment) { + var formatted = formatExperiment(experiment); + activeExperiments[formatted.id] = formatted; + return activeExperiments; + }, {}); + /* eslint-enable no-param-reassign */ + } + }; + }, + + push: sinon.stub() + }; +}; + // passed into context.integration (not context.integrations!) for all track calls for some reason var optimizelyContext = { name: 'optimizely', @@ -199,6 +276,28 @@ describe('Optimizely', function() { OAuthClientId: '5360906403' }); }); + + describe('on an Edge page', function() { + beforeEach(function() { + sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); + sinon.stub(window.optimizelyEdge, 'push'); + window.optimizelyEdge = []; + }); + + it('should call initEdgeIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledWith(optimizely.initEdgeIntegration); + sinon.assert.notCalled(optimizely.initWebIntegration); + }); + }); + + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + }); + }); }); describe('#setRedirectInfo', function() { @@ -582,6 +681,135 @@ describe('Optimizely', function() { }); }); + describe('#initEdgeIntegration', function() { + beforeEach(function() { + sinon.stub(optimizely, 'sendEdgeExperimentData'); + }); + + context('after the Optimizely Edge microsnippet has loaded', function() { + beforeEach(function() { + mockWindowEdge(); + }); + + it('calls sendEdgeExperimentData for active Optimizely Edge experiments', function() { + optimizely.initEdgeIntegration(); + sinon.assert.calledTwice(optimizely.sendEdgeExperimentData); + // fix + sinon.assert.calledWith({ + id: '7522212694', + name: 'Wells Fargo Scam', + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + }, + // these are returned by real Optimizely API but will not be send to integrations + isActive: false, + reason: undefined, + visitorRedirected: true + }); + sinon.assert.calledWith({ + id: '7547682694', + name: 'Worlds Group Stage', + variation: { + id: '7557950020', + name: 'Variation #1' + }, + // these are returned by real Optimizely API but will not be send to integrations + isActive: true, + reason: undefined, + visitorRedirected: false + }); + }); + + // until Edge supports its API counterparts, use Web's API's for now. + it('listens for future experiment activations', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: sinon.match.function + }); + }); + }); + + context('before the Edge microsnippet has loaded', function() { + beforeEach(function() { + window.optimizelyEdge = { + push: sinon.stub() + }; + optimizely.initEdgeIntegration(); + }); + + it('defers the redirect check until snippet initialization', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: sinon.match.function + }); + }); + + // Edge does not have a way to handle redirect info. + context.skip('once the snippet finally initializes', function() { + beforeEach(function() { + // by default mock data has no redirect experiments active + mockWindowEdge(); + }); + + context('if a redirect experiment has executed', function() { + beforeEach(function() { + mockWindowOptimizely(); + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; + }); + + it('captures redirect info', function() { + window.optimizely.push.firstCall.args[0].handler(); + sinon.assert.calledOnce(optimizely.setRedirectInfo); + sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { + experimentId: 'TODO', + variationId: 'TODO', + referrer: 'barstools.com' + }); + }); + }); + + context("if a redirect experiment hasn't executed", function() { + it('does not capture redirect info', function() { + window.optimizely.push.firstCall.args[0].handler(); + sinon.assert.notCalled(optimizely.setRedirectInfo); + }); + }); + }); + + it('does not immediately call sendWebDecisionToSegment', function() { + optimizely.initEdgeIntegration(); + sinon.assert.notCalled(optimizely.sendEdgeExperimentData); + }); + + // We push to Optimizely Web (which exists silently on Edge pages), + // so this works awkwardly. + it('listens for future campaign activations', function() { + sinon.assert.calledWithExactly(window.optimizely.push, { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'campaignDecided' + }, + handler: sinon.match.function + }); + }); + + // TODO: context('when a future campaign activation occurs') + + // TODO: context('when a future campaign decision occurs without activation') + }); + }); + describe('#sendWebDecisionToSegment', function() { // TODO: Turn these into proper _unit_ tests. // * Directly call sendWebDecisionToSegment (after calling setRedirectInfo in cases where @@ -799,6 +1027,236 @@ describe('Optimizely', function() { }); }); + describe('#sendEdgeDecisionToSegment', function() { + beforeEach(function() { + mockWindowOptimizely(); + }); + + context('options.sendRevenueOnlyForOrderCompleted', function() { + beforeEach(function() { + sinon.stub(window.optimizelyEdge, 'push'); + }); + + 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 + }); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'Order Updated', + tags: {} + }); + }); + + 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 + }); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'Order Completed', + tags: { + revenue: 999 + } + }); + }); + + 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 + }); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'Checkout Started', + tags: { + revenue: 999 + } + }); + }); + }); + + context('options.listen', function() { + beforeEach(function() { + optimizely.options.listen = true; + sinon.stub(analytics, 'track'); + }); + + it('should send standard active experiment data via `.track()`', function(done) { + // Mock data by default has two active campaign/experiments. + // Going to leave just the one that was created as a standard + // experiment inside Optimizely X (not campaign) + window.optimizelyEdge.newMockData[1111182111].isActive = false; + + analytics.initialize(); + + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + experimentId: '1111182111', + experimentName: 'Coding Bootcamp', + variationId: '7333333333', + variationName: 'Variation DBC' + }, + { integration: optimizelyContext } + ]); + }); + }); + + it('should send personalized experiment data via `.track()`', function(done) { + // Mock data by default has two active experiments. + // Going to leave just the personalized experiment + window.optimizelyEdge.newMockData[7547682694].isActive = false; + analytics.initialize(); + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + experimentId: '7547682694', + experimentName: 'Worlds Group Stage', + variationId: '7557950020', + variationName: 'Variation #1' + }, + { integration: optimizelyContext } + ]); + }); + }); + + it('should map custom properties and send campaign data via `.track()`', function(done) { + optimizely.options.customCampaignProperties = { + experimentId: 'experiment_id', + experimentName: 'experiment_name' + }; + + window.optimizelyEdge.newMockData.experiment_id = '124'; + window.optimizelyEdge.newMockData.experiment_name = + 'custom experiment name'; + + window.optimizelyEdge.newMockData[7547682694].isActive = false; + analytics.initialize(); + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + experimentId: '124', + experimentName: 'custom experiment name', + variationId: '7557950020', + variationName: 'Variation #1' + }, + { integration: optimizelyContext } + ]); + }); + }); + + it('should not map existing properties if custom properties not specified`', function(done) { + optimizely.options.customCampaignProperties = {}; + + window.optimizelyEdge.newMockData.experiment_id = '124'; + window.optimizelyEdge.newMockData.experiment_name = + 'custom experiment name'; + + window.optimizelyEdge.newMockData[7547682694].isActive = false; + analytics.initialize(); + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + experimentId: '7547682694', + experimentName: 'Worlds Group Stage', + variationId: '7557950020', + variationName: 'Variation #1' + }, + { integration: optimizelyContext } + ]); + }); + }); + + // Edge does not have a way to retrieve redirect info + it.skip('should send redirect experiment data via `.track()`', function(done) { + // Enable just the campaign with redirect variation + window.optimizely.newMockData[2347102720].isActive = true; + window.optimizely.newMockData[7547101713].isActive = false; + window.optimizely.newMockData[2542102702].isActive = false; + var context = { + integration: optimizelyContext, + page: { referrer: 'barstools.com' } + }; + analytics.initialize(); + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + campaignName: 'Get Rich or Die Tryin', + campaignId: '2347102720', + experimentId: '7522212694', + experimentName: 'Wells Fargo Scam', + variationId: '7551111120', + variationName: 'Variation Corruption #1884', + audienceId: '7100568438', + audienceName: 'Middle Class', + referrer: 'barstools.com', + isInCampaignHoldback: false + }, + context + ]); + }); + }); + + it("should send Google's nonInteraction flag via `.track()`", function(done) { + // Mock data has two active campaigns running + // For convenience, we'll disable one of them + window.optimizelyEdge.newMockData[7547682694] = false; + optimizely.options.nonInteraction = true; + analytics.initialize(); + executeAsyncTest(done, function() { + assert.deepEqual(analytics.track.args[0], [ + 'Experiment Viewed', + { + experimentId: '7547682694', + experimentName: 'Worlds Group Stage', + variationId: '7557950020', + variationName: 'Variation #1', + nonInteraction: 1 + }, + { integration: optimizelyContext } + ]); + }); + }); + + it('should not send inactive experiments', function(done) { + // deactivate all experiments + window.optimizely.newMockData[7522212694].isActive = false; + window.optimizely.newMockData[7547682694].isActive = false; + window.optimizely.newMockData[1111182111].isActive = false; + analytics.initialize(); + executeAsyncTest(done, function() { + sinon.assert.notCalled(analytics.track); + }); + }); + }); + }); + +describe('after loading', function() { + beforeEach(function(done) { + mockWindowOptimizely(); + analytics.initialize(); + analytics.page(); + analytics.once('ready', done); + }); + describe('#track', function() { beforeEach(function() { analytics.initialize(); @@ -856,6 +1314,25 @@ describe('Optimizely', function() { }); }); }); + }); + + context( + 'when the Optimizely Edge microsnippet has initialized', + function() { + beforeEach(function() { + sinon.stub(window.optimizelyEdge, 'push'); + }); + + it('should send an event', function() { + analytics.track('event'); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'event', + tags: {} + }); + }); + } + ); context('when Optimizely Full Stack is implemented', function() { beforeEach(function() { From 38e6f7de2261b9d69a31593a827d4992afaeb2ea Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 12 Aug 2020 21:31:03 -0700 Subject: [PATCH 08/17] fix unit tests --- integrations/optimizely/lib/index.js | 92 +-- integrations/optimizely/test/index.test.js | 624 +++++++++++++-------- 2 files changed, 430 insertions(+), 286 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 2a655fcf8..2c8f20c2c 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -266,9 +266,15 @@ Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { /** * TODO: description + * @api private * @param {Object} experimentState + * @param {String} experimentState.id + * @param {String} experimentState.name + * @param {Object} experimentState.variation + * @param {String} experimentState.variation.id + * @param {String} experimentState.variation.name */ -Optimizely.prototype.sendEdgeExperimentData = function(experimentState) { +Optimizely.prototype.sendEdgeExperimentToSegment = function(experimentState) { var variation = experimentState.variation; var context = { integration: optimizelyContext }; // backward compatibility @@ -396,51 +402,27 @@ Optimizely.prototype.initWebIntegration = function() { }; /** - * TODO: description + * This function fetches all active Optimizely Performance Edge experiments, + * invoking the sendEdgeExperimentToSegment callback for each one. + * + * @api private */ Optimizely.prototype.initEdgeIntegration = function() { + var self = this; + var edgeActiveExperiment = function(id) { - var state = + var edgeState = window.optimizelyEdge && window.optimizelyEdge.get && window.optimizelyEdge.get('state'); - if (state) { - var allActiveExperiments = state.getActiveExperiments(); + if (edgeState) { + var allActiveExperiments = edgeState.getActiveExperiments(); var experimentState = allActiveExperiments[id]; // TODO: referrer - self.sendEdgeExperimentData(experimentState); - } - }; - - /** - * If this code is running after Optimizely on the page, there might already be - * some experiments active. This function makes sure all those experiments are - * handled. - */ - var registerCurrentlyActiveEdgeExperiment = function() { - window.optimizelyEdge = window.optimizelyEdge || []; - var edgeState = - window.optimizelyEdge.get && window.optimizelyEdge.get('state'); - if (edgeState) { - var activeExperiments = edgeState.getActiveExperiments(); - for (var id in activeExperiments) { - if ({}.hasOwnProperty.call(activeExperiments, id)) { - edgeActiveExperiment(id); - } + if (experimentState) { + self.sendEdgeExperimentToSegment(experimentState); } - } else { - window.optimizely.push({ - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'initialized' - }, - handler: function() { - // check referrer - // TODO: implement a way to get redirect info in Edge. - } - }); } }; @@ -448,6 +430,9 @@ Optimizely.prototype.initEdgeIntegration = function() { * At any moment, a new Edge experiment can be activated (manual or conditional activation). * This function registers a listener that listens to newly activated Edge experiment and * handles them. + * + * However, the Edge API does not support a 'campaignDecided' listener. For now, we can + * utilize the Web API to listen to newly activated Edge experiments. */ var registerFutureActiveEdgeExperiment = function() { push({ @@ -459,12 +444,45 @@ Optimizely.prototype.initEdgeIntegration = function() { handler: function(event) { var experimentId = event.data.decision.experimentId; if (experimentId && event.data.decision.variationId) { - edgeActiveExperiment(experimentId); + var edgeState = + window.optimizelyEdge.get && window.optimizelyEdge.get('state'); + if (edgeState) { + var allActiveExperiments = edgeState.getActiveExperiments(); + var experimentState = allActiveExperiments[experimentId]; + if (experimentState) { + self.sendEdgeExperimentToSegment(experimentState); + } + } } } }); }; + /** + * If this code is running after Optimizely on the page, there might already be + * some experiments active. This function makes sure all those experiments are + * handled. + */ + var registerCurrentlyActiveEdgeExperiment = function() { + window.optimizelyEdge = window.optimizelyEdge || []; + var edgeState = + window.optimizelyEdge.get && window.optimizelyEdge.get('state'); + if (edgeState) { + var activeExperiments = edgeState.getActiveExperiments(); + for (var id in activeExperiments) { + if ({}.hasOwnProperty.call(activeExperiments, id)) { + edgeActiveExperiment(id); + } + } + } + }; + + // Normally, we would like to check referrer info, but we don't provide + // a 'getRedirectInfo' API in Edge. We will skip checking if an + // experiment is from a redirect. + // + // Additionally, because a track event for page requires redirect info, + // we will not be sending such event. registerCurrentlyActiveEdgeExperiment(); registerFutureActiveEdgeExperiment(); }; diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index a35712003..a5df0eded 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -186,7 +186,7 @@ var mockWindowEdge = function() { get: function() { return { getActiveExperiments: function() { - var data = _.filter(window.optimizelyEdge.newMockData, { + var data = _.filter(window.optimizelyEdge.edgeMockData, { isActive: true }); @@ -255,7 +255,7 @@ describe('Optimizely', function() { sinon.restore(); }); - describe('#initialize', function() { + describe('#initialize on Web', function() { beforeEach(function(done) { sinon.stub(Optimizely.prototype, 'initWebIntegration'); sinon.stub(window.optimizely, 'push'); @@ -264,35 +264,25 @@ describe('Optimizely', function() { analytics.page(); }); - it('should call initWebIntegration', function(done) { - executeAsyncTest(done, function() { - sinon.assert.calledWith(optimizely.initWebIntegration); - }); - }); - - it('should flag source of integration', function() { - sinon.assert.calledWith(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' + context('if on a Web page', function() { + beforeEach(function(done) { + analytics.once('ready', done); + analytics.initialize(); + analytics.page(); }); - }); - describe('on an Edge page', function() { - beforeEach(function() { - sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); - sinon.stub(window.optimizelyEdge, 'push'); - window.optimizelyEdge = []; + afterEach(function() { + analytics.reset(); }); - it('should call initEdgeIntegration', function(done) { + it('should call initWebIntegration', function(done) { executeAsyncTest(done, function() { - sinon.assert.calledWith(optimizely.initEdgeIntegration); - sinon.assert.notCalled(optimizely.initWebIntegration); + sinon.assert.calledWith(optimizely.initWebIntegration); }); }); it('should flag source of integration', function() { - sinon.assert.calledWith(window.optimizelyEdge.push, { + sinon.assert.calledWith(window.optimizely.push, { type: 'integration', OAuthClientId: '5360906403' }); @@ -300,6 +290,35 @@ describe('Optimizely', function() { }); }); + // causes another test suite to fail (#sendWebDecisionToSegment). + describe.skip('#initialize on Edge', function() { + beforeEach(function(done) { + window.optimizelyEdge = []; + sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); + sinon.stub(window.optimizelyEdge, 'push'); + analytics.once('ready', done); + analytics.initialize(); + analytics.page(); + }); + + afterEach(function() { + delete window.optimizelyEdge; + }); + + it('should call initEdgeIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledWith(optimizely.initEdgeIntegration); + }); + }); + + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + }); + }); + describe('#setRedirectInfo', function() { beforeEach(function(done) { analytics.initialize(); @@ -681,135 +700,6 @@ describe('Optimizely', function() { }); }); - describe('#initEdgeIntegration', function() { - beforeEach(function() { - sinon.stub(optimizely, 'sendEdgeExperimentData'); - }); - - context('after the Optimizely Edge microsnippet has loaded', function() { - beforeEach(function() { - mockWindowEdge(); - }); - - it('calls sendEdgeExperimentData for active Optimizely Edge experiments', function() { - optimizely.initEdgeIntegration(); - sinon.assert.calledTwice(optimizely.sendEdgeExperimentData); - // fix - sinon.assert.calledWith({ - id: '7522212694', - name: 'Wells Fargo Scam', - variation: { - id: '7551111120', - name: 'Variation Corruption #1884' - }, - // these are returned by real Optimizely API but will not be send to integrations - isActive: false, - reason: undefined, - visitorRedirected: true - }); - sinon.assert.calledWith({ - id: '7547682694', - name: 'Worlds Group Stage', - variation: { - id: '7557950020', - name: 'Variation #1' - }, - // these are returned by real Optimizely API but will not be send to integrations - isActive: true, - reason: undefined, - visitorRedirected: false - }); - }); - - // until Edge supports its API counterparts, use Web's API's for now. - it('listens for future experiment activations', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'campaignDecided' - }, - handler: sinon.match.function - }); - }); - }); - - context('before the Edge microsnippet has loaded', function() { - beforeEach(function() { - window.optimizelyEdge = { - push: sinon.stub() - }; - optimizely.initEdgeIntegration(); - }); - - it('defers the redirect check until snippet initialization', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'initialized' - }, - handler: sinon.match.function - }); - }); - - // Edge does not have a way to handle redirect info. - context.skip('once the snippet finally initializes', function() { - beforeEach(function() { - // by default mock data has no redirect experiments active - mockWindowEdge(); - }); - - context('if a redirect experiment has executed', function() { - beforeEach(function() { - mockWindowOptimizely(); - // Make sure window.optimizely.getRedirectInfo returns something - window.optimizely.newMockData[2347102720].isActive = true; - }); - - it('captures redirect info', function() { - window.optimizely.push.firstCall.args[0].handler(); - sinon.assert.calledOnce(optimizely.setRedirectInfo); - sinon.assert.alwaysCalledWith(optimizely.setRedirectInfo, { - experimentId: 'TODO', - variationId: 'TODO', - referrer: 'barstools.com' - }); - }); - }); - - context("if a redirect experiment hasn't executed", function() { - it('does not capture redirect info', function() { - window.optimizely.push.firstCall.args[0].handler(); - sinon.assert.notCalled(optimizely.setRedirectInfo); - }); - }); - }); - - it('does not immediately call sendWebDecisionToSegment', function() { - optimizely.initEdgeIntegration(); - sinon.assert.notCalled(optimizely.sendEdgeExperimentData); - }); - - // We push to Optimizely Web (which exists silently on Edge pages), - // so this works awkwardly. - it('listens for future campaign activations', function() { - sinon.assert.calledWithExactly(window.optimizely.push, { - type: 'addListener', - filter: { - type: 'lifecycle', - name: 'campaignDecided' - }, - handler: sinon.match.function - }); - }); - - // TODO: context('when a future campaign activation occurs') - - // TODO: context('when a future campaign decision occurs without activation') - }); - }); - describe('#sendWebDecisionToSegment', function() { // TODO: Turn these into proper _unit_ tests. // * Directly call sendWebDecisionToSegment (after calling setRedirectInfo in cases where @@ -1027,16 +917,320 @@ describe('Optimizely', function() { }); }); - describe('#sendEdgeDecisionToSegment', function() { + describe('#initEdgeIntegration', function() { beforeEach(function() { - mockWindowOptimizely(); + window.optimizelyEdge = []; + sinon.stub(optimizely, 'sendEdgeExperimentToSegment'); + sinon.stub(window.optimizely, 'push'); }); - context('options.sendRevenueOnlyForOrderCompleted', function() { + afterEach(function() { + delete window.optimizelyEdge; + sinon.reset(); + }); + + context('before the Edge microsnippet has loaded', function() { + var prePushStub; + + beforeEach(function() { + prePushStub = sinon.stub(); + window.optimizely = { + push: prePushStub + }; + + optimizely.initEdgeIntegration(); + }); + + // Optimizely Edge is not supporting an API to get redirect information. + context.skip('if a redirect experiment has executed', function() { + beforeEach(function() { + mockWindowEdge(); + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizelyEdge.edgeMockData[2347102720].isActive = true; + }); + + 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 simulated an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', + referrer: 'barstools.com' + }); + }); + }); + + // Optimizely Edge is not supporting an API to get redirect information. + context.skip("if a redirect experiment hasn't executed", function() { + beforeEach(function() { + // by default mock data has no redirect experiments active + mockWindowOptimizely(); + }); + + it('does not capture 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 simulated an 'initialized' event. + initializedCalls[0].args[0].handler(); + + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); + }); + }); + + it('does not immediately call sendEdgeExperimentToSegment', function() { + sinon.assert.notCalled(optimizely.sendEdgeExperimentToSegment); + }); + + context('when an experiment is finally decided', function() { + var handler; + + beforeEach(function() { + // Make sure the code is actually listening for experiments + var campaignDecidedCalls = _.filter(prePushStub.getCalls(), { + 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; + }); + + context('and the experiment is active', function() { + beforeEach(function() { + mockWindowEdge(); + window.optimizelyEdge.edgeMockData[7522212694].isActive = true; + + handler({ + data: { + decision: { + experimentId: '7522212694', + variationId: '7551111120' + } + } + }); + }); + + it('calls #sendEdgeExperimentToSegment', function() { + sinon.assert.calledWithExactly( + optimizely.sendEdgeExperimentToSegment, + sinon.match({ + id: '7522212694', + name: 'Wells Fargo Scam', + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + } + }) + ); + }); + }); + + context('and the experiment is inactive', function() { + beforeEach(function() { + mockWindowEdge(); + handler({ + data: { + decision: { + experimentId: '7522212694', + variationId: '7551111120' + } + } + }); + }); + + it('does not call #sendEdgeExperimentToSegment', function() { + sinon.assert.notCalled(optimizely.sendEdgeExperimentToSegment); + }); + }); + }); + }); + + context('after the Optimizely Edge microsnippet has loaded', function() { beforeEach(function() { - sinon.stub(window.optimizelyEdge, 'push'); + mockWindowEdge(); + }); + + // Optimizely Edge is not supporting an API to get redirect information. + context.skip('if a redirect experiment has executed', function() { + beforeEach(function() { + // Make sure window.optimizely.getRedirectInfo returns something + window.optimizely.newMockData[2347102720].isActive = true; + + optimizely.initWebIntegration(); + }); + + it('immediately captures redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, { + experimentId: '7522212694', + variationId: '7551111120', + referrer: 'barstools.com' + }); + }); + + it('captures redirect info _before_ tracking decisions', function() { + sinon.assert.callOrder( + optimizely.setRedirectInfo, + optimizely.sendWebDecisionToSegment + ); + }); + }); + + // Optimizely Edge is not supporting an API to get redirect information. + context.skip("if a redirect experiment hasn't executed", function() { + beforeEach(function() { + optimizely.initWebIntegration(); + }); + + it('does not capture redirect info', function() { + sinon.assert.calledOnceWithExactly(optimizely.setRedirectInfo, null); + }); }); + it('calls sendEdgeExperimentToSegment for active Optimizely Edge experiments', function() { + optimizely.initEdgeIntegration(); + + sinon.assert.calledTwice(optimizely.sendEdgeExperimentToSegment); + sinon.assert.calledWithExactly(optimizely.sendEdgeExperimentToSegment, { + id: '1111182111', + name: 'Coding Bootcamp', + variation: { + id: '7333333333', + name: 'Variation DBC' + } + }); + + sinon.assert.calledWithExactly(optimizely.sendEdgeExperimentToSegment, { + id: '7547682694', + name: 'Worlds Group Stage', + variation: { + id: '7557950020', + name: 'Variation #1' + } + }); + }); + + context('when a future experiment is decided', function() { + var handler; + + beforeEach(function() { + optimizely.initEdgeIntegration(); + // Forget about the initial campaigns that were tracked. + optimizely.sendEdgeExperimentToSegment.resetHistory(); + + // Make sure the code is actually listening for campaign decisions + var campaignDecidedCalls = _.filter( + window.optimizely.push.getCalls(), + { + 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; + }); + + context('and the experiment is active', function() { + beforeEach(function() { + window.optimizelyEdge.edgeMockData[7522212694].isActive = true; + + handler({ + data: { + decision: { + experimentId: '7522212694', + variationId: '7551111120' + } + } + }); + }); + + it('calls #sendEdgeExperimentToSegment', function() { + sinon.assert.calledWithExactly( + optimizely.sendEdgeExperimentToSegment, + sinon.match({ + id: '7522212694', + name: 'Wells Fargo Scam', + variation: { + id: '7551111120', + name: 'Variation Corruption #1884' + } + }) + ); + }); + }); + + context('and the experiment is inactive', function() { + beforeEach(function() { + handler({ + data: { + decision: { + experimentId: '7522212694', + variationId: '7551111120' + } + } + }); + }); + + it('does not call #sendEdgeExperimentToSegment', function() { + sinon.assert.notCalled(optimizely.sendEdgeExperimentToSegment); + }); + }); + }); + }); + }); + + describe('#sendEdgeDecisionToSegment', function() { + beforeEach(function() { + window.optimizelyEdge = []; + mockWindowEdge(); + }); + + afterEach(function() { + delete window.optimizelyEdge; + }); + + 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); @@ -1098,77 +1292,7 @@ describe('Optimizely', function() { // Mock data by default has two active campaign/experiments. // Going to leave just the one that was created as a standard // experiment inside Optimizely X (not campaign) - window.optimizelyEdge.newMockData[1111182111].isActive = false; - - analytics.initialize(); - - executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '1111182111', - experimentName: 'Coding Bootcamp', - variationId: '7333333333', - variationName: 'Variation DBC' - }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should send personalized experiment data via `.track()`', function(done) { - // Mock data by default has two active experiments. - // Going to leave just the personalized experiment - window.optimizelyEdge.newMockData[7547682694].isActive = false; - analytics.initialize(); - executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '7547682694', - experimentName: 'Worlds Group Stage', - variationId: '7557950020', - variationName: 'Variation #1' - }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should map custom properties and send campaign data via `.track()`', function(done) { - optimizely.options.customCampaignProperties = { - experimentId: 'experiment_id', - experimentName: 'experiment_name' - }; - - window.optimizelyEdge.newMockData.experiment_id = '124'; - window.optimizelyEdge.newMockData.experiment_name = - 'custom experiment name'; - - window.optimizelyEdge.newMockData[7547682694].isActive = false; - analytics.initialize(); - executeAsyncTest(done, function() { - assert.deepEqual(analytics.track.args[0], [ - 'Experiment Viewed', - { - experimentId: '124', - experimentName: 'custom experiment name', - variationId: '7557950020', - variationName: 'Variation #1' - }, - { integration: optimizelyContext } - ]); - }); - }); - - it('should not map existing properties if custom properties not specified`', function(done) { - optimizely.options.customCampaignProperties = {}; - - window.optimizelyEdge.newMockData.experiment_id = '124'; - window.optimizelyEdge.newMockData.experiment_name = - 'custom experiment name'; - - window.optimizelyEdge.newMockData[7547682694].isActive = false; + window.optimizelyEdge.edgeMockData[1111182111].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { assert.deepEqual(analytics.track.args[0], [ @@ -1218,17 +1342,19 @@ describe('Optimizely', function() { it("should send Google's nonInteraction flag via `.track()`", function(done) { // Mock data has two active campaigns running // For convenience, we'll disable one of them - window.optimizelyEdge.newMockData[7547682694] = false; + window.optimizelyEdge.edgeMockData[7547682694] = false; optimizely.options.nonInteraction = true; analytics.initialize(); executeAsyncTest(done, function() { + console.log(analytics.track.args[0]); + assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { - experimentId: '7547682694', - experimentName: 'Worlds Group Stage', - variationId: '7557950020', - variationName: 'Variation #1', + experimentId: '1111182111', + experimentName: 'Coding Bootcamp', + variationId: '7333333333', + variationName: 'Variation DBC', nonInteraction: 1 }, { integration: optimizelyContext } @@ -1238,9 +1364,9 @@ describe('Optimizely', function() { it('should not send inactive experiments', function(done) { // deactivate all experiments - window.optimizely.newMockData[7522212694].isActive = false; - window.optimizely.newMockData[7547682694].isActive = false; - window.optimizely.newMockData[1111182111].isActive = false; + window.optimizelyEdge.edgeMockData[7522212694].isActive = false; + window.optimizelyEdge.edgeMockData[7547682694].isActive = false; + window.optimizelyEdge.edgeMockData[1111182111].isActive = false; analytics.initialize(); executeAsyncTest(done, function() { sinon.assert.notCalled(analytics.track); @@ -1249,14 +1375,6 @@ describe('Optimizely', function() { }); }); -describe('after loading', function() { - beforeEach(function(done) { - mockWindowOptimizely(); - analytics.initialize(); - analytics.page(); - analytics.once('ready', done); - }); - describe('#track', function() { beforeEach(function() { analytics.initialize(); @@ -1314,13 +1432,19 @@ describe('after loading', function() { }); }); }); - }); - context( - 'when the Optimizely Edge microsnippet has initialized', - function() { + context('when Optimizely Edge microsnippet is initialized', function() { beforeEach(function() { - sinon.stub(window.optimizelyEdge, 'push'); + window.optimizelyEdge = { + push: sinon.stub() + }; + window.optimizely = { + push: sinon.stub() + }; + }); + + afterEach(function() { + delete window.optimizelyEdge; }); it('should send an event', function() { @@ -1330,9 +1454,11 @@ describe('after loading', function() { eventName: 'event', tags: {} }); + + // does not send a duplicate event under Web + sinon.assert.notCalled(window.optimizely.push); }); - } - ); + }); context('when Optimizely Full Stack is implemented', function() { beforeEach(function() { From 394547acce3ebc9d5f5385bebf53572981531ab5 Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 12 Aug 2020 21:35:00 -0700 Subject: [PATCH 09/17] some clean up --- integrations/optimizely/test/index.test.js | 28 +++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index a5df0eded..f8766add1 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -264,28 +264,16 @@ describe('Optimizely', function() { analytics.page(); }); - context('if on a Web page', function() { - beforeEach(function(done) { - analytics.once('ready', done); - analytics.initialize(); - analytics.page(); - }); - - afterEach(function() { - analytics.reset(); - }); - - it('should call initWebIntegration', function(done) { - executeAsyncTest(done, function() { - sinon.assert.calledWith(optimizely.initWebIntegration); - }); + it('should call initWebIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledWith(optimizely.initWebIntegration); }); + }); - it('should flag source of integration', function() { - sinon.assert.calledWith(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizely.push, { + type: 'integration', + OAuthClientId: '5360906403' }); }); }); From 07448845dcceab98dec19d1884360fa245bf31ed Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 12 Aug 2020 22:07:12 -0700 Subject: [PATCH 10/17] final cleanup --- integrations/optimizely/lib/index.js | 33 +++---- integrations/optimizely/test/index.test.js | 106 ++++++++++----------- 2 files changed, 63 insertions(+), 76 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 2c8f20c2c..dc7afe3e0 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -265,7 +265,11 @@ Optimizely.prototype.sendWebDecisionToSegment = function(campaignState) { }; /** - * TODO: description + * sendEdgeExperimentToSegment (Optimizely Performance Edge) + * + * This function is called for each experiment created in Performance Edge that are running on the page. + * This function will also be executed for any experiments activated at a later stage since initEdgeIntegration + * attached listeners on the page. Currently, those listeners leverage the Web API. * @api private * @param {Object} experimentState * @param {String} experimentState.id @@ -410,22 +414,6 @@ Optimizely.prototype.initWebIntegration = function() { Optimizely.prototype.initEdgeIntegration = function() { var self = this; - var edgeActiveExperiment = function(id) { - var edgeState = - window.optimizelyEdge && - window.optimizelyEdge.get && - window.optimizelyEdge.get('state'); - if (edgeState) { - var allActiveExperiments = edgeState.getActiveExperiments(); - var experimentState = allActiveExperiments[id]; - - // TODO: referrer - if (experimentState) { - self.sendEdgeExperimentToSegment(experimentState); - } - } - }; - /** * At any moment, a new Edge experiment can be activated (manual or conditional activation). * This function registers a listener that listens to newly activated Edge experiment and @@ -469,11 +457,12 @@ Optimizely.prototype.initEdgeIntegration = function() { window.optimizelyEdge.get && window.optimizelyEdge.get('state'); if (edgeState) { var activeExperiments = edgeState.getActiveExperiments(); - for (var id in activeExperiments) { - if ({}.hasOwnProperty.call(activeExperiments, id)) { - edgeActiveExperiment(id); + + each(function(experimentState) { + if (experimentState) { + self.sendEdgeExperimentToSegment(experimentState); } - } + }, activeExperiments); } }; @@ -481,7 +470,7 @@ Optimizely.prototype.initEdgeIntegration = function() { // a 'getRedirectInfo' API in Edge. We will skip checking if an // experiment is from a redirect. // - // Additionally, because a track event for page requires redirect info, + // Additionally, because a page event requires redirect info, // we will not be sending such event. registerCurrentlyActiveEdgeExperiment(); registerFutureActiveEdgeExperiment(); diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index f8766add1..14fe8666f 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -255,58 +255,6 @@ describe('Optimizely', function() { sinon.restore(); }); - describe('#initialize on Web', function() { - beforeEach(function(done) { - sinon.stub(Optimizely.prototype, 'initWebIntegration'); - sinon.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 flag source of integration', function() { - sinon.assert.calledWith(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); - }); - }); - - // causes another test suite to fail (#sendWebDecisionToSegment). - describe.skip('#initialize on Edge', function() { - beforeEach(function(done) { - window.optimizelyEdge = []; - sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); - sinon.stub(window.optimizelyEdge, 'push'); - analytics.once('ready', done); - analytics.initialize(); - analytics.page(); - }); - - afterEach(function() { - delete window.optimizelyEdge; - }); - - it('should call initEdgeIntegration', function(done) { - executeAsyncTest(done, function() { - sinon.assert.calledWith(optimizely.initEdgeIntegration); - }); - }); - - it('should flag source of integration', function() { - sinon.assert.calledWith(window.optimizelyEdge.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); - }); - }); - describe('#setRedirectInfo', function() { beforeEach(function(done) { analytics.initialize(); @@ -1334,8 +1282,6 @@ describe('Optimizely', function() { optimizely.options.nonInteraction = true; analytics.initialize(); executeAsyncTest(done, function() { - console.log(analytics.track.args[0]); - assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { @@ -1598,4 +1544,56 @@ describe('Optimizely', function() { }); }); }); + + describe('#initialize on Web', function() { + beforeEach(function(done) { + sinon.stub(Optimizely.prototype, 'initWebIntegration'); + sinon.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 flag source of integration', function() { + sinon.assert.calledWith(window.optimizely.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + }); + }); + + // causes another test suite to fail (#sendWebDecisionToSegment). + describe('#initialize on Edge', function() { + beforeEach(function(done) { + window.optimizelyEdge = []; + sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); + sinon.stub(window.optimizelyEdge, 'push'); + analytics.once('ready', done); + analytics.initialize(); + analytics.page(); + }); + + afterEach(function() { + delete window.optimizelyEdge; + }); + + it('should call initEdgeIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledWith(optimizely.initEdgeIntegration); + }); + }); + + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + }); + }); }); From ac7995b5811fae8d02b4316344a54fea36314279 Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Fri, 28 Aug 2020 11:46:59 -0700 Subject: [PATCH 11/17] cr --- integrations/optimizely/lib/index.js | 99 +++++++++++++++++++--------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index dc7afe3e0..9aa1d3686 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -42,35 +42,74 @@ var optimizelyContext = { Optimizely.prototype.initialize = function() { var self = this; - // Flag source of integration (requested by Optimizely) - if (window.optimizelyEdge) { - edgePush({ - type: 'integration', - OAuthClientId: '5360906403' - }); - - // Initialize listeners for Optimizely Edge decisions. - // We're calling 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() { - self.initEdgeIntegration(); - }); - } else { - push({ - type: 'integration', - OAuthClientId: '5360906403' - }); - - // Initialize listeners for Optimizely Web decisions. - // We're calling 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() { - self.initWebIntegration(); - }); - } + window.optimizelyEdge.push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + // Initialize the Edge integration if that hasn't been done already + if (window.optimizelyEdge && window.optimizelyEdge.get) { + edgePush({ + type: 'integration', + // Flag source of integration (requested by Optimizely) + OAuthClientId: '5360906403' + }); + + // Initialize listeners for Optimizely Edge decisions. + // We're calling 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() { + self.initEdgeIntegration(); + }); + } + } + }); + window.optimizely.push({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + // We have to check this because Edge microsnippets currently process + // window.optimizely calls and not just window.optimizelyEdge calls. + if (window.optimizelyEdge && window.optimizelyEdge.get) { + // Initialize the Edge integration if that hasn't been done already + edgePush({ + type: 'integration', + // Flag source of integration (requested by Optimizely) + OAuthClientId: '5360906403' + }); + + // Initialize listeners for Optimizely Edge decisions. + // We're calling 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() { + self.initEdgeIntegration(); + }); + } else if (window.optimizely && window.optimizely.get) { + // Initialize the Web integration + push({ + type: 'integration', + // Flag source of integration (requested by Optimizely) + OAuthClientId: '5360906403' + }); + + // Initialize listeners for Optimizely Web decisions. + // We're calling 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() { + self.initWebIntegration(); + }); + } + } + }); this.ready(); }; @@ -118,7 +157,7 @@ Optimizely.prototype.track = function(track) { tags: eventProperties }; - if (window.optimizelyEdge) { + if (window.optimizelyEdge && window.optimizelyEdge.get) { // Track via Optimizely Edge edgePush(payload); } else { @@ -423,7 +462,7 @@ Optimizely.prototype.initEdgeIntegration = function() { * utilize the Web API to listen to newly activated Edge experiments. */ var registerFutureActiveEdgeExperiment = function() { - push({ + edgePush({ type: 'addListener', filter: { type: 'lifecycle', From 6bfeef1434b45511b249f15f52f48d9bae5c0ed0 Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Tue, 22 Sep 2020 19:58:25 -0700 Subject: [PATCH 12/17] changing of unit tests part 1 --- integrations/optimizely/lib/index.js | 38 +++- integrations/optimizely/test/index.test.js | 213 ++++++++++++--------- 2 files changed, 158 insertions(+), 93 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index 9aa1d3686..d4cf2d153 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -330,6 +330,14 @@ Optimizely.prototype.sendEdgeExperimentToSegment = function(experimentState) { variationId: variation.id }; + 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; @@ -453,6 +461,29 @@ Optimizely.prototype.initWebIntegration = function() { Optimizely.prototype.initEdgeIntegration = function() { var self = this; + var checkReferrer = function() { + var edgeState = + window.optimizelyEdge.get && window.optimizelyEdge.get('state'); + if (edgeState) { + self.setRedirectInfo(edgeState.getRedirectInfo()); + } else { + edgePush({ + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: function() { + edgeState = + window.optimizelyEdge.get && window.optimizelyEdge.get('state'); + if (edgeState) { + self.setRedirectInfo(edgeState.getRedirectInfo()); + } + } + }); + } + }; + /** * At any moment, a new Edge experiment can be activated (manual or conditional activation). * This function registers a listener that listens to newly activated Edge experiment and @@ -505,12 +536,7 @@ Optimizely.prototype.initEdgeIntegration = function() { } }; - // Normally, we would like to check referrer info, but we don't provide - // a 'getRedirectInfo' API in Edge. We will skip checking if an - // experiment is from a redirect. - // - // Additionally, because a page event requires redirect info, - // we will not be sending such event. + checkReferrer(); registerCurrentlyActiveEdgeExperiment(); registerFutureActiveEdgeExperiment(); }; diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 14fe8666f..309e7c792 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -28,6 +28,29 @@ function executeAsyncTest(done, test) { }); } +/** + * + * @api private + * @param {*} prePushStub the stub of the prePushed global + */ +function invokeHandler(prePushStub) { + 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(); +} + /** * Test account: han@segment.com */ @@ -211,6 +234,21 @@ var mockWindowEdge = function() { return activeExperiments; }, {}); /* eslint-enable no-param-reassign */ + }, + 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; } }; }, @@ -226,8 +264,8 @@ var optimizelyContext = { }; describe('Optimizely', function() { - this.timeout(0); - + var preWebPushStub; + var preEdgePushStub; var analytics; var optimizely; var options = { @@ -244,7 +282,16 @@ describe('Optimizely', function() { analytics.use(Optimizely); analytics.use(tester); analytics.add(optimizely); - window.optimizely = []; + + preEdgePushStub = sinon.stub(); + window.optimizelyEdge = { + push: preEdgePushStub + }; + + preWebPushStub = sinon.stub(); + window.optimizely = { + push: preWebPushStub + }; }); afterEach(function() { @@ -253,6 +300,65 @@ describe('Optimizely', function() { optimizely.reset(); sandbox(); sinon.restore(); + sinon.reset(); + }); + + describe('#initialize', function() { + beforeEach(function(done) { + sinon.stub(Optimizely.prototype, 'initWebIntegration'); + sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); + + analytics.once('ready', done); + analytics.initialize(); + }); + + it('should push initialize listener to window.optimizely and window.optimizelyEdge', function() { + sinon.assert.calledOnce(preWebPushStub); + sinon.assert.calledOnce(preEdgePushStub); + }); + + context('when Optimizely Web is initialized', function() { + beforeEach(function() { + mockWindowOptimizely(); + invokeHandler(preWebPushStub); + }); + + it('should call initWebIntegration and not call initEdgeIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledOnce(Optimizely.prototype.initWebIntegration); + sinon.assert.notCalled(Optimizely.prototype.initEdgeIntegration); + }); + }); + + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizely.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + // initial call to push `initialize` listener + sinon.assert.calledOnce(window.optimizelyEdge.push); + }); + }); + + context('when Optimizely Edge is initialized', function() { + beforeEach(function() { + mockWindowEdge(); + invokeHandler(preEdgePushStub); + }); + + it('should call initEdgeIntegration', function(done) { + executeAsyncTest(done, function() { + sinon.assert.calledOnce(Optimizely.prototype.initEdgeIntegration); + }); + }); + + it('should flag source of integration', function() { + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'integration', + OAuthClientId: '5360906403' + }); + }); + }); }); describe('#setRedirectInfo', function() { @@ -645,7 +751,9 @@ describe('Optimizely', function() { // with particular arguments. beforeEach(function() { + analytics.initialize(); mockWindowOptimizely(); + invokeHandler(preWebPushStub); }); context('options.variations', function() { @@ -855,25 +963,13 @@ describe('Optimizely', function() { describe('#initEdgeIntegration', function() { beforeEach(function() { - window.optimizelyEdge = []; sinon.stub(optimizely, 'sendEdgeExperimentToSegment'); - sinon.stub(window.optimizely, 'push'); - }); - - afterEach(function() { - delete window.optimizelyEdge; - sinon.reset(); }); context('before the Edge microsnippet has loaded', function() { var prePushStub; beforeEach(function() { - prePushStub = sinon.stub(); - window.optimizely = { - push: prePushStub - }; - optimizely.initEdgeIntegration(); }); @@ -949,7 +1045,7 @@ describe('Optimizely', function() { beforeEach(function() { // Make sure the code is actually listening for experiments - var campaignDecidedCalls = _.filter(prePushStub.getCalls(), { + var experimentDecidedCalls = _.filter(preEdgePushStub.getCalls(), { args: [ { type: 'addListener', @@ -960,9 +1056,9 @@ describe('Optimizely', function() { } ] }); - assert.equal(campaignDecidedCalls.length, 1); + assert.equal(experimentDecidedCalls.length, 1); // We'll call this later in order to simulate the 'campaignDecided' event. - handler = campaignDecidedCalls[0].args[0].handler; + handler = experimentDecidedCalls[0].args[0].handler; }); context('and the experiment is active', function() { @@ -1089,7 +1185,7 @@ describe('Optimizely', function() { // Make sure the code is actually listening for campaign decisions var campaignDecidedCalls = _.filter( - window.optimizely.push.getCalls(), + window.optimizelyEdge.push.getCalls(), { args: [ { @@ -1158,12 +1254,9 @@ describe('Optimizely', function() { describe('#sendEdgeDecisionToSegment', function() { beforeEach(function() { - window.optimizelyEdge = []; + analytics.initialize(); mockWindowEdge(); - }); - - afterEach(function() { - delete window.optimizelyEdge; + invokeHandler(preEdgePushStub); }); context('options.sendRevenueOnlyForOrderCompleted', function() { @@ -1319,6 +1412,8 @@ describe('Optimizely', function() { window.optimizely = { push: sinon.stub() }; + mockWindowOptimizely(); + invokeHandler(preWebPushStub); }); it('should send an event', function() { @@ -1369,16 +1464,8 @@ describe('Optimizely', function() { context('when Optimizely Edge microsnippet is initialized', function() { beforeEach(function() { - window.optimizelyEdge = { - push: sinon.stub() - }; - window.optimizely = { - push: sinon.stub() - }; - }); - - afterEach(function() { - delete window.optimizelyEdge; + mockWindowEdge(); + invokeHandler(preEdgePushStub); }); it('should send an event', function() { @@ -1390,7 +1477,11 @@ describe('Optimizely', function() { }); // does not send a duplicate event under Web - sinon.assert.notCalled(window.optimizely.push); + sinon.assert.neverCalledWith(window.optimizely.push, { + type: 'event', + eventName: 'event', + tags: {} + }); }); }); @@ -1544,56 +1635,4 @@ describe('Optimizely', function() { }); }); }); - - describe('#initialize on Web', function() { - beforeEach(function(done) { - sinon.stub(Optimizely.prototype, 'initWebIntegration'); - sinon.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 flag source of integration', function() { - sinon.assert.calledWith(window.optimizely.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); - }); - }); - - // causes another test suite to fail (#sendWebDecisionToSegment). - describe('#initialize on Edge', function() { - beforeEach(function(done) { - window.optimizelyEdge = []; - sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); - sinon.stub(window.optimizelyEdge, 'push'); - analytics.once('ready', done); - analytics.initialize(); - analytics.page(); - }); - - afterEach(function() { - delete window.optimizelyEdge; - }); - - it('should call initEdgeIntegration', function(done) { - executeAsyncTest(done, function() { - sinon.assert.calledWith(optimizely.initEdgeIntegration); - }); - }); - - it('should flag source of integration', function() { - sinon.assert.calledWith(window.optimizelyEdge.push, { - type: 'integration', - OAuthClientId: '5360906403' - }); - }); - }); }); From e30113c0b3a693e77d6414512d79f4be9f2930ea Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 23 Sep 2020 15:15:45 -0700 Subject: [PATCH 13/17] unit tests pt 2, referrer for edge --- integrations/optimizely/test/index.test.js | 127 +++++++++++++++------ 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 309e7c792..86dad506b 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -29,7 +29,9 @@ function executeAsyncTest(done, test) { } /** - * + * Since our integration is unaware of whether Segment is integrated with a Web or Edge page, + * we are now adding initialize listeners. In these tests, we must activate analytics.initialize() + * by invoking the listener's handler * @api private * @param {*} prePushStub the stub of the prePushed global */ @@ -135,6 +137,7 @@ var mockWindowOptimizely = function() { get: function() { return { + // mock API calls that reflect what Optimizely public APIs return getCampaignStates: function(options) { if (!options.isActive) { throw new Error('Incorrect call to getCampaignStates'); @@ -208,6 +211,7 @@ var mockWindowEdge = function() { get: function() { return { + // mock API calls that reflect what Optimizely public APIs return getActiveExperiments: function() { var data = _.filter(window.optimizelyEdge.edgeMockData, { isActive: true @@ -236,18 +240,19 @@ var mockWindowEdge = function() { /* eslint-enable no-param-reassign */ }, 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' - // }; - // } - // } + var data = _.filter(window.optimizelyEdge.edgeMockData, { + isActive: true + }); + + for (var i = 0; i < data.length; i++) { + if (data[i].visitorRedirected) { + return { + experimentId: data[i].id, + variationId: data[i].variation.id, + referrer: 'barstools.com' + }; + } + } return null; } }; @@ -300,7 +305,6 @@ describe('Optimizely', function() { optimizely.reset(); sandbox(); sinon.restore(); - sinon.reset(); }); describe('#initialize', function() { @@ -317,6 +321,8 @@ describe('Optimizely', function() { sinon.assert.calledOnce(preEdgePushStub); }); + // fragile; stub history is not resetting quick enough, but these tests are passing on its own + // and in its current order context('when Optimizely Web is initialized', function() { beforeEach(function() { mockWindowOptimizely(); @@ -340,6 +346,8 @@ describe('Optimizely', function() { }); }); + // fragile; stub history is not resetting quick enough, but these tests are passing on its own + // and in its current order context('when Optimizely Edge is initialized', function() { beforeEach(function() { mockWindowEdge(); @@ -349,6 +357,8 @@ describe('Optimizely', function() { it('should call initEdgeIntegration', function(done) { executeAsyncTest(done, function() { sinon.assert.calledOnce(Optimizely.prototype.initEdgeIntegration); + // no need to check that initWebIntegration is not called, since it will never be called if + // Edge exists on the page. }); }); @@ -964,21 +974,26 @@ describe('Optimizely', function() { describe('#initEdgeIntegration', function() { beforeEach(function() { sinon.stub(optimizely, 'sendEdgeExperimentToSegment'); + sinon.stub(optimizely, 'setRedirectInfo'); }); context('before the Edge microsnippet has loaded', function() { var prePushStub; beforeEach(function() { + prePushStub = sinon.stub(); + window.optimizelyEdge = { + push: prePushStub + }; + optimizely.initEdgeIntegration(); }); - // Optimizely Edge is not supporting an API to get redirect information. - context.skip('if a redirect experiment has executed', function() { + context('if a redirect experiment has executed', function() { beforeEach(function() { mockWindowEdge(); // Make sure window.optimizely.getRedirectInfo returns something - window.optimizelyEdge.edgeMockData[2347102720].isActive = true; + window.optimizelyEdge.edgeMockData[7522212694].isActive = true; }); it('eventually captures redirect info', function() { @@ -1008,10 +1023,10 @@ describe('Optimizely', function() { }); // Optimizely Edge is not supporting an API to get redirect information. - context.skip("if a redirect experiment hasn't executed", function() { + context("if a redirect experiment hasn't executed", function() { beforeEach(function() { // by default mock data has no redirect experiments active - mockWindowOptimizely(); + mockWindowEdge(); }); it('does not capture redirect info', function() { @@ -1045,7 +1060,7 @@ describe('Optimizely', function() { beforeEach(function() { // Make sure the code is actually listening for experiments - var experimentDecidedCalls = _.filter(preEdgePushStub.getCalls(), { + var experimentDecidedCalls = _.filter(prePushStub.getCalls(), { args: [ { type: 'addListener', @@ -1117,12 +1132,12 @@ describe('Optimizely', function() { }); // Optimizely Edge is not supporting an API to get redirect information. - context.skip('if a redirect experiment has executed', function() { + context('if a redirect experiment has executed', function() { beforeEach(function() { // Make sure window.optimizely.getRedirectInfo returns something - window.optimizely.newMockData[2347102720].isActive = true; + window.optimizelyEdge.edgeMockData[7522212694].isActive = true; - optimizely.initWebIntegration(); + optimizely.initEdgeIntegration(); }); it('immediately captures redirect info', function() { @@ -1136,15 +1151,15 @@ describe('Optimizely', function() { it('captures redirect info _before_ tracking decisions', function() { sinon.assert.callOrder( optimizely.setRedirectInfo, - optimizely.sendWebDecisionToSegment + optimizely.sendEdgeExperimentToSegment ); }); }); // Optimizely Edge is not supporting an API to get redirect information. - context.skip("if a redirect experiment hasn't executed", function() { + context("if a redirect experiment hasn't executed", function() { beforeEach(function() { - optimizely.initWebIntegration(); + optimizely.initEdgeIntegration(); }); it('does not capture redirect info', function() { @@ -1205,6 +1220,7 @@ describe('Optimizely', function() { context('and the experiment is active', function() { beforeEach(function() { + mockWindowEdge(); window.optimizelyEdge.edgeMockData[7522212694].isActive = true; handler({ @@ -1338,11 +1354,11 @@ describe('Optimizely', function() { }); // Edge does not have a way to retrieve redirect info - it.skip('should send redirect experiment data via `.track()`', function(done) { + it('should send redirect experiment data via `.track()`', function(done) { // Enable just the campaign with redirect variation - window.optimizely.newMockData[2347102720].isActive = true; - window.optimizely.newMockData[7547101713].isActive = false; - window.optimizely.newMockData[2542102702].isActive = false; + window.optimizelyEdge.edgeMockData[7522212694].isActive = true; + window.optimizelyEdge.edgeMockData[7547682694].isActive = false; + window.optimizelyEdge.edgeMockData[1111182111].isActive = false; var context = { integration: optimizelyContext, page: { referrer: 'barstools.com' } @@ -1352,16 +1368,11 @@ describe('Optimizely', function() { assert.deepEqual(analytics.track.args[0], [ 'Experiment Viewed', { - campaignName: 'Get Rich or Die Tryin', - campaignId: '2347102720', experimentId: '7522212694', experimentName: 'Wells Fargo Scam', variationId: '7551111120', variationName: 'Variation Corruption #1884', - audienceId: '7100568438', - audienceName: 'Middle Class', - referrer: 'barstools.com', - isInCampaignHoldback: false + referrer: 'barstools.com' }, context ]); @@ -1592,6 +1603,50 @@ describe('Optimizely', function() { analytics.initialize(); }); + context('when Optimizely Edge is implemented', function() { + beforeEach(function() { + window.optimizelyEdge = { + push: sinon.stub(), + get: sinon.stub() + }; + }); + + it('should send an event for a named page', function() { + var referrer = window.document.referrer; + analytics.page('Home'); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'Viewed Home Page', + tags: { + name: 'Home', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } + }); + }); + + it('should send an event for a named and categorized page', function() { + var referrer = window.document.referrer; + analytics.page('Blog', 'New Integration'); + sinon.assert.calledWith(window.optimizelyEdge.push, { + type: 'event', + eventName: 'Viewed Blog New Integration Page', + tags: { + name: 'New Integration', + category: 'Blog', + path: '/context.html', + referrer: referrer, + search: '', + title: '', + url: 'http://localhost:9876/context.html' + } + }); + }); + }); + context('when Optimizely Web is implemented', function() { beforeEach(function() { window.optimizely = { From 5dd667b2a9f5ff29c836c6fccf5b21fba5085cef Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 23 Sep 2020 15:36:33 -0700 Subject: [PATCH 14/17] clean up --- integrations/optimizely/lib/index.js | 17 +---------------- integrations/optimizely/test/index.test.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/integrations/optimizely/lib/index.js b/integrations/optimizely/lib/index.js index d4cf2d153..11a82d953 100644 --- a/integrations/optimizely/lib/index.js +++ b/integrations/optimizely/lib/index.js @@ -77,22 +77,7 @@ Optimizely.prototype.initialize = function() { handler: function() { // We have to check this because Edge microsnippets currently process // window.optimizely calls and not just window.optimizelyEdge calls. - if (window.optimizelyEdge && window.optimizelyEdge.get) { - // Initialize the Edge integration if that hasn't been done already - edgePush({ - type: 'integration', - // Flag source of integration (requested by Optimizely) - OAuthClientId: '5360906403' - }); - - // Initialize listeners for Optimizely Edge decisions. - // We're calling 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() { - self.initEdgeIntegration(); - }); - } else if (window.optimizely && window.optimizely.get) { + if (!(window.optimizelyEdge && window.optimizelyEdge.get)) { // Initialize the Web integration push({ type: 'integration', diff --git a/integrations/optimizely/test/index.test.js b/integrations/optimizely/test/index.test.js index 86dad506b..a215260b1 100644 --- a/integrations/optimizely/test/index.test.js +++ b/integrations/optimizely/test/index.test.js @@ -308,6 +308,15 @@ describe('Optimizely', function() { }); describe('#initialize', function() { + var args = { + type: 'addListener', + filter: { + type: 'lifecycle', + name: 'initialized' + }, + handler: sinon.match.any + }; + beforeEach(function(done) { sinon.stub(Optimizely.prototype, 'initWebIntegration'); sinon.stub(Optimizely.prototype, 'initEdgeIntegration'); @@ -317,8 +326,8 @@ describe('Optimizely', function() { }); it('should push initialize listener to window.optimizely and window.optimizelyEdge', function() { - sinon.assert.calledOnce(preWebPushStub); - sinon.assert.calledOnce(preEdgePushStub); + sinon.assert.calledOnceWithExactly(preWebPushStub, args); + sinon.assert.calledOnceWithExactly(preEdgePushStub, args); }); // fragile; stub history is not resetting quick enough, but these tests are passing on its own From 1da50a8f0f1f4d9ac35214dbc5dc1334014aab39 Mon Sep 17 00:00:00 2001 From: Patrick Shih Date: Wed, 7 Oct 2020 16:44:26 -0700 Subject: [PATCH 15/17] revert yarn.lock --- yarn.lock | 126 +++--------------------------------------------------- 1 file changed, 5 insertions(+), 121 deletions(-) diff --git a/yarn.lock b/yarn.lock index ac84a095e..c0759b622 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.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= +"@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== dependencies: element-matches-polyfill "^1.0.0" whatwg-fetch "^3.0.0" @@ -1485,20 +1485,6 @@ 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" @@ -1507,14 +1493,6 @@ "@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" @@ -1524,15 +1502,6 @@ 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" @@ -1874,11 +1843,6 @@ 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" @@ -2550,18 +2514,6 @@ 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" @@ -2592,11 +2544,6 @@ 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" @@ -3351,13 +3298,6 @@ 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" @@ -3541,11 +3481,6 @@ 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" @@ -4484,11 +4419,6 @@ 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" @@ -4840,11 +4770,6 @@ 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" @@ -6220,11 +6145,6 @@ 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" @@ -6831,17 +6751,6 @@ 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" @@ -7603,11 +7512,6 @@ 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" @@ -8614,19 +8518,6 @@ 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" @@ -9120,13 +9011,6 @@ 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" @@ -9429,7 +9313,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.0, type-detect@^4.0.5, type-detect@^4.0.8: +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== From 6990bfccbd130bf017b510b96ee0465d37f3e952 Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 7 Oct 2020 17:26:19 -0700 Subject: [PATCH 16/17] Update yarn.lock. --- yarn.lock | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index c0759b622..1c4cec165 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://registry.yarnpkg.com/@segment/tracktor/-/tracktor-0.12.1.tgz#ca3e868f8b51c7da585764a482874addc31ea3e1" + integrity sha512-M9/XhBOHzerK1ZoiL/wOaM4gmzBOV6e2Z/xeic85qjKtiFSqNOBthlpyEGfEDKo6niEVBQm6wn86HgcAEkMDAg== 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.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" + integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" + integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== + 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://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" + integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== + 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.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.2.0.tgz#fcff83ab86f83b5498f4a967869c079408d9b5eb" + integrity sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw== + 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://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + 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://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + 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://registry.yarnpkg.com/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://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + 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://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + 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://registry.yarnpkg.com/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://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + 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.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + 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.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd" + integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A== + 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://registry.yarnpkg.com/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.2.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.0.tgz#1d333967e30023609f7347351ebc0dc964c0f3c9" + integrity sha512-eSNXz1XMcGEMHw08NJXSyTHIu6qTCOiN8x9ODACmZpNQpr0aXTBXBnI4xTzQzR+TEpOmLiKowGf9flCuKIzsbw== + dependencies: + "@sinonjs/commons" "^1.8.1" + "@sinonjs/fake-timers" "^6.0.1" + "@sinonjs/formatio" "^5.0.1" + "@sinonjs/samsam" "^5.2.0" + diff "^4.0.2" + nise "^4.0.4" + 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.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + 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== From d2c271f54fb3fe25afe3a5137ddf9d27ec78e8fb Mon Sep 17 00:00:00 2001 From: Brennan Gamwell Date: Wed, 7 Oct 2020 17:28:54 -0700 Subject: [PATCH 17/17] Bump Optimizely version to 4.1.0. --- integrations/optimizely/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/optimizely/package.json b/integrations/optimizely/package.json index 12e554a49..68c8217f0 100644 --- a/integrations/optimizely/package.json +++ b/integrations/optimizely/package.json @@ -1,7 +1,7 @@ { "name": "@segment/analytics.js-integration-optimizely", "description": "The Optimizely analytics.js integration.", - "version": "3.5.0", + "version": "4.1.0", "keywords": [ "analytics.js", "analytics.js-integration",