From 678ccb7fd97e25e15bac572c3cb899a3af23b976 Mon Sep 17 00:00:00 2001 From: Alex S <49695018+alexs-mparticle@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:33:27 -0500 Subject: [PATCH] fix: Move ready queue processing after identity request (#933) --- src/identity.js | 6 + src/mp-instance.js | 51 ++----- src/pre-init-utils.ts | 36 +++++ test/jest/pre-init-utils.spec.ts | 69 +++++++++ test/src/_test.index.ts | 2 +- test/src/config/utils.js | 4 +- test/src/tests-core-sdk.js | 38 ++--- test/src/tests-forwarders.js | 45 ++---- test/src/tests-helpers.js | 33 +++- test/src/tests-identities-attributes.ts | 153 ++++++++++++++++++- test/src/tests-identity.ts | 5 +- test/src/tests-mparticle-instance-manager.js | 2 +- test/src/tests-persistence.ts | 8 +- test/src/tests-queue-public-methods.js | 72 +++++---- 14 files changed, 377 insertions(+), 147 deletions(-) create mode 100644 src/pre-init-utils.ts create mode 100644 test/jest/pre-init-utils.spec.ts diff --git a/src/identity.js b/src/identity.js index 81e9299a..3abebed7 100644 --- a/src/identity.js +++ b/src/identity.js @@ -18,6 +18,7 @@ import { } from './utils'; import { hasMPIDAndUserLoginChanged, hasMPIDChanged } from './user-utils'; import { getNewIdentitiesByName } from './type-utils'; +import { processReadyQueue } from './pre-init-utils'; export default function Identity(mpInstance) { const { getFeatureFlag, extend } = mpInstance._Helpers; @@ -1679,6 +1680,11 @@ export default function Identity(mpInstance) { 'Error parsing JSON response from Identity server: ' + e ); } + mpInstance._Store.isInitialized = true; + + mpInstance._preInit.readyQueue = processReadyQueue( + mpInstance._preInit.readyQueue + ); }; // send a user identity change request on identify, login, logout, modify when any values change. diff --git a/src/mp-instance.js b/src/mp-instance.js index 39afdd0e..1736ce1d 100644 --- a/src/mp-instance.js +++ b/src/mp-instance.js @@ -36,10 +36,11 @@ import Consent from './consent'; import KitBlocker from './kitBlocking'; import ConfigAPIClient from './configAPIClient'; import IdentityAPIClient from './identityApiClient'; -import { isEmpty, isFunction } from './utils'; +import { isFunction } from './utils'; import { LocalStorageVault } from './vault'; import { removeExpiredIdentityCacheDates } from './identity-utils'; import IntegrationCapture from './integrationCapture'; +import { processReadyQueue } from './pre-init-utils'; const { Messages, HTTPCodes, FeatureFlags } = Constants; const { ReportBatching, CaptureIntegrationSpecificIds } = FeatureFlags; @@ -1361,15 +1362,17 @@ function completeSDKInitialization(apiKey, config, mpInstance) { ); } - mpInstance._Store.isInitialized = true; + // We will continue to clear out the ready queue as part of the initial init flow + // if an identify request is unnecessary, such as if there is an existing session + if ( + (mpInstance._Store.mpid && !mpInstance._Store.identifyCalled) || + mpInstance._Store.webviewBridgeEnabled + ) { + mpInstance._Store.isInitialized = true; - // Call any functions that are waiting for the library to be initialized - try { mpInstance._preInit.readyQueue = processReadyQueue( mpInstance._preInit.readyQueue ); - } catch (error) { - mpInstance.Logger.error(error); } // https://go.mparticle.com/work/SQDSDKS-6040 @@ -1508,42 +1511,6 @@ function processIdentityCallback( } } -function processPreloadedItem(readyQueueItem) { - const args = readyQueueItem; - const method = args.splice(0, 1)[0]; - // if the first argument is a method on the base mParticle object, run it - if (mParticle[args[0]]) { - mParticle[method].apply(this, args); - // otherwise, the method is on either eCommerce or Identity objects, ie. "eCommerce.setCurrencyCode", "Identity.login" - } else { - const methodArray = method.split('.'); - try { - var computedMPFunction = mParticle; - for (let i = 0; i < methodArray.length; i++) { - const currentMethod = methodArray[i]; - computedMPFunction = computedMPFunction[currentMethod]; - } - computedMPFunction.apply(this, args); - } catch (e) { - throw new Error('Unable to compute proper mParticle function ' + e); - } - } -} - -function processReadyQueue(readyQueue) { - if (!isEmpty(readyQueue)) { - readyQueue.forEach(function(readyQueueItem) { - if (isFunction(readyQueueItem)) { - readyQueueItem(); - } else if (Array.isArray(readyQueueItem)) { - processPreloadedItem(readyQueueItem); - } - }); - } - // https://go.mparticle.com/work/SQDSDKS-6835 - return []; -} - function queueIfNotInitialized(func, self) { if (!self.isInitialized()) { self.ready(function() { diff --git a/src/pre-init-utils.ts b/src/pre-init-utils.ts new file mode 100644 index 00000000..05178088 --- /dev/null +++ b/src/pre-init-utils.ts @@ -0,0 +1,36 @@ +import { isEmpty, isFunction } from './utils'; + +export const processReadyQueue = (readyQueue): Function[] => { + if (!isEmpty(readyQueue)) { + readyQueue.forEach(readyQueueItem => { + if (isFunction(readyQueueItem)) { + readyQueueItem(); + } else if (Array.isArray(readyQueueItem)) { + processPreloadedItem(readyQueueItem); + } + }); + } + return []; +}; + +const processPreloadedItem = (readyQueueItem): void => { + const args = readyQueueItem; + const method = args.splice(0, 1)[0]; + + // if the first argument is a method on the base mParticle object, run it + if (typeof window !== 'undefined' && window.mParticle && window.mParticle[args[0]]) { + window.mParticle[method].apply(this, args); + // otherwise, the method is on either eCommerce or Identity objects, ie. "eCommerce.setCurrencyCode", "Identity.login" + } else { + const methodArray = method.split('.'); + try { + let computedMPFunction = window.mParticle; + for (const currentMethod of methodArray) { + computedMPFunction = computedMPFunction[currentMethod]; + } + ((computedMPFunction as unknown) as Function).apply(this, args); + } catch (e) { + throw new Error('Unable to compute proper mParticle function ' + e); + } + } +}; diff --git a/test/jest/pre-init-utils.spec.ts b/test/jest/pre-init-utils.spec.ts new file mode 100644 index 00000000..75255518 --- /dev/null +++ b/test/jest/pre-init-utils.spec.ts @@ -0,0 +1,69 @@ +import { processReadyQueue } from '../../src/pre-init-utils'; + +describe('pre-init-utils', () => { + describe('#processReadyQueue', () => { + it('should return an empty array if readyQueue is empty', () => { + const result = processReadyQueue([]); + expect(result).toEqual([]); + }); + + it('should process functions passed as arguments', () => { + const functionSpy = jest.fn(); + const readyQueue: Function[] = [functionSpy, functionSpy, functionSpy]; + const result = processReadyQueue(readyQueue); + expect(functionSpy).toHaveBeenCalledTimes(3); + expect(result).toEqual([]); + }); + + it('should process functions passed as arrays', () => { + const functionSpy = jest.fn(); + (window.mParticle as any) = { + fakeFunction: functionSpy, + }; + const readyQueue = [['fakeFunction']]; + processReadyQueue(readyQueue); + expect(functionSpy).toHaveBeenCalled(); + }); + + it('should process functions passed as arrays with arguments', () => { + const functionSpy = jest.fn(); + (window.mParticle as any) = { + fakeFunction: functionSpy, + args: () => {}, + }; + const readyQueue = [['fakeFunction', 'args']]; + processReadyQueue(readyQueue); + expect(functionSpy).toHaveBeenCalledWith('args'); + }); + + it('should process arrays passed as arguments with multiple methods', () => { + const functionSpy = jest.fn(); + (window.mParticle as any) = { + fakeFunction: { + anotherFakeFunction: functionSpy, + }, + }; + const readyQueue = [['fakeFunction.anotherFakeFunction', 'foo']]; + processReadyQueue(readyQueue); + expect(functionSpy).toHaveBeenCalledWith('foo'); + }); + + it('should process arrays passed as arguments with multiple methods and arguments', () => { + const functionSpy = jest.fn(); + const functionSpy2 = jest.fn(); + (window.mParticle as any) = { + fakeFunction: functionSpy, + anotherFakeFunction: functionSpy2, + }; + const readyQueue = [['fakeFunction', 'foo'], ['anotherFakeFunction', 'bar']]; + processReadyQueue(readyQueue); + expect(functionSpy).toHaveBeenCalledWith('foo'); + expect(functionSpy2).toHaveBeenCalledWith('bar'); + }); + + it('should throw an error if it cannot compute the proper mParticle function', () => { + const readyQueue = [['Identity.login']]; + expect(() => processReadyQueue(readyQueue)).toThrowError("Unable to compute proper mParticle function TypeError: Cannot read properties of undefined (reading 'login')"); + }); + }); +}); \ No newline at end of file diff --git a/test/src/_test.index.ts b/test/src/_test.index.ts index e847c974..7cb1e5b0 100644 --- a/test/src/_test.index.ts +++ b/test/src/_test.index.ts @@ -10,8 +10,8 @@ import './tests-kit-blocking'; import './tests-event-logging'; import './tests-eCommerce'; import './tests-persistence'; -import './tests-forwarders'; import './tests-helpers'; +import './tests-forwarders'; import './tests-cookie-syncing'; import './tests-identities-attributes'; import './tests-native-sdk'; diff --git a/test/src/config/utils.js b/test/src/config/utils.js index 06b6476a..ac1d3fb2 100644 --- a/test/src/config/utils.js +++ b/test/src/config/utils.js @@ -633,7 +633,8 @@ var pluses = /\+/g, }, hasIdentifyReturned = () => { return window.mParticle.Identity.getCurrentUser()?.getMPID() === testMPID; - }; + }, + hasIdentityCallInflightReturned = () => !mParticle.getInstance()?._Store?.identityCallInFlight; var TestsCore = { getLocalStorageProducts: getLocalStorageProducts, @@ -661,6 +662,7 @@ var TestsCore = { waitForCondition: waitForCondition, fetchMockSuccess: fetchMockSuccess, hasIdentifyReturned: hasIdentifyReturned, + hasIdentityCallInflightReturned, }; export default TestsCore; \ No newline at end of file diff --git a/test/src/tests-core-sdk.js b/test/src/tests-core-sdk.js index 7061838e..d0f91ddd 100644 --- a/test/src/tests-core-sdk.js +++ b/test/src/tests-core-sdk.js @@ -1,3 +1,4 @@ +import { expect } from 'chai'; import Utils from './config/utils'; import Store from '../../src/store'; import Constants, { HTTP_ACCEPTED, HTTP_OK } from '../../src/constants'; @@ -11,7 +12,7 @@ const DefaultConfig = Constants.DefaultConfig, findEventFromRequest = Utils.findEventFromRequest, findBatch = Utils.findBatch; -const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; +const { waitForCondition, fetchMockSuccess, hasIdentifyReturned, hasIdentityCallInflightReturned } = Utils; describe('core SDK', function() { beforeEach(function() { @@ -121,7 +122,7 @@ describe('core SDK', function() { }); }); - it('should process ready queue when initialized', function(done) { + it('should process ready queue when initialized', async function() { let readyFuncCalled = false; mParticle._resetForTests(MPConfig); @@ -130,10 +131,9 @@ describe('core SDK', function() { readyFuncCalled = true; }); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); - Should(readyFuncCalled).equal(true); - - done(); + expect(readyFuncCalled).equal(true); }); it('should set app version on the payload', function(done) { @@ -150,14 +150,12 @@ describe('core SDK', function() { }) }); - it('should get app version', function(done) { + it('should get app version', async function() { + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setAppVersion('2.0'); const appVersion = mParticle.getAppVersion(); - - appVersion.should.equal('2.0'); - - done(); + expect(appVersion).to.equal('2.0'); }); it('should get environment setting when set to `production`', function(done) { @@ -1191,15 +1189,14 @@ describe('core SDK', function() { }); }); - it('should initialize without a config object passed to init', function(done) { + it('should initialize without a config object passed to init', async function() { // this instance occurs when self hosting and the user only passes an object into init mParticle._resetForTests(MPConfig); mParticle.init(apiKey); + await waitForCondition(hasIdentityCallInflightReturned); mParticle.getInstance()._Store.isInitialized.should.equal(true); - - done(); }); it('should generate hash both on the mparticle instance and the mparticle instance manager', function(done) { @@ -1262,18 +1259,17 @@ describe('core SDK', function() { done(); }); - it('should set a device id when calling setDeviceId', function(done) { + it('should set a device id when calling setDeviceId', async function() { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); // this das should be the SDK auto generated one, which is 36 characters long mParticle.getDeviceId().length.should.equal(36); mParticle.setDeviceId('foo-guid'); mParticle.getDeviceId().should.equal('foo-guid'); - - done(); }); it('should set a device id when set on mParticle.config', function(done) { @@ -1310,34 +1306,32 @@ describe('core SDK', function() { done(); }); - it('should set the wrapper sdk info in Store when mParticle._setWrapperSDKInfo() method is called after init is called', function(done) { + it('should set the wrapper sdk info in Store when mParticle._setWrapperSDKInfo() method is called after init is called', async function() { mParticle._resetForTests(MPConfig); mParticle._setWrapperSDKInfo('flutter', '1.0.3'); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); mParticle.getInstance()._Store.wrapperSDKInfo.name.should.equal('flutter'); mParticle.getInstance()._Store.wrapperSDKInfo.version.should.equal('1.0.3'); mParticle.getInstance()._Store.wrapperSDKInfo.isInfoSet.should.equal(true); - - done(); }); - it('should not set the wrapper sdk info in Store after it has previously been set', function(done) { + it('should not set the wrapper sdk info in Store after it has previously been set', async function() { mParticle._resetForTests(MPConfig); mParticle._setWrapperSDKInfo('flutter', '1.0.3'); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); mParticle._setWrapperSDKInfo('none', '2.0.5'); mParticle.getInstance()._Store.wrapperSDKInfo.name.should.equal('flutter'); mParticle.getInstance()._Store.wrapperSDKInfo.version.should.equal('1.0.3'); mParticle.getInstance()._Store.wrapperSDKInfo.isInfoSet.should.equal(true); - - done(); }); describe('pod feature flag', function() { diff --git a/test/src/tests-forwarders.js b/test/src/tests-forwarders.js index d0411197..6a2bdacc 100644 --- a/test/src/tests-forwarders.js +++ b/test/src/tests-forwarders.js @@ -19,7 +19,9 @@ const { findEventFromRequest, setLocalStorage, forwarderDefaultConfiguration, MockForwarder, - MockSideloadedKit,hasIdentifyReturned + MockSideloadedKit, + hasIdentifyReturned, + hasIdentityCallInflightReturned, } = Utils; let mockServer; @@ -1116,7 +1118,7 @@ describe('forwarders', function() { done(); }); - it('should invoke forwarder opt out', function(done) { + it('should invoke forwarder opt out', async function() { mParticle._resetForTests(MPConfig); const mockForwarder = new MockForwarder(); @@ -1125,14 +1127,13 @@ describe('forwarders', function() { window.mParticle.config.kitConfigs.push(config1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setOptOut(true); window.MockForwarder1.instance.should.have.property( 'setOptOutCalled', true ); - - done(); }); it('should invoke forwarder setuserattribute', function(done) { @@ -2710,21 +2711,21 @@ describe('forwarders', function() { done(); }); - it('should set integration attributes on forwarders', function(done) { + it('should set integration attributes on forwarders', async function() { mParticle.init(apiKey, window.mParticle.config) + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg' }); const adobeIntegrationAttributes = mParticle.getIntegrationAttributes( 128 ); adobeIntegrationAttributes.MCID.should.equal('abcdefg'); - - done(); }); - it('should clear integration attributes when an empty object or a null is passed', function(done) { + it('should clear integration attributes when an empty object or a null is passed', async function() { mParticle.init(apiKey, window.mParticle.config) + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg' }); let adobeIntegrationAttributes = mParticle.getIntegrationAttributes( @@ -2743,12 +2744,11 @@ describe('forwarders', function() { mParticle.setIntegrationAttribute(128, null); adobeIntegrationAttributes = mParticle.getIntegrationAttributes(128); Object.keys(adobeIntegrationAttributes).length.should.equal(0); - - done(); }); - it('should set only strings as integration attributes', function(done) { + it('should set only strings as integration attributes', async function() { mParticle.init(apiKey, window.mParticle.config) + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setIntegrationAttribute(128, { MCID: 'abcdefg', @@ -2760,9 +2760,8 @@ describe('forwarders', function() { const adobeIntegrationAttributes = mParticle.getIntegrationAttributes( 128 ); - Object.keys(adobeIntegrationAttributes).length.should.equal(1); - done(); + Object.keys(adobeIntegrationAttributes).length.should.equal(1); }); it('should add integration delays to the integrationDelays object', function(done) { @@ -3074,7 +3073,7 @@ describe('forwarders', function() { // This will pass when we add mpInstance._Store.isInitialized = true; to mp-instance before `processIdentityCallback` - it('configures forwarders before events are logged via identify callback', function(done) { + it('configures forwarders before events are logged via identify callback', async function() { mParticle._resetForTests(MPConfig); window.mParticle.config.identifyRequest = { userIdentities: { @@ -3093,12 +3092,7 @@ describe('forwarders', function() { ); window.mParticle.config.rq = []; mParticle.init(apiKey, window.mParticle.config); - waitForCondition(() => { - return ( - window.mParticle.getInstance()?._Store?.identityCallInFlight === false - ); - }) - .then(() => { + await waitForCondition(hasIdentityCallInflightReturned); window.MockForwarder1.instance.should.have.property( 'processCalled', true @@ -3109,22 +3103,13 @@ describe('forwarders', function() { window.mParticle.config.rq = []; mParticle.init(apiKey, window.mParticle.config); - - waitForCondition(() => { - return ( - window.mParticle.getInstance()?._Store?.identityCallInFlight === false - ); - }) - .then(() => { + await waitForCondition(hasIdentityCallInflightReturned); window.MockForwarder1.instance.should.have.property( 'processCalled', true ); - done(); - }); - }); }); it('should retain preInit.forwarderConstructors, and reinitialize forwarders after calling reset, then init', function(done) { diff --git a/test/src/tests-helpers.js b/test/src/tests-helpers.js index 57a41cff..632ffa2b 100644 --- a/test/src/tests-helpers.js +++ b/test/src/tests-helpers.js @@ -1,9 +1,29 @@ -import { apiKey } from './config/constants'; +import { + urls, + apiKey, + testMPID, + mParticle, +} from './config/constants'; import sinon from 'sinon'; +import Utils from './config/utils'; + +const { waitForCondition, fetchMockSuccess, hasIdentityCallInflightReturned} = Utils; describe('helpers', function() { beforeEach(function() { + fetchMockSuccess(urls.events); + fetchMockSuccess(urls.identify, { + context: null, + matched_identities: { + device_application_stamp: 'my-das', + }, + is_ephemeral: true, + mpid: testMPID, + is_logged_in: false, + }); + mParticle.init(apiKey, window.mParticle.config); + }); it('should correctly validate an attribute value', function(done) { @@ -36,7 +56,8 @@ describe('helpers', function() { done(); }); - it('should return event name in warning when sanitizing invalid attributes', function(done) { + it('should return event name in warning when sanitizing invalid attributes', async function() { + await waitForCondition(hasIdentityCallInflightReturned); const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); mParticle.logEvent('eventName', mParticle.EventType.Location, {invalidValue: {}}); @@ -46,8 +67,6 @@ describe('helpers', function() { bond.getCalls()[0].args[0].should.eql( "For 'eventName', the corresponding attribute value of 'invalidValue' must be a string, number, boolean, or null." ); - - done(); }); it('should return product name in warning when sanitizing invalid attributes', function(done) { @@ -74,7 +93,9 @@ describe('helpers', function() { done(); }); - it('should return commerce event name in warning when sanitizing invalid attributes', function(done) { + it('should return commerce event name in warning when sanitizing invalid attributes', async function() { + await waitForCondition(hasIdentityCallInflightReturned); + const bond = sinon.spy(mParticle.getInstance().Logger, 'warning'); const product1 = mParticle.eCommerce.createProduct('prod1', 'prod1sku', 999); @@ -89,8 +110,6 @@ describe('helpers', function() { bond.getCalls()[0].args[0].should.eql( "For 'eCommerce - AddToCart', the corresponding attribute value of 'invalidValue' must be a string, number, boolean, or null." ); - - done(); }); it('should correctly validate an identity request with copyUserAttribute as a key using any identify method', function(done) { diff --git a/test/src/tests-identities-attributes.ts b/test/src/tests-identities-attributes.ts index 4b91bc70..af7c84a9 100644 --- a/test/src/tests-identities-attributes.ts +++ b/test/src/tests-identities-attributes.ts @@ -1,4 +1,3 @@ -import sinon from 'sinon'; import { expect } from 'chai'; import fetchMock from 'fetch-mock/esm/client'; import { @@ -6,16 +5,17 @@ import { apiKey, testMPID, MPConfig, - MILLISECONDS_IN_ONE_DAY_PLUS_ONE_SECOND, } from './config/constants'; import Utils from './config/utils'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { AllUserAttributes, UserAttributesValue } from '@mparticle/web-sdk'; import { UserAttributes } from '../../src/identity-user-interfaces'; -import { UserAttributeChangeEvent } from '@mparticle/event-models'; -const { waitForCondition, fetchMockSuccess, hasIdentifyReturned } = Utils; - +import { Batch, CustomEvent, UserAttributeChangeEvent } from '@mparticle/event-models'; const { + waitForCondition, + fetchMockSuccess, + hasIdentifyReturned, + hasIdentityCallInflightReturned, findEventFromRequest, findBatch, getLocalStorage, @@ -54,16 +54,30 @@ const BAD_USER_ATTRIBUTE_KEY_AS_ARRAY = ([ const BAD_USER_ATTRIBUTE_LIST_VALUE = (1234 as unknown) as UserAttributesValue[]; describe('identities and attributes', function() { + let beforeEachCallbackCalled = false; + let hasBeforeEachCallbackReturned; + beforeEach(function() { fetchMockSuccess(urls.identify, { mpid: testMPID, is_logged_in: false }); fetchMock.post(urls.events, 200); + + mParticle.config.identityCallback = function() { + // There are some tests that need to verify that the initial init + // call within the beforeEach method has completed before they + // can introduce a new identityCallback for their specific assertions. + beforeEachCallbackCalled = true; + }; + mParticle.init(apiKey, window.mParticle.config); + + hasBeforeEachCallbackReturned = () => beforeEachCallbackCalled; }); afterEach(function() { + beforeEachCallbackCalled = false; fetchMock.restore(); }); @@ -1223,6 +1237,135 @@ describe('identities and attributes', function() { }) }); + it('should order user identity change events before logging any events', async () => { + mParticle._resetForTests(MPConfig); + fetchMock.resetHistory(); + + // Clear out before each init call + await waitForCondition(hasBeforeEachCallbackReturned); + + window.mParticle.config.identifyRequest = { + userIdentities: { + email: 'initial@gmail.com', + }, + }; + + mParticle.init(apiKey, window.mParticle.config); + + await waitForCondition(hasIdentityCallInflightReturned); + + mParticle.logEvent('Test Event 1'); + mParticle.logEvent('Test Event 2'); + mParticle.logEvent('Test Event 3'); + + expect(fetchMock.calls().length).to.equal(7); + + const firstCall = fetchMock.calls()[0]; + expect(firstCall[0].split('/')[4]).to.equal('identify', 'First Call'); + + const secondCall = fetchMock.calls()[1]; + expect(secondCall[0].split('/')[6]).to.equal('events'); + const secondCallBody = JSON.parse(secondCall[1].body as unknown as string) as Batch; + expect(secondCallBody.events[0].event_type).to.equal('session_start', 'Second Call'); + + const thirdCall = fetchMock.calls()[2]; + expect(thirdCall[0].split('/')[6]).to.equal('events'); + const thirdCallBody = JSON.parse(thirdCall[1].body as unknown as string) as Batch; + expect(thirdCallBody.events[0].event_type).to.equal('application_state_transition', 'Third Call'); + + const fourthCall = fetchMock.calls()[3]; + expect(fourthCall[0].split('/')[6]).to.equal('events'); + const fourthCallBody = JSON.parse(fourthCall[1].body as unknown as string) as Batch; + expect(fourthCallBody.events[0].event_type).to.equal('user_identity_change', 'Fourth Call'); + + const fifthCall = fetchMock.calls()[4]; + expect(fifthCall[0].split('/')[6]).to.equal('events'); + const fifthCallBody = JSON.parse(fifthCall[1].body as unknown as string) as Batch; + const fifthCallEvent = fifthCallBody.events[0] as CustomEvent; + expect(fifthCallEvent.event_type).to.equal('custom_event', 'Fifth Call'); + expect(fifthCallEvent.data.event_name).to.equal('Test Event 1', 'Fifth Call'); + + const sixthCall = fetchMock.calls()[5]; + expect(sixthCall[0].split('/')[6]).to.equal('events'); + const sixthCallBody = JSON.parse(sixthCall[1].body as unknown as string) as Batch; + const sixthCallEvent = sixthCallBody.events[0] as CustomEvent; + expect(sixthCallBody.events[0].event_type).to.equal('custom_event', 'Sixth Call'); + expect(sixthCallEvent.data.event_name).to.equal('Test Event 2', 'Sixth Call'); + + const seventhEvent = fetchMock.calls()[6]; + expect(seventhEvent[0].split('/')[6]).to.equal('events'); + const seventhEventBody = JSON.parse(seventhEvent[1].body as unknown as string) as Batch; + const seventhEventEvent = seventhEventBody.events[0] as CustomEvent; + expect(seventhEventBody.events[0].event_type).to.equal('custom_event', 'Seventh Call'); + expect(seventhEventEvent.data.event_name).to.equal('Test Event 3', 'Seventh Call'); + }); + + it('should order user identity change events before logging any events that are in the ready queue', async () => { + + mParticle._resetForTests(MPConfig); + fetchMock.resetHistory(); + + // Clear out before each init call + await waitForCondition(hasBeforeEachCallbackReturned); + + window.mParticle.config.identifyRequest = { + userIdentities: { + email: 'initial@gmail.com', + }, + }; + + mParticle.init(apiKey, window.mParticle.config); + + mParticle.logEvent('Test Event 1'); + mParticle.logEvent('Test Event 2'); + mParticle.logEvent('Test Event 3'); + + expect(mParticle.getInstance()._preInit.readyQueue.length).to.equal(3); + + await waitForCondition(hasIdentityCallInflightReturned); + + expect(fetchMock.calls().length).to.equal(7); + + const firstCall = fetchMock.calls()[0]; + expect(firstCall[0].split('/')[4]).to.equal('identify', 'First Call'); + + const secondCall = fetchMock.calls()[1]; + expect(secondCall[0].split('/')[6]).to.equal('events'); + const secondCallBody = JSON.parse(secondCall[1].body as unknown as string) as Batch; + expect(secondCallBody.events[0].event_type).to.equal('session_start', 'Second Call'); + + const thirdCall = fetchMock.calls()[2]; + expect(thirdCall[0].split('/')[6]).to.equal('events'); + const thirdCallBody = JSON.parse(thirdCall[1].body as unknown as string) as Batch; + expect(thirdCallBody.events[0].event_type).to.equal('application_state_transition', 'Third Call'); + + const fourthCall = fetchMock.calls()[3]; + expect(fourthCall[0].split('/')[6]).to.equal('events'); + const fourthCallBody = JSON.parse(fourthCall[1].body as unknown as string) as Batch; + expect(fourthCallBody.events[0].event_type).to.equal('user_identity_change', 'Fourth Call'); + + const fifthCall = fetchMock.calls()[4]; + expect(fifthCall[0].split('/')[6]).to.equal('events'); + const fifthCallBody = JSON.parse(fifthCall[1].body as unknown as string) as Batch; + const fifthCallEvent = fifthCallBody.events[0] as CustomEvent; + expect(fifthCallEvent.event_type).to.equal('custom_event', 'Fifth Call'); + expect(fifthCallEvent.data.event_name).to.equal('Test Event 1', 'Fifth Call'); + + const sixthCall = fetchMock.calls()[5]; + expect(sixthCall[0].split('/')[6]).to.equal('events'); + const sixthCallBody = JSON.parse(sixthCall[1].body as unknown as string) as Batch; + const sixthCallEvent = sixthCallBody.events[0] as CustomEvent; + expect(sixthCallBody.events[0].event_type).to.equal('custom_event', 'Sixth Call'); + expect(sixthCallEvent.data.event_name).to.equal('Test Event 2', 'Sixth Call'); + + const seventhEvent = fetchMock.calls()[6]; + expect(seventhEvent[0].split('/')[6]).to.equal('events'); + const seventhEventBody = JSON.parse(seventhEvent[1].body as unknown as string) as Batch; + const seventhEventEvent = seventhEventBody.events[0] as CustomEvent; + expect(seventhEventBody.events[0].event_type).to.equal('custom_event', 'Seventh Call'); + expect(seventhEventEvent.data.event_name).to.equal('Test Event 3', 'Seventh Call'); + }); + it('should send historical UIs on batches when MPID changes', function(done) { mParticle._resetForTests(MPConfig); diff --git a/test/src/tests-identity.ts b/test/src/tests-identity.ts index b83893c1..b2ec0da9 100644 --- a/test/src/tests-identity.ts +++ b/test/src/tests-identity.ts @@ -39,6 +39,7 @@ const { setCookie, MockForwarder, waitForCondition, + hasIdentityCallInflightReturned, } = Utils; const { HTTPCodes } = Constants; @@ -93,7 +94,6 @@ describe('identity', function() { let hasIdentifyReturned; let hasLoginReturned; let hasLogOutReturned; - let hasIdentityCallInflightReturned; let beforeEachCallbackCalled = false; let hasBeforeEachCallbackReturned @@ -138,9 +138,6 @@ describe('identity', function() { ); }; - hasIdentityCallInflightReturned = () => - !mParticle.getInstance()?._Store?.identityCallInFlight; - hasBeforeEachCallbackReturned = () => beforeEachCallbackCalled; }); diff --git a/test/src/tests-mparticle-instance-manager.js b/test/src/tests-mparticle-instance-manager.js index 6b09ff63..694cc37d 100644 --- a/test/src/tests-mparticle-instance-manager.js +++ b/test/src/tests-mparticle-instance-manager.js @@ -401,7 +401,7 @@ describe('mParticle instance manager', function() { 'apiKey3', 'purchase' ); - instance1Event.should.be.ok(); + Should(instance1Event).be.ok(); Should(instance2Event).not.be.ok(); Should(instance3Event).not.be.ok(); diff --git a/test/src/tests-persistence.ts b/test/src/tests-persistence.ts index ee8ca0a3..5408d6d7 100644 --- a/test/src/tests-persistence.ts +++ b/test/src/tests-persistence.ts @@ -29,7 +29,8 @@ const { findBatch, fetchMockSuccess, hasIdentifyReturned, - waitForCondition + waitForCondition, + hasIdentityCallInflightReturned, } = Utils; describe('persistence', () => { @@ -2048,18 +2049,17 @@ describe('persistence', () => { }); }); - it('should save to persistence a device id set with setDeviceId', done => { + it('should save to persistence a device id set with setDeviceId', async () => { mParticle._resetForTests(MPConfig); mParticle.init(apiKey, mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned); mParticle.setDeviceId('foo-guid'); mParticle .getInstance() ._Persistence.getLocalStorage() .gs.das.should.equal('foo-guid'); - - done(); }); it('should save to persistence a device id set via mParticle.config', done => { diff --git a/test/src/tests-queue-public-methods.js b/test/src/tests-queue-public-methods.js index 442fda64..c798fc6a 100644 --- a/test/src/tests-queue-public-methods.js +++ b/test/src/tests-queue-public-methods.js @@ -1,23 +1,20 @@ -import sinon from 'sinon'; import { apiKey, MPConfig, testMPID, urls } from './config/constants'; import { SDKProductActionType } from '../../src/sdkRuntimeModels'; +import Utils from './config/utils'; + +const { waitForCondition, fetchMockSuccess, hasIdentityCallInflightReturned } = Utils; describe('Queue Public Methods', function () { - let mockServer; beforeEach(function () { - mockServer = sinon.createFakeServer(); - mockServer.respondImmediately = true; - - mockServer.respondWith(urls.identify, [ - 200, - {}, - JSON.stringify({ mpid: testMPID, is_logged_in: false }), - ]); + fetchMockSuccess(urls.events); + fetchMockSuccess(urls.identify, { + mpid: testMPID, + is_logged_in: false, + }); }); afterEach(function () { - mockServer.reset(); mParticle._resetForTests(MPConfig); }); @@ -29,12 +26,11 @@ describe('Queue Public Methods', function () { mParticle.getInstance().should.have.property('isInitialized'); }); - it('should return false if not yet initialized, and true after initialization', function () { + it('should return false if not yet initialized, and true after initialization', async function () { mParticle.isInitialized().should.equal(false); mParticle.getInstance().isInitialized().should.equal(false); - window.mParticle.init(apiKey, window.mParticle.config); - + await waitForCondition(hasIdentityCallInflightReturned) mParticle.isInitialized().should.equal(true); mParticle.getInstance().isInitialized().should.equal(true); }); @@ -47,11 +43,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setAppVersion('1.2.3'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -63,11 +60,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setAppName('Timmy'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -81,13 +79,14 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.ready(function () { console.log('fired ready function'); }); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -99,11 +98,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setPosition(10, 4); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -119,7 +119,7 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { const event = { name: 'test event', }; @@ -128,6 +128,7 @@ describe('Queue Public Methods', function () { mParticle.logBaseEvent(event); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -139,11 +140,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.logEvent('Test Event'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -155,11 +157,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.logError('test error', {}); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -171,11 +174,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.logPageView('test page view', {}); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -187,11 +191,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setOptOut(true); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -203,11 +208,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setIntegrationAttribute('12345', {}); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -219,19 +225,21 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.setSessionAttribute('foo', 'bar'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); describe('#isInitialized', function () { - it('returns true when Store is initialized', function () { + it('returns true when Store is initialized', async function () { mParticle.getInstance().isInitialized().should.be.false(); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance().isInitialized().should.be.true(); }); }); @@ -245,11 +253,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.eCommerce.setCurrencyCode('USD'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -262,12 +271,13 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { const product = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.eCommerce.logProductAction(SDKProductActionType.Purchase, product); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -279,11 +289,12 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.eCommerce.logPromotion('Test'); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); }); @@ -297,13 +308,14 @@ describe('Queue Public Methods', function () { mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); }); - it('should process queue after initialization', function () { + it('should process queue after initialization', async function () { const product = window.mParticle.eCommerce.createProduct('iphone', 'iphoneSKU', 999); const impression = window.mParticle.eCommerce.createImpression('iphone impression', product); mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); mParticle.eCommerce.logImpression(impression); mParticle.getInstance()._preInit.readyQueue.length.should.equal(1); mParticle.init(apiKey, window.mParticle.config); + await waitForCondition(hasIdentityCallInflightReturned) mParticle.getInstance()._preInit.readyQueue.length.should.equal(0); }); });