From b28c45dc0ed3572dfb7dee31a0ac5fc45217487b Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 17 Jan 2025 11:42:51 -0500 Subject: [PATCH] refactor: Simplify cookie sync manager (#963) --- src/cookieSyncManager.ts | 189 +++++------- src/identity.js | 6 +- src/utils.ts | 18 ++ test/jest/cookieSyncManager.spec.ts | 460 ++++++++++++---------------- test/jest/utils.spec.ts | 16 +- test/src/tests-cookie-syncing.ts | 16 +- 6 files changed, 304 insertions(+), 401 deletions(-) diff --git a/src/cookieSyncManager.ts b/src/cookieSyncManager.ts index 9822a577e..d0b9c7cc8 100644 --- a/src/cookieSyncManager.ts +++ b/src/cookieSyncManager.ts @@ -1,4 +1,8 @@ -import { Dictionary, isEmpty, replaceAmpWithAmpersand, replaceMPID } from './utils'; +import { + Dictionary, + isEmpty, + createCookieSyncUrl, +} from './utils'; import Constants from './constants'; import { MParticleWebSDK } from './sdkRuntimeModels'; import { MPID } from '@mparticle/web-sdk'; @@ -25,7 +29,6 @@ export interface IPixelConfiguration { } export interface ICookieSyncManager { attemptCookieSync: ( - previousMPID: MPID, mpid: MPID, mpidIsNotInCookies?: boolean ) => void; @@ -34,9 +37,6 @@ export interface ICookieSyncManager { moduleId: string, mpid: MPID, cookieSyncDates: CookieSyncDates, - filteringConsentRuleValues: IConsentRules, - mpidIsNotInCookies: boolean, - requiresConsent: boolean ) => void; combineUrlWithRedirect: ( mpid: MPID, @@ -45,20 +45,6 @@ export interface ICookieSyncManager { ) => string; } -const hasFrequencyCapExpired = ( - frequencyCap: number, - lastSyncDate?: number, -): boolean => { - if (!lastSyncDate) { - return true; - } - - return ( - new Date().getTime() > - new Date(lastSyncDate).getTime() + frequencyCap * DAYS_IN_MILLISECONDS - ); -} - export default function CookieSyncManager( this: ICookieSyncManager, mpInstance: MParticleWebSDK @@ -67,100 +53,74 @@ export default function CookieSyncManager( // Public this.attemptCookieSync = ( - previousMPID: MPID, mpid: MPID, mpidIsNotInCookies?: boolean ): void => { - if (!mpid || mpInstance._Store.webviewBridgeEnabled) { + const { pixelConfigurations, webviewBridgeEnabled } = mpInstance._Store; + + if (!mpid || webviewBridgeEnabled) { return; } - const { pixelConfigurations } = mpInstance._Store; const persistence = mpInstance._Persistence.getPersistence(); + if (isEmpty(persistence)) { + return; + } + pixelConfigurations.forEach((pixelSettings: IPixelConfiguration) => { // set requiresConsent to false to start each additional pixel configuration // set to true only if filteringConsenRuleValues.values.length exists let requiresConsent = false; - // Filtering rules as defined in UI - const { filteringConsentRuleValues } = pixelSettings; + const { + filteringConsentRuleValues, + pixelUrl, + redirectUrl, + moduleId, + // Tells you how often we should do a cookie sync (in days) + frequencyCap, + } = pixelSettings; const { values } = filteringConsentRuleValues || {}; + if (isEmpty(pixelUrl)) { + return; + } + if (!isEmpty(values)) { requiresConsent = true; } - // Kit Module ID - const moduleId = pixelSettings.moduleId.toString(); - - // Tells you how often we should do a cookie sync (in days) - const frequencyCap = pixelSettings.frequencyCap; + // If MPID is new to cookies, we should not try to perform the cookie sync + // because a cookie sync can only occur once a user either consents or doesn't. + // we should not check if it's enabled if the user has a blank consent + if (requiresConsent && mpidIsNotInCookies) { + return; + } - // Url for cookie sync pixel - const pixelUrl = replaceAmpWithAmpersand(pixelSettings.pixelUrl); + const { isEnabledForUserConsent } = mpInstance._Consent; - const redirectUrl = pixelSettings.redirectUrl - ? replaceAmpWithAmpersand(pixelSettings.redirectUrl) - : null; + if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) { + return; + } - const urlWithRedirect = this.combineUrlWithRedirect( - mpid, - pixelUrl, - redirectUrl - ); + const cookieSyncDates: CookieSyncDates = persistence[mpid]?.csd ?? {}; + const lastSyncDateForModule: number = cookieSyncDates[moduleId] || null; - if (previousMPID && previousMPID !== mpid) { - if (persistence && persistence[mpid]) { - if (!persistence[mpid].csd) { - persistence[mpid].csd = {}; - } - self.performCookieSync( - urlWithRedirect, - moduleId, - mpid, - persistence[mpid].csd, - filteringConsentRuleValues, - mpidIsNotInCookies, - requiresConsent - ); - } + if (!isLastSyncDateExpired(frequencyCap, lastSyncDateForModule)) { return; - } else { - if (!persistence || !persistence[mpid]) { - return; - } - - if (!persistence[mpid].csd) { - persistence[mpid].csd = {}; - } - - const lastSyncDateForModule = persistence[mpid].csd[moduleId] || null; - - // Check to see if we need to refresh cookieSync - if (hasFrequencyCapExpired(frequencyCap, lastSyncDateForModule)) { - self.performCookieSync( - urlWithRedirect, - moduleId, - mpid, - persistence[mpid].csd, - filteringConsentRuleValues, - mpidIsNotInCookies, - requiresConsent - ); - } } - }); - }; - this.combineUrlWithRedirect = ( - mpid: MPID, - pixelUrl: string, - redirectUrl: string, - ): string => { - const url = replaceMPID(pixelUrl, mpid); - const redirect = redirectUrl ? replaceMPID(redirectUrl, mpid) : ''; - return url + encodeURIComponent(redirect); + // Url for cookie sync pixel + const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl) + + self.performCookieSync( + fullUrl, + moduleId.toString(), + mpid, + cookieSyncDates + ); + }); }; // Private @@ -169,35 +129,34 @@ export default function CookieSyncManager( moduleId: string, mpid: MPID, cookieSyncDates: CookieSyncDates, - filteringConsentRuleValues: IConsentRules, - mpidIsNotInCookies: boolean, - requiresConsent: boolean ): void => { - // if MPID is new to cookies, we should not try to perform the cookie sync - // because a cookie sync can only occur once a user either consents or doesn't - // we should not check if its enabled if the user has a blank consent - if (requiresConsent && mpidIsNotInCookies) { - return; - } + const img = document.createElement('img'); - if ( - // https://go.mparticle.com/work/SQDSDKS-5009 - mpInstance._Consent.isEnabledForUserConsent( - filteringConsentRuleValues, - mpInstance.Identity.getCurrentUser() - ) - ) { - const img = document.createElement('img'); - - mpInstance.Logger.verbose(InformationMessages.CookieSync); - img.onload = function() { - cookieSyncDates[moduleId] = new Date().getTime(); - mpInstance._Persistence.saveUserCookieSyncDatesToPersistence( - mpid, - cookieSyncDates - ); - }; - img.src = url; - } + mpInstance.Logger.verbose(InformationMessages.CookieSync); + img.onload = function() { + cookieSyncDates[moduleId] = new Date().getTime(); + + mpInstance._Persistence.saveUserCookieSyncDatesToPersistence( + mpid, + cookieSyncDates + ); + }; + img.src = url; }; } + +export const isLastSyncDateExpired = ( + frequencyCap: number, + lastSyncDate?: number +): boolean => { + // If there is no lastSyncDate, then there is no previous cookie sync, so we should sync the cookie + if (!lastSyncDate) { + return true; + } + + // Otherwise, compare the last sync date to determine if it should do a cookie sync again + return ( + new Date().getTime() > + new Date(lastSyncDate).getTime() + frequencyCap * DAYS_IN_MILLISECONDS + ); +}; \ No newline at end of file diff --git a/src/identity.js b/src/identity.js index a551c7cb5..686e574db 100644 --- a/src/identity.js +++ b/src/identity.js @@ -1230,10 +1230,7 @@ export default function Identity(mpInstance) { this.getUserIdentities().userIdentities, mpInstance._APIClient.prepareForwardingStats ); - mpInstance._CookieSyncManager.attemptCookieSync( - null, - this.getMPID() - ); + mpInstance._CookieSyncManager.attemptCookieSync(this.getMPID()); }, isLoggedIn: function() { return isLoggedIn; @@ -1562,7 +1559,6 @@ export default function Identity(mpInstance) { ); mpInstance._CookieSyncManager.attemptCookieSync( - previousMPID, identityApiResult.mpid, mpidIsNotInCookies ); diff --git a/src/utils.ts b/src/utils.ts index 389179d06..614208b4a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -180,6 +180,23 @@ const replaceMPID = (value: string, mpid: MPID): string => value.replace('%%mpid const replaceAmpWithAmpersand = (value: string): string => value.replace(/&/g, '&'); +const createCookieSyncUrl = ( + mpid: MPID, + pixelUrl: string, + redirectUrl?: string +): string => { + const modifiedPixelUrl = replaceAmpWithAmpersand(pixelUrl); + const modifiedDirectUrl = redirectUrl + ? replaceAmpWithAmpersand(redirectUrl) + : null; + + const url = replaceMPID(modifiedPixelUrl, mpid); + const redirect = modifiedDirectUrl + ? replaceMPID(modifiedDirectUrl, mpid) + : ''; + return url + encodeURIComponent(redirect); +}; + // FIXME: REFACTOR for V3 // only used in store.js to sanitize server-side formatting of // booleans when checking for `isDevelopmentMode` @@ -341,6 +358,7 @@ const getHref = (): string => { export { createCookieString, revertCookieString, + createCookieSyncUrl, valueof, converted, decoded, diff --git a/test/jest/cookieSyncManager.spec.ts b/test/jest/cookieSyncManager.spec.ts index 01467a92a..f8bbaa0d5 100644 --- a/test/jest/cookieSyncManager.spec.ts +++ b/test/jest/cookieSyncManager.spec.ts @@ -1,6 +1,8 @@ import CookieSyncManager, { DAYS_IN_MILLISECONDS, IPixelConfiguration, + CookieSyncDates, + isLastSyncDateExpired } from '../../src/cookieSyncManager'; import { MParticleWebSDK } from '../../src/sdkRuntimeModels'; import { testMPID } from '../src/config/constants'; @@ -13,10 +15,12 @@ const pixelSettings: IPixelConfiguration = { isProduction: true, settings: {}, frequencyCap: 14, - pixelUrl: '', - redirectUrl: '', + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', }; +const pixelUrlAndRedirectUrl = 'https://test.com%3Fredirect%3Dhttps%3A%2F%2Fredirect.com%26mpid%3DtestMPID'; + describe('CookieSyncManager', () => { describe('#attemptCookieSync', () => { // https://go.mparticle.com/work/SQDSDKS-6915 @@ -31,49 +35,63 @@ describe('CookieSyncManager', () => { csd: {} }}), }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', testMPID, {}, - undefined, - true, - false, - ); + ); }); - it('should return early if mpid is not defined', () => { + it('should not call performCookieSync if pixelURL is empty', () => { + const pixelSettingsWithoutPixelUrl = {...pixelSettings, pixelUrl: ''} const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + pixelConfigurations: [pixelSettingsWithoutPixelUrl], }, _Persistence: { getPersistence: () => ({testMPID: { csd: {} }}), }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, null, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); - it('should return early if webviewBridgeEnabled is true', () => { + it('should not call performCookieSync if mpid is not defined', () => { const mockMPInstance = ({ _Store: { - webviewBridgeEnabled: true, + webviewBridgeEnabled: false, pixelConfigurations: [pixelSettings], }, _Persistence: { @@ -86,22 +104,16 @@ describe('CookieSyncManager', () => { const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(null, true); expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); - it('should toggle requiresConsent value if filtering filteringConsentRuleValues are defined', () => { - const myPixelSettings: IPixelConfiguration = { - filteringConsentRuleValues: { - values: ['test'], - }, - } as unknown as IPixelConfiguration; - + it('should not call performCookieSync if webviewBridgeEnabled is true', () => { const mockMPInstance = ({ _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [{...pixelSettings, ...myPixelSettings}], + webviewBridgeEnabled: true, + pixelConfigurations: [pixelSettings], }, _Persistence: { getPersistence: () => ({testMPID: { @@ -113,142 +125,94 @@ describe('CookieSyncManager', () => { const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); - expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', - '5', - testMPID, - {}, - { values: ['test'] }, - true, - true, - ); + expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); - it('should update the urlWithRedirect if a redirectUrl is defined', () => { + it('should call performCookieSync when there are filteringConsentRuleValues and mpidIsNotInCookies = false', () => { const myPixelSettings: IPixelConfiguration = { pixelUrl: 'https://test.com', + moduleId: 5, redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', + filteringConsentRuleValues: { + values: ['test'], + }, } as unknown as IPixelConfiguration; const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, - pixelConfigurations: [{...pixelSettings, ...myPixelSettings}], + pixelConfigurations: [myPixelSettings], }, _Persistence: { getPersistence: () => ({testMPID: { csd: {} }}), }, - } as unknown) as MParticleWebSDK; - - const cookieSyncManager = new CookieSyncManager(mockMPInstance); - cookieSyncManager.performCookieSync = jest.fn(); - - cookieSyncManager.attemptCookieSync(null, testMPID, true); - - expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - 'https://test.com%3Fredirect%3Dhttps%3A%2F%2Fredirect.com%26mpid%3DtestMPID', - '5', - testMPID, - {}, - undefined, - true, - false, - ); - }); - - // https://go.mparticle.com/work/SQDSDKS-6915 - it('should call performCookieSync with mpid if previousMpid and mpid do not match', () => { - const mockMPInstance = ({ - _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), }, - _Persistence: { - getPersistence: () => ({testMPID: { - }}), + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync('other-mpid', testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, false); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', testMPID, {}, - undefined, - true, - false, ); }); - it('should include mpid AND csd when calling performCookieSync if previousMpid and mpid do not match', () => { + it('should update the cookie sync url if a redirectUrl is defined', () => { + const myPixelSettings: IPixelConfiguration = { + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', + } as unknown as IPixelConfiguration; + const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + pixelConfigurations: [{...pixelSettings, ...myPixelSettings}], }, _Persistence: { getPersistence: () => ({testMPID: { - csd: { 5: 1234567890 } + csd: {} }}), }, - } as unknown) as MParticleWebSDK; - - const cookieSyncManager = new CookieSyncManager(mockMPInstance); - cookieSyncManager.performCookieSync = jest.fn(); - - cookieSyncManager.attemptCookieSync('other-mpid', testMPID, true); - - expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', - '5', - testMPID, - { - 5: 1234567890 - }, - undefined, - true, - false, - ); - }); - - it('should call performCookieSync with mpid if previousMpid and mpid match', () => { - const mockMPInstance = ({ - _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), }, - _Persistence: { - getPersistence: () => ({testMPID: { - }}), + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(testMPID, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', testMPID, {}, - undefined, - true, - false, - ); + ); }); - it('should perform a cookie sync if lastSyncDateForModule is null', () => { + it('should call performCookieSync if lastSyncDateForModule is null', () => { const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, @@ -257,25 +221,30 @@ describe('CookieSyncManager', () => { _Persistence: { getPersistence: () => ({testMPID: {}}), }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', testMPID, {}, - undefined, - true, - false, ); }); - it('should perform a cookie sync if lastSyncDateForModule has passed the frequency cap', () => { + it('should call performCookieSync if lastSyncDateForModule has passed the frequency cap', () => { const now = new Date().getTime(); // Rev the date by one @@ -291,76 +260,67 @@ describe('CookieSyncManager', () => { csd: { 5: cookieSyncDateInPast } }}), }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', testMPID, { 5: cookieSyncDateInPast }, - undefined, - true, - false, ); }); - it('should perform a cookie sync if lastSyncDateForModule is past the frequency cap even if csd is empty', () => { - const now = new Date().getTime(); - + it('should call performCookieSync when there was not a previous cookie-sync', () => { const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, pixelConfigurations: [pixelSettings], }, _Persistence: { - // This will make lastSyncDateForModule undefined, which bypasses the - // actual time check, but still performs a cookie sync getPersistence: () => ({testMPID: {}}), }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + Logger: { + verbose: jest.fn(), + }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( - '', + pixelUrlAndRedirectUrl, '5', - testMPID, + testMPID, {}, - undefined, - true, - false, ); }); - it('should sync cookies when there was not a previous cookie-sync', () => { - const mockMPInstance = ({ - _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], - }, - _Persistence: { - getPersistence: () => ({}), - }, - } as unknown) as MParticleWebSDK; - - const cookieSyncManager = new CookieSyncManager(mockMPInstance); - - cookieSyncManager.attemptCookieSync(null, '456', true); - expect(mockMPInstance._Store.pixelConfigurations.length).toBe(1); - expect(mockMPInstance._Store.pixelConfigurations[0].moduleId).toBe(5); - }); - - it('should not perform a cookie sync if persistence is empty', () => { + it('should not call performCookieSync if persistence is empty', () => { const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, @@ -374,34 +334,33 @@ describe('CookieSyncManager', () => { const cookieSyncManager = new CookieSyncManager(mockMPInstance); cookieSyncManager.performCookieSync = jest.fn(); - cookieSyncManager.attemptCookieSync(null, testMPID, true); + cookieSyncManager.attemptCookieSync(testMPID, true); expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); - }); - describe('#performCookieSync', () => { - it('should add cookie sync dates to a tracking pixel', () => { - const mockImage = { - onload: jest.fn(), - src: '', - }; - jest.spyOn(document, 'createElement').mockReturnValue( - (mockImage as unknown) as HTMLImageElement - ); + it('should not call performCookieSync if the user has not consented to the cookie sync', () => { + const loggerSpy = jest.fn(); + const myPixelSettings: IPixelConfiguration = { + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', + filteringConsentRuleValues: { + values: ['test'], + }, + } as unknown as IPixelConfiguration; const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + pixelConfigurations: [myPixelSettings], }, _Persistence: { - setCookieSyncDates: jest.fn(), - getPersistence: jest.fn(), - saveUserCookieSyncDatesToPersistence: jest.fn(), + getPersistence: () => ({testMPID: { + csd: {} + }}), }, _Consent: { - isEnabledForUserConsent: jest.fn().mockReturnValue(true), + isEnabledForUserConsent: jest.fn().mockReturnValue(false), }, Identity: { getCurrentUser: jest.fn().mockReturnValue({ @@ -409,51 +368,39 @@ describe('CookieSyncManager', () => { }), }, Logger: { - verbose: jest.fn(), + verbose: loggerSpy, }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); - let cookieSyncDates = []; - cookieSyncManager.performCookieSync( - 'https://test.com', - 42, - '1234', - cookieSyncDates, - null, + cookieSyncManager.attemptCookieSync( + '123', false, - false ); - // Simulate image load - mockImage.onload(); - - expect(mockImage.src).toBe('https://test.com'); - expect(cookieSyncDates[42]).toBeDefined(); - expect(cookieSyncDates[42]).toBeGreaterThan(0); + expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); - it('should log a verbose message when a cookie sync is performed', () => { - const mockImage = { - onload: jest.fn(), - src: '', - }; - jest.spyOn(document, 'createElement').mockReturnValue( - (mockImage as unknown) as HTMLImageElement - ); - - const loggerSpy = jest.fn(); + it('should return early if requiresConsent and mpidIsNotInCookies are both true', () => { + const myPixelSettings: IPixelConfiguration = { + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', + filteringConsentRuleValues: { + values: ['test'], + }, + } as unknown as IPixelConfiguration; const loggerSpy = jest.fn(); const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], + pixelConfigurations: [myPixelSettings], // empty values will make require consent to be true }, _Persistence: { - setCookieSyncDates: jest.fn(), - getPersistence: jest.fn(), - saveUserCookieSyncDatesToPersistence: jest.fn(), + getPersistence: () => ({testMPID: { + csd: {} + }}), }, _Consent: { isEnabledForUserConsent: jest.fn().mockReturnValue(true), @@ -469,25 +416,19 @@ describe('CookieSyncManager', () => { } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); - let cookieSyncDates = []; - cookieSyncManager.performCookieSync( - 'https://test.com', - 42, - '1234', - cookieSyncDates, - null, - false, - false + cookieSyncManager.attemptCookieSync( + '123', + true ); - // Simulate image load - mockImage.onload(); - - expect(loggerSpy).toHaveBeenCalledWith('Performing cookie sync'); + expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); + }); - it('should return early if the user has not consented to the cookie sync', () => { + describe('#performCookieSync', () => { + it('should add cookie sync data to a tracking pixel', () => { const mockImage = { onload: jest.fn(), src: '', @@ -496,20 +437,17 @@ describe('CookieSyncManager', () => { (mockImage as unknown) as HTMLImageElement ); - const loggerSpy = jest.fn(); - const mockMPInstance = ({ _Store: { webviewBridgeEnabled: false, pixelConfigurations: [pixelSettings], }, _Persistence: { - setCookieSyncDates: jest.fn(), getPersistence: jest.fn(), saveUserCookieSyncDatesToPersistence: jest.fn(), }, _Consent: { - isEnabledForUserConsent: jest.fn().mockReturnValue(false), + isEnabledForUserConsent: jest.fn().mockReturnValue(true), }, Identity: { getCurrentUser: jest.fn().mockReturnValue({ @@ -517,39 +455,49 @@ describe('CookieSyncManager', () => { }), }, Logger: { - verbose: loggerSpy, + verbose: jest.fn(), }, } as unknown) as MParticleWebSDK; const cookieSyncManager = new CookieSyncManager(mockMPInstance); - let cookieSyncDates = []; + jest.useFakeTimers(); + + // Mock the Date constructor + const OriginalDate = global.Date; + class MockDate extends OriginalDate { + constructor() { + super(100); // Always return 100 as the date + } + } + + // Override global Date + global.Date = MockDate as unknown as DateConstructor; + + let cookieSyncDates: CookieSyncDates = {}; cookieSyncManager.performCookieSync( 'https://test.com', 42, '1234', cookieSyncDates, - null, - false, - false, ); // Simulate image load mockImage.onload(); - expect(mockImage.src).toBe(''); - expect(cookieSyncDates[42]).toBeUndefined(); - }); + expect(mockImage.src).toBe('https://test.com'); + expect(cookieSyncDates[42]).toBeDefined(); + expect(cookieSyncDates[42]).toBe(100) - it('should return early if requiresConsent and mpidIsNotInCookies are both true', () => { - const mockImage = { - onload: jest.fn(), - src: '', - }; - jest.spyOn(document, 'createElement').mockReturnValue( - (mockImage as unknown) as HTMLImageElement + expect(mockMPInstance._Persistence.saveUserCookieSyncDatesToPersistence).toBeCalledWith( + '1234', {42: 100} ); + global.Date = OriginalDate; + jest.useRealTimers(); + }); + + it('should log a verbose message when a cookie sync is performed', () => { const loggerSpy = jest.fn(); const mockMPInstance = ({ @@ -558,9 +506,9 @@ describe('CookieSyncManager', () => { pixelConfigurations: [pixelSettings], }, _Persistence: { - setCookieSyncDates: jest.fn(), - getPersistence: jest.fn(), - saveUserCookieSyncDatesToPersistence: jest.fn(), + getPersistence: () => ({testMPID: { + csd: {} + }}), }, _Consent: { isEnabledForUserConsent: jest.fn().mockReturnValue(true), @@ -577,64 +525,32 @@ describe('CookieSyncManager', () => { const cookieSyncManager = new CookieSyncManager(mockMPInstance); - let cookieSyncDates = []; + let cookieSyncDates = {}; cookieSyncManager.performCookieSync( 'https://test.com', 42, '1234', cookieSyncDates, - null, - true, - true, ); - // Simulate image load - mockImage.onload(); - - expect(mockImage.src).toBe(''); - expect(cookieSyncDates[42]).toBeUndefined(); + expect(loggerSpy).toHaveBeenCalledWith('Performing cookie sync'); }); }); - describe('#combineUrlWithRedirect', () => { - it('should combine a pixelUrl with a redirectUrl', () => { - const mockMPInstance = ({ - _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], - }, - } as unknown) as MParticleWebSDK; - - const cookieSyncManager = new CookieSyncManager(mockMPInstance); - - // Note: We expect that the input of the pixelUrl will include a `?` - // (ideally at the end of the string) - // so that the redirectUrl can be processed by the backend correctly - const result = cookieSyncManager.combineUrlWithRedirect( - '1234', - 'https://test.com/some/path?', - 'https://redirect.mparticle.com/v1/sync?esid=1234&MPID=%%mpid%%&ID=$UID&Key=testMPID&env=2' - ); - - expect(result).toBe('https://test.com/some/path?https%3A%2F%2Fredirect.mparticle.com%2Fv1%2Fsync%3Fesid%3D1234%26amp%3BMPID%3D1234%26amp%3BID%3D%24UID%26amp%3BKey%3DtestMPID%26amp%3Benv%3D2'); + describe('#isLastSyncDateExpired', () => { + const frequencyCap = 14; // days + it('should return true if there is no last sync date', () => { + expect(isLastSyncDateExpired(frequencyCap, null)).toBe(true); }); - it('should return the pixelUrl if no redirectUrl is defined', () => { - const mockMPInstance = ({ - _Store: { - webviewBridgeEnabled: false, - pixelConfigurations: [pixelSettings], - }, - } as unknown) as MParticleWebSDK; - - const cookieSyncManager = new CookieSyncManager(mockMPInstance); - - const result = cookieSyncManager.combineUrlWithRedirect( - '1234', - 'https://test.com', - ); + it('should return true if lastSyncDate is beyond the frequencyCap', () => { + const lastSyncDate = 0; // beginning of time + expect(isLastSyncDateExpired(frequencyCap, lastSyncDate)).toBe(true); + }); - expect(result).toBe('https://test.com'); + it('should return false if lastSyncDate is beyond the frequencyCap', () => { + const lastSyncDate = new Date().getTime(); // now + expect(isLastSyncDateExpired(frequencyCap, lastSyncDate)).toBe(false); }); }); }); diff --git a/test/jest/utils.spec.ts b/test/jest/utils.spec.ts index f9ac67dcd..73494c3b8 100644 --- a/test/jest/utils.spec.ts +++ b/test/jest/utils.spec.ts @@ -4,6 +4,7 @@ import { getHref, replaceMPID, replaceAmpWithAmpersand, + createCookieSyncUrl, } from '../../src/utils'; import { deleteAllCookies } from './utils'; @@ -183,4 +184,17 @@ describe('Utils', () => { ); }); }); -}); + + describe('#createCookieSyncUrl', () => { + const pixelUrl: string = 'https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=%%mpid%%&redir='; + const redirectUrl: string = 'https://cookiesync.mparticle.com/v1/sync?esid=123456&MPID=%%mpid%%&ID=${DD_UUID}&Key=mpApiKey&env=2'; + + it('should return a cookieSyncUrl when both pixelUrl and redirectUrl are not null', () => { + expect(createCookieSyncUrl('testMPID', pixelUrl, redirectUrl)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir=https%3A%2F%2Fcookiesync.mparticle.com%2Fv1%2Fsync%3Fesid%3D123456%26MPID%3DtestMPID%26ID%3D%24%7BDD_UUID%7D%26Key%3DmpApiKey%26env%3D2'); + }); + + it('should return a cookieSyncUrl when pixelUrl is not null but redirectUrl is null', () => { + expect(createCookieSyncUrl('testMPID', pixelUrl, null)).toBe('https://abc.abcdex.net/ibs:exampleid=12345&exampleuuid=testMPID&redir='); + }); + }); +}); \ No newline at end of file diff --git a/test/src/tests-cookie-syncing.ts b/test/src/tests-cookie-syncing.ts index 6eae30508..01fa95359 100644 --- a/test/src/tests-cookie-syncing.ts +++ b/test/src/tests-cookie-syncing.ts @@ -18,8 +18,8 @@ const pixelSettings: IPixelConfiguration = { isProduction: true, settings: {}, frequencyCap: 14, - pixelUrl: '', - redirectUrl: '', + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', }; declare global { @@ -277,8 +277,8 @@ describe('cookie syncing', function() { isProduction: true, settings: {}, frequencyCap: 14, - pixelUrl: '', - redirectUrl: '', + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', }, ], }; @@ -1222,8 +1222,8 @@ describe('cookie syncing', function() { isProduction: true, settings: {}, frequencyCap: 14, - pixelUrl: '', - redirectUrl: '', + pixelUrl: 'https://test.com', + redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%', }; pixelSettings1.filteringConsentRuleValues = { @@ -1247,8 +1247,8 @@ describe('cookie syncing', function() { isProduction: true, settings: {}, frequencyCap: 14, - pixelUrl: '', - redirectUrl: '', + pixelUrl: 'https://test2.com', + redirectUrl: '?redirect=https://redirect2.com&mpid=%%mpid%%', }; window.mParticle.config.pixelConfigs = [pixelSettings1, pixelSettings2];