From 5cfbf343c89c03c6e324ea56d6d3728728ea478b Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Thu, 13 Nov 2025 22:52:42 -0500 Subject: [PATCH 1/2] fix: Append domain query parameter to Trade Desk cookie syncs --- src/cookieSyncManager.ts | 14 +- src/utils.ts | 16 ++- test/jest/cookieSyncManager.spec.ts | 214 +++++++++++++++++++++++++++- test/jest/utils.spec.ts | 26 ++++ 4 files changed, 265 insertions(+), 5 deletions(-) diff --git a/src/cookieSyncManager.ts b/src/cookieSyncManager.ts index c02f35334..5f087e439 100644 --- a/src/cookieSyncManager.ts +++ b/src/cookieSyncManager.ts @@ -13,6 +13,16 @@ const { InformationMessages } = Messages; export const DAYS_IN_MILLISECONDS = 1000 * 60 * 60 * 24; +// Partner module IDs for cookie sync configurations +export const PARTNER_MODULE_IDS = { + AdobeEventForwarder: 11, + DoubleclickDFP: 41, + AppNexus: 50, + Lotame: 58, + TradeDesk: 103, + VerizonMedia: 155, +} as const; + export type CookieSyncDates = Dictionary; export interface IPixelConfiguration { @@ -112,7 +122,9 @@ export default function CookieSyncManager( } // Url for cookie sync pixel - const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl) + // Add domain parameter for Trade Desk + const domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined; + const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl, domain); self.performCookieSync( fullUrl, diff --git a/src/utils.ts b/src/utils.ts index 52a7b3de4..4e44ea914 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -206,18 +206,28 @@ const replaceAmpWithAmpersand = (value: string): string => value.replace(/&/ const createCookieSyncUrl = ( mpid: MPID, pixelUrl: string, - redirectUrl?: string + redirectUrl?: string, + domain?: string ): string => { const modifiedPixelUrl = replaceAmpWithAmpersand(pixelUrl); const modifiedDirectUrl = redirectUrl ? replaceAmpWithAmpersand(redirectUrl) : null; - const url = replaceMPID(modifiedPixelUrl, mpid); + let url = replaceMPID(modifiedPixelUrl, mpid); + const redirect = modifiedDirectUrl ? replaceMPID(modifiedDirectUrl, mpid) : ''; - return url + encodeURIComponent(redirect); + + let fullUrl = url + encodeURIComponent(redirect); + + if (domain) { + const separator = fullUrl.includes('?') ? '&' : '?'; + fullUrl += `${separator}domain=${domain}`; + } + + return fullUrl; }; // FIXME: REFACTOR for V3 diff --git a/test/jest/cookieSyncManager.spec.ts b/test/jest/cookieSyncManager.spec.ts index 701008a5b..624e5304d 100644 --- a/test/jest/cookieSyncManager.spec.ts +++ b/test/jest/cookieSyncManager.spec.ts @@ -2,7 +2,8 @@ import CookieSyncManager, { DAYS_IN_MILLISECONDS, IPixelConfiguration, CookieSyncDates, - isLastSyncDateExpired + isLastSyncDateExpired, + PARTNER_MODULE_IDS } from '../../src/cookieSyncManager'; import { IMParticleWebSDKInstance } from '../../src/mp-instance'; import { testMPID } from '../src/config/constants'; @@ -425,6 +426,217 @@ describe('CookieSyncManager', () => { expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled(); }); + + describe('Trade Desk domain parameter', () => { + const originalLocation = window.location; + + beforeEach(() => { + // Mock window.location.hostname + delete (window as any).location; + (window as any).location = { ...originalLocation, hostname: 'example.com' }; + }); + + afterEach(() => { + (window as any).location = originalLocation; + }); + + it('should add domain parameter for Trade Desk (module ID 103)', () => { + const tradeDeskPixelSettings: IPixelConfiguration = { + ...pixelSettings, + moduleId: PARTNER_MODULE_IDS.TradeDesk, // 103 + pixelUrl: 'https://insight.adsrvr.org/track/up?adv=abc123', + redirectUrl: '', + }; + + const mockMPInstance = ({ + _Store: { + webviewBridgeEnabled: false, + pixelConfigurations: [tradeDeskPixelSettings], + }, + _Persistence: { + getPersistence: () => ({testMPID: { + csd: {} + }}), + }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + } as unknown) as IMParticleWebSDKInstance; + + const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); + + cookieSyncManager.attemptCookieSync(testMPID, true); + + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + 'https://insight.adsrvr.org/track/up?adv=abc123&domain=example.com', + '103', + testMPID, + {}, + ); + }); + + it('should not add domain parameter for non-Trade Desk partners', () => { + const nonTradeDeskPixelSettings: IPixelConfiguration = { + ...pixelSettings, + moduleId: PARTNER_MODULE_IDS.AppNexus, // 50 + pixelUrl: 'https://ib.adnxs.com/cookie_sync?adv=abc123', + redirectUrl: '', + }; + + const mockMPInstance = ({ + _Store: { + webviewBridgeEnabled: false, + pixelConfigurations: [nonTradeDeskPixelSettings], + }, + _Persistence: { + getPersistence: () => ({testMPID: { + csd: {} + }}), + }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + } as unknown) as IMParticleWebSDKInstance; + + const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); + + cookieSyncManager.attemptCookieSync(testMPID, true); + + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + 'https://ib.adnxs.com/cookie_sync?adv=abc123', + '50', + testMPID, + {}, + ); + }); + + it('should handle multiple pixel configurations with mixed Trade Desk and non-Trade Desk', () => { + const tradeDeskPixelSettings: IPixelConfiguration = { + ...pixelSettings, + moduleId: PARTNER_MODULE_IDS.TradeDesk, + pixelUrl: 'https://insight.adsrvr.org/track/up?adv=ttd123', + redirectUrl: '', + }; + + const appNexusPixelSettings: IPixelConfiguration = { + ...pixelSettings, + moduleId: PARTNER_MODULE_IDS.AppNexus, + pixelUrl: 'https://ib.adnxs.com/cookie_sync?adv=anx123', + redirectUrl: '', + }; + + const mockMPInstance = ({ + _Store: { + webviewBridgeEnabled: false, + pixelConfigurations: [tradeDeskPixelSettings, appNexusPixelSettings], + }, + _Persistence: { + getPersistence: () => ({testMPID: { + csd: {} + }}), + }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + } as unknown) as IMParticleWebSDKInstance; + + const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); + + cookieSyncManager.attemptCookieSync(testMPID, true); + + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledTimes(2); + + // Check Trade Desk call (with domain) + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + 'https://insight.adsrvr.org/track/up?adv=ttd123&domain=example.com', + '103', + testMPID, + {}, + ); + + // Check AppNexus call (without domain) + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + 'https://ib.adnxs.com/cookie_sync?adv=anx123', + '50', + testMPID, + {}, + ); + }); + + it('should handle domain parameter with hyphens and subdomains', () => { + // Mock a hostname with hyphens and subdomains + delete (window as any).location; + (window as any).location = { ...originalLocation, hostname: 'sub-domain.example.com' }; + + const tradeDeskPixelSettings: IPixelConfiguration = { + ...pixelSettings, + moduleId: PARTNER_MODULE_IDS.TradeDesk, + pixelUrl: 'https://insight.adsrvr.org/track/up', + redirectUrl: '', + }; + + const mockMPInstance = ({ + _Store: { + webviewBridgeEnabled: false, + pixelConfigurations: [tradeDeskPixelSettings], + }, + _Persistence: { + getPersistence: () => ({testMPID: { + csd: {} + }}), + }, + _Consent: { + isEnabledForUserConsent: jest.fn().mockReturnValue(true), + }, + Identity: { + getCurrentUser: jest.fn().mockReturnValue({ + getMPID: () => testMPID, + }), + }, + } as unknown) as IMParticleWebSDKInstance; + + const cookieSyncManager = new CookieSyncManager(mockMPInstance); + cookieSyncManager.performCookieSync = jest.fn(); + + cookieSyncManager.attemptCookieSync(testMPID, true); + + expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith( + 'https://insight.adsrvr.org/track/up?domain=sub-domain.example.com', + '103', + testMPID, + {}, + ); + }); + }); + }); + + describe('PARTNER_MODULE_IDS', () => { + it('should contain all expected partner module IDs', () => { + expect(PARTNER_MODULE_IDS.AdobeEventForwarder).toBe(11); + expect(PARTNER_MODULE_IDS.DoubleclickDFP).toBe(41); + expect(PARTNER_MODULE_IDS.AppNexus).toBe(50); + expect(PARTNER_MODULE_IDS.Lotame).toBe(58); + expect(PARTNER_MODULE_IDS.TradeDesk).toBe(103); + expect(PARTNER_MODULE_IDS.VerizonMedia).toBe(155); + }); }); describe('#performCookieSync', () => { diff --git a/test/jest/utils.spec.ts b/test/jest/utils.spec.ts index 5e1d67f4e..4545e691e 100644 --- a/test/jest/utils.spec.ts +++ b/test/jest/utils.spec.ts @@ -240,6 +240,32 @@ describe('Utils', () => { 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='); }); + + it('should add domain parameter when provided', () => { + const simplePixelUrl = 'https://test.com/pixel'; + expect(createCookieSyncUrl('testMPID', simplePixelUrl, null, 'example.com')).toBe('https://test.com/pixel?domain=example.com'); + }); + + it('should handle domain parameter with hyphens and dots', () => { + const simplePixelUrl = 'https://test.com/pixel'; + expect(createCookieSyncUrl('testMPID', simplePixelUrl, null, 'sub-domain.example.com')).toBe('https://test.com/pixel?domain=sub-domain.example.com'); + }); + + it('should handle domain parameter with redirect URL', () => { + const simplePixelUrl = 'https://test.com/pixel'; + const simpleRedirectUrl = 'https://redirect.com/callback'; + expect(createCookieSyncUrl('testMPID', simplePixelUrl, simpleRedirectUrl, 'example.com')).toBe('https://test.com/pixelhttps%3A%2F%2Fredirect.com%2Fcallback?domain=example.com'); + }); + + it('should not add domain parameter when domain is undefined', () => { + const simplePixelUrl = 'https://test.com/pixel'; + expect(createCookieSyncUrl('testMPID', simplePixelUrl, null, undefined)).toBe('https://test.com/pixel'); + }); + + it('should not add domain parameter when domain is empty string', () => { + const simplePixelUrl = 'https://test.com/pixel'; + expect(createCookieSyncUrl('testMPID', simplePixelUrl, null, '')).toBe('https://test.com/pixel'); + }); }); describe('#inArray', () => { From 3a7aae5eabd505a9295ff272b69d35c6425e6281 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 14 Nov 2025 08:44:18 -0500 Subject: [PATCH 2/2] update comments --- src/cookieSyncManager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cookieSyncManager.ts b/src/cookieSyncManager.ts index 5f087e439..036080181 100644 --- a/src/cookieSyncManager.ts +++ b/src/cookieSyncManager.ts @@ -121,9 +121,11 @@ export default function CookieSyncManager( return; } - // Url for cookie sync pixel - // Add domain parameter for Trade Desk + // The Trade Desk requires a URL parameter for GDPR enabled users. + // It is optional but to simplify the code, we add it for all Trade + // // Desk cookie syncs. const domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined; + // Add domain parameter for Trade Desk const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl, domain); self.performCookieSync(