Skip to content

Commit 2c977b9

Browse files
committed
feat: Include consent query parameters for cookie sync
1 parent b28c45d commit 2c977b9

File tree

2 files changed

+205
-16
lines changed

2 files changed

+205
-16
lines changed

src/cookieSyncManager.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ export const DAYS_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
1515

1616
export type CookieSyncDates = Dictionary<number>;
1717

18+
// this is just a partial definition of TCData for the purposes of our implementation. The full schema can be found here:
19+
// https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#tcdata
20+
type TCData = {
21+
gdprApplies?: boolean;
22+
tcString?: string;
23+
};
24+
declare global {
25+
interface Window {
26+
__tcfapi: any;
27+
}
28+
}
29+
1830
export interface IPixelConfiguration {
1931
name?: string;
2032
moduleId: number;
@@ -38,12 +50,13 @@ export interface ICookieSyncManager {
3850
mpid: MPID,
3951
cookieSyncDates: CookieSyncDates,
4052
) => void;
41-
combineUrlWithRedirect: (
53+
performCookieSyncWithGDPR: (
54+
url: string,
55+
moduleId: string,
4256
mpid: MPID,
43-
pixelUrl: string,
44-
redirectUrl: string
45-
) => string;
46-
}
57+
cookieSyncDates: CookieSyncDates,
58+
) => void
59+
};
4760

4861
export default function CookieSyncManager(
4962
this: ICookieSyncManager,
@@ -113,13 +126,18 @@ export default function CookieSyncManager(
113126

114127
// Url for cookie sync pixel
115128
const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl)
116-
117-
self.performCookieSync(
118-
fullUrl,
119-
moduleId.toString(),
120-
mpid,
121-
cookieSyncDates
122-
);
129+
const moduleIdString = moduleId.toString();
130+
131+
if (isTcfApiAvailable()) {
132+
self.performCookieSyncWithGDPR(fullUrl, moduleIdString, mpid, cookieSyncDates);
133+
} else {
134+
self.performCookieSync(
135+
fullUrl,
136+
moduleIdString,
137+
mpid,
138+
cookieSyncDates
139+
);
140+
}
123141
});
124142
};
125143

@@ -143,6 +161,33 @@ export default function CookieSyncManager(
143161
};
144162
img.src = url;
145163
};
164+
165+
this.performCookieSyncWithGDPR = (
166+
url: string,
167+
moduleId: string,
168+
mpid: MPID,
169+
cookieSyncDates: CookieSyncDates
170+
): void => {
171+
let _url: string = url;
172+
173+
function callback(inAppTCData: TCData, success: boolean): void {
174+
// If call to getInAppTCData is successful, append the applicable gdpr
175+
// and tcString to url
176+
if (success) {
177+
const gdprApplies = inAppTCData.gdprApplies ? 1 : 0;
178+
const tcString = inAppTCData.tcString;
179+
_url += `&gdpr=${gdprApplies}&gdpr_consent=${tcString}`;
180+
}
181+
self.performCookieSync(_url, moduleId, mpid, cookieSyncDates);
182+
}
183+
try {
184+
window.__tcfapi('getInAppTCData', 2, callback);
185+
}
186+
catch (error) {
187+
const errorMessage = (error as Error).message || error.toString();
188+
mpInstance.Logger.error(errorMessage);
189+
}
190+
};
146191
}
147192

148193
export const isLastSyncDateExpired = (
@@ -159,4 +204,12 @@ export const isLastSyncDateExpired = (
159204
new Date().getTime() >
160205
new Date(lastSyncDate).getTime() + frequencyCap * DAYS_IN_MILLISECONDS
161206
);
162-
};
207+
};
208+
209+
export const isTcfApiAvailable = (): boolean => {
210+
if (typeof window.__tcfapi === 'function') {
211+
return true;
212+
} else {
213+
return false;
214+
}
215+
}

test/jest/cookieSyncManager.spec.ts

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import CookieSyncManager, {
22
DAYS_IN_MILLISECONDS,
33
IPixelConfiguration,
44
CookieSyncDates,
5-
isLastSyncDateExpired
5+
isLastSyncDateExpired,
6+
isTcfApiAvailable
67
} from '../../src/cookieSyncManager';
78
import { MParticleWebSDK } from '../../src/sdkRuntimeModels';
89
import { testMPID } from '../src/config/constants';
@@ -390,7 +391,8 @@ describe('CookieSyncManager', () => {
390391
filteringConsentRuleValues: {
391392
values: ['test'],
392393
},
393-
} as unknown as IPixelConfiguration; const loggerSpy = jest.fn();
394+
} as unknown as IPixelConfiguration;
395+
const loggerSpy = jest.fn();
394396

395397
const mockMPInstance = ({
396398
_Store: {
@@ -553,4 +555,138 @@ describe('CookieSyncManager', () => {
553555
expect(isLastSyncDateExpired(frequencyCap, lastSyncDate)).toBe(false);
554556
});
555557
});
556-
});
558+
559+
describe('#isTcfApiAvailable', () => {
560+
it('should return true if window.__tcfapi exists on the page', () => {
561+
window.__tcfapi = jest.fn();
562+
expect(isTcfApiAvailable()).toBe(true)
563+
});
564+
});
565+
566+
describe('#performCookieSyncWithGDPR', () => {
567+
const verboseLoggerSpy = jest.fn();
568+
const errorLoggerSpy = jest.fn();
569+
570+
let cookieSyncManager: any;
571+
const mockMpInstance = ({
572+
Logger: {
573+
verbose: verboseLoggerSpy,
574+
error: errorLoggerSpy
575+
},
576+
_Persistence: {
577+
saveUserCookieSyncDatesToPersistence: jest.fn(),
578+
},
579+
Identity: {
580+
getCurrentUser: jest.fn(),
581+
},
582+
} as unknown) as MParticleWebSDK;
583+
584+
beforeEach(() => {
585+
cookieSyncManager = new CookieSyncManager(mockMpInstance);
586+
global.window.__tcfapi = jest.fn();
587+
})
588+
589+
afterEach(() => {
590+
jest.clearAllMocks();
591+
})
592+
593+
it('should append GDPR parameters to the URL if __tcfapi callback succeeds', () => {
594+
const mockCookieSyncDates = {};
595+
const mockUrl = 'https://example.com/cookie-sync';
596+
const moduleId = 'module1';
597+
const mpid = '12345';
598+
599+
// Mock __tcfapi to call the callback with success
600+
(window.__tcfapi as jest.Mock).mockImplementation((
601+
command,
602+
version,
603+
callback
604+
) => {
605+
expect(command).toBe('getInAppTCData');
606+
expect(version).toBe(2);
607+
// Simulate a successful response
608+
callback(
609+
{ gdprApplies: true, tcString: 'test-consent-string' },
610+
true
611+
);
612+
});
613+
614+
const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');
615+
616+
// Call the function under test
617+
cookieSyncManager.performCookieSyncWithGDPR(
618+
mockUrl,
619+
moduleId,
620+
mpid,
621+
mockCookieSyncDates
622+
);
623+
624+
expect(performCookieSyncSpy).toHaveBeenCalledWith(
625+
`${mockUrl}&gdpr=1&gdpr_consent=test-consent-string`,
626+
moduleId,
627+
mpid,
628+
mockCookieSyncDates
629+
);
630+
});
631+
632+
it('should fall back to performCookieSync if __tcfapi callback fails', () => {
633+
const mockCookieSyncDates = {};
634+
const mockUrl = 'https://example.com/cookie-sync';
635+
const moduleId = 'module1';
636+
const mpid = '12345';
637+
638+
// Mock __tcfapi to call the callback with failure
639+
(window.__tcfapi as jest.Mock).mockImplementation((command, version, callback) => {
640+
callback(null, false); // Simulate a failure
641+
});
642+
643+
// Spy on the `performCookieSync` method
644+
const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');
645+
646+
// Call the function under test
647+
cookieSyncManager.performCookieSyncWithGDPR(
648+
mockUrl,
649+
moduleId,
650+
mpid,
651+
mockCookieSyncDates
652+
);
653+
654+
// Assert that the fallback method was called with the original URL
655+
expect(performCookieSyncSpy).toHaveBeenCalledWith(
656+
mockUrl,
657+
moduleId,
658+
mpid,
659+
mockCookieSyncDates
660+
);
661+
});
662+
663+
it('should handle exceptions thrown by __tcfapi gracefully', () => {
664+
const mockCookieSyncDates = {};
665+
const mockUrl = 'https://example.com/cookie-sync';
666+
const moduleId = 'module1';
667+
const mpid = '12345';
668+
669+
// Mock __tcfapi to throw an error
670+
(window.__tcfapi as jest.Mock).mockImplementation(() => {
671+
throw new Error('Test Error');
672+
});
673+
674+
// Spy on the `performCookieSync` method
675+
const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');
676+
677+
// Call the function under test
678+
cookieSyncManager.performCookieSyncWithGDPR(
679+
mockUrl,
680+
moduleId,
681+
mpid,
682+
mockCookieSyncDates
683+
);
684+
685+
// Assert that the fallback method was called with the original URL
686+
expect(performCookieSyncSpy).not.toHaveBeenCalled();
687+
688+
// Ensure the error was logged (if applicable)
689+
expect(errorLoggerSpy).toHaveBeenCalledWith('Test Error');
690+
});
691+
});
692+
});

0 commit comments

Comments
 (0)