Skip to content

Commit

Permalink
feat: Include consent query parameters for cookie sync
Browse files Browse the repository at this point in the history
  • Loading branch information
rmi22186 committed Jan 21, 2025
1 parent 29f34b0 commit 9246789
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 16 deletions.
79 changes: 66 additions & 13 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export const DAYS_IN_MILLISECONDS = 1000 * 60 * 60 * 24;

export type CookieSyncDates = Dictionary<number>;

// this is just a partial definition of TCData for the purposes of our implementation. The full schema can be found here:
// https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#tcdata
type TCData = {
gdprApplies?: boolean;
tcString?: string;
};
declare global {
interface Window {
__tcfapi: any;
}
}

export interface IPixelConfiguration {
name?: string;
moduleId: number;
Expand All @@ -38,12 +50,13 @@ export interface ICookieSyncManager {
mpid: MPID,
cookieSyncDates: CookieSyncDates,
) => void;
combineUrlWithRedirect: (
performCookieSyncWithGDPR: (
url: string,
moduleId: string,
mpid: MPID,
pixelUrl: string,
redirectUrl: string
) => string;
}
cookieSyncDates: CookieSyncDates,
) => void
};

export default function CookieSyncManager(
this: ICookieSyncManager,
Expand Down Expand Up @@ -113,13 +126,18 @@ export default function CookieSyncManager(

// Url for cookie sync pixel
const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl)

self.performCookieSync(
fullUrl,
moduleId.toString(),
mpid,
cookieSyncDates
);
const moduleIdString = moduleId.toString();

if (isTcfApiAvailable()) {
self.performCookieSyncWithGDPR(fullUrl, moduleIdString, mpid, cookieSyncDates);
} else {
self.performCookieSync(
fullUrl,
moduleIdString,
mpid,
cookieSyncDates
);
}
});
};

Expand All @@ -143,6 +161,33 @@ export default function CookieSyncManager(
};
img.src = url;
};

this.performCookieSyncWithGDPR = (
url: string,
moduleId: string,
mpid: MPID,
cookieSyncDates: CookieSyncDates
): void => {
let _url: string = url;

function callback(inAppTCData: TCData, success: boolean): void {
// If call to getInAppTCData is successful, append the applicable gdpr
// and tcString to url
if (success) {
const gdprApplies = inAppTCData.gdprApplies ? 1 : 0;
const tcString = inAppTCData.tcString;
_url += `&gdpr=${gdprApplies}&gdpr_consent=${tcString}`;
}
self.performCookieSync(_url, moduleId, mpid, cookieSyncDates);
}
try {
window.__tcfapi('getInAppTCData', 2, callback);
}
catch (error) {
const errorMessage = (error as Error).message || error.toString();
mpInstance.Logger.error(errorMessage);
}
};
}

export const isLastSyncDateExpired = (
Expand All @@ -159,4 +204,12 @@ export const isLastSyncDateExpired = (
new Date().getTime() >
new Date(lastSyncDate).getTime() + frequencyCap * DAYS_IN_MILLISECONDS
);
};
};

export const isTcfApiAvailable = (): boolean => {
if (typeof window.__tcfapi === 'function') {
return true;
} else {
return false;
}
}
142 changes: 139 additions & 3 deletions 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,
isTcfApiAvailable
} from '../../src/cookieSyncManager';
import { MParticleWebSDK } from '../../src/sdkRuntimeModels';
import { testMPID } from '../src/config/constants';
Expand Down Expand Up @@ -390,7 +391,8 @@ describe('CookieSyncManager', () => {
filteringConsentRuleValues: {
values: ['test'],
},
} as unknown as IPixelConfiguration; const loggerSpy = jest.fn();
} as unknown as IPixelConfiguration;
const loggerSpy = jest.fn();

const mockMPInstance = ({
_Store: {
Expand Down Expand Up @@ -553,4 +555,138 @@ describe('CookieSyncManager', () => {
expect(isLastSyncDateExpired(frequencyCap, lastSyncDate)).toBe(false);
});
});
});

describe('#isTcfApiAvailable', () => {
it('should return true if window.__tcfapi exists on the page', () => {
window.__tcfapi = jest.fn();
expect(isTcfApiAvailable()).toBe(true)
});
});

describe('#performCookieSyncWithGDPR', () => {
const verboseLoggerSpy = jest.fn();
const errorLoggerSpy = jest.fn();

let cookieSyncManager: any;
const mockMpInstance = ({
Logger: {
verbose: verboseLoggerSpy,
error: errorLoggerSpy
},
_Persistence: {
saveUserCookieSyncDatesToPersistence: jest.fn(),
},
Identity: {
getCurrentUser: jest.fn(),
},
} as unknown) as MParticleWebSDK;

beforeEach(() => {
cookieSyncManager = new CookieSyncManager(mockMpInstance);
global.window.__tcfapi = jest.fn();
})

afterEach(() => {
jest.clearAllMocks();
})

it('should append GDPR parameters to the URL if __tcfapi callback succeeds', () => {
const mockCookieSyncDates = {};
const mockUrl = 'https://example.com/cookie-sync';
const moduleId = 'module1';
const mpid = '12345';

// Mock __tcfapi to call the callback with success
(window.__tcfapi as jest.Mock).mockImplementation((
command,
version,
callback
) => {
expect(command).toBe('getInAppTCData');
expect(version).toBe(2);
// Simulate a successful response
callback(
{ gdprApplies: true, tcString: 'test-consent-string' },
true
);
});

const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');

// Call the function under test
cookieSyncManager.performCookieSyncWithGDPR(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);

expect(performCookieSyncSpy).toHaveBeenCalledWith(
`${mockUrl}&gdpr=1&gdpr_consent=test-consent-string`,
moduleId,
mpid,
mockCookieSyncDates
);
});

it('should fall back to performCookieSync if __tcfapi callback fails', () => {
const mockCookieSyncDates = {};
const mockUrl = 'https://example.com/cookie-sync';
const moduleId = 'module1';
const mpid = '12345';

// Mock __tcfapi to call the callback with failure
(window.__tcfapi as jest.Mock).mockImplementation((command, version, callback) => {
callback(null, false); // Simulate a failure
});

// Spy on the `performCookieSync` method
const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');

// Call the function under test
cookieSyncManager.performCookieSyncWithGDPR(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);

// Assert that the fallback method was called with the original URL
expect(performCookieSyncSpy).toHaveBeenCalledWith(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);
});

it('should handle exceptions thrown by __tcfapi gracefully', () => {
const mockCookieSyncDates = {};
const mockUrl = 'https://example.com/cookie-sync';
const moduleId = 'module1';
const mpid = '12345';

// Mock __tcfapi to throw an error
(window.__tcfapi as jest.Mock).mockImplementation(() => {
throw new Error('Test Error');
});

// Spy on the `performCookieSync` method
const performCookieSyncSpy = jest.spyOn(cookieSyncManager, 'performCookieSync');

// Call the function under test
cookieSyncManager.performCookieSyncWithGDPR(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);

// Assert that the fallback method was called with the original URL
expect(performCookieSyncSpy).not.toHaveBeenCalled();

// Ensure the error was logged (if applicable)
expect(errorLoggerSpy).toHaveBeenCalledWith('Test Error');
});
});
});

0 comments on commit 9246789

Please sign in to comment.