Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;

export interface IPixelConfiguration {
Expand Down Expand Up @@ -111,8 +121,12 @@ export default function CookieSyncManager(
return;
}

// Url for cookie sync pixel
const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl)
// 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(
fullUrl,
Expand Down
16 changes: 13 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,28 @@ const replaceAmpWithAmpersand = (value: string): string => value.replace(/&amp;/
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
Expand Down
214 changes: 213 additions & 1 deletion test/jest/cookieSyncManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
26 changes: 26 additions & 0 deletions test/jest/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading