Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Include consent query parameters for cookie sync #966

Open
wants to merge 12 commits into
base: refactor/ts-migration-blackout-2024
Choose a base branch
from
Open
38 changes: 29 additions & 9 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from './utils';
import Constants from './constants';
import { MParticleWebSDK } from './sdkRuntimeModels';
import { MPID } from '@mparticle/web-sdk';
import { Logger, MPID } from '@mparticle/web-sdk';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { Logger, MPID } from '@mparticle/web-sdk';
import { MPID } from '@mparticle/web-sdk';

import { IConsentRules } from './consent';

const { Messages } = Constants;
Expand Down Expand Up @@ -66,10 +66,10 @@ export default function CookieSyncManager(
const self = this;

// Public
this.attemptCookieSync = async (
this.attemptCookieSync = (
mpid: MPID,
mpidIsNotInCookies?: boolean
): Promise<void> => {
): void => {
const { pixelConfigurations, webviewBridgeEnabled } = mpInstance._Store;

if (!mpid || webviewBridgeEnabled) {
Expand All @@ -82,7 +82,7 @@ export default function CookieSyncManager(
return;
}

for (const pixelSettings of pixelConfigurations as IPixelConfiguration[]) {
pixelConfigurations.forEach(async (pixelSettings: IPixelConfiguration) => {
// set requiresConsent to false to start each additional pixel configuration
// set to true only if filteringConsenRuleValues.values.length exists
let requiresConsent = false;
Expand Down Expand Up @@ -129,14 +129,23 @@ export default function CookieSyncManager(
const cookieSyncUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl)
const moduleIdString = moduleId.toString();

const fullUrl: string = isTcfApiAvailable() ? await getGdprConsentUrl(cookieSyncUrl) : cookieSyncUrl;
// fullUrl will be the cookieSyncUrl if an error is thrown, or there if TcfApi is not available
let fullUrl: string;
try {
fullUrl = isTcfApiAvailable() ? await appendGdprConsentUrl(cookieSyncUrl, mpInstance.Logger) : cookieSyncUrl;
} catch (error) {
fullUrl = cookieSyncUrl;
const errorMessage = (error as Error).message || error.toString();
mpInstance.Logger.error(errorMessage);
}

self.performCookieSync(
fullUrl,
moduleIdString,
mpid,
cookieSyncDates
);
}
})
};

// Private
Expand All @@ -146,6 +155,10 @@ export default function CookieSyncManager(
mpid: MPID,
cookieSyncDates: CookieSyncDates,
): void => {
console.log(url)
console.log(moduleId);
console.log(mpid);
console.log(cookieSyncDates)
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
const img = document.createElement('img');

mpInstance.Logger.verbose(InformationMessages.CookieSync);
Expand Down Expand Up @@ -179,19 +192,26 @@ export const isLastSyncDateExpired = (

export const isTcfApiAvailable = (): boolean => isFunction(window.__tcfapi);

async function getGdprConsentUrl(url: string): Promise<string> {
export async function appendGdprConsentUrl(url: string, logger: Logger): Promise<string> {
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
return new Promise((resolve, reject) => {
try {
window.__tcfapi('getInAppTCData', 2, (inAppTCData: TCData, success: boolean) => {
const tcfAPICallBack = (inAppTCData: TCData, success: boolean) => {
if (success && inAppTCData) {
const gdprApplies = inAppTCData.gdprApplies ? 1 : 0;
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
const tcString = inAppTCData.tcString;
resolve(`${url}&gdpr=${gdprApplies}&gdpr_consent=${tcString}`);
} else {
resolve(url); // No GDPR data; fallback to original URL
}
});
}

// `getInAppTCData` is the function name
// 2 is the version of TCF (2.2 as of 1/22/2025)
// callback
window.__tcfapi('getInAppTCData', 2, tcfAPICallBack);
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
const errorMessage = (error as Error).message || error.toString();
logger.error(errorMessage);
reject(error);
}
});
Expand Down
122 changes: 57 additions & 65 deletions test/jest/cookieSyncManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import CookieSyncManager, {
IPixelConfiguration,
CookieSyncDates,
isLastSyncDateExpired,
isTcfApiAvailable
isTcfApiAvailable,
appendGdprConsentUrl
} from '../../src/cookieSyncManager';
import { MParticleWebSDK } from '../../src/sdkRuntimeModels';
import { testMPID } from '../src/config/constants';
Expand Down Expand Up @@ -563,9 +564,10 @@ describe('CookieSyncManager', () => {
});
});

describe('#performCookieSyncWithGDPR', () => {
describe('#appendGdprConsentUrl', () => {
const verboseLoggerSpy = jest.fn();
const errorLoggerSpy = jest.fn();
const mockUrl = 'https://example.com/cookie-sync';

let cookieSyncManager: any;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this?

const mockMpInstance = ({
Expand All @@ -590,12 +592,7 @@ describe('CookieSyncManager', () => {
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';

it('should append GDPR parameters to the URL if __tcfapi callback succeeds', async () => {
// Mock __tcfapi to call the callback with success
(window.__tcfapi as jest.Mock).mockImplementation((
command,
Expand All @@ -611,82 +608,77 @@ describe('CookieSyncManager', () => {
);
});

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

// Call the function under test
cookieSyncManager.performCookieSyncWithGDPR(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);
const fullUrl = await appendGdprConsentUrl(mockUrl, mockMpInstance.Logger);

expect(performCookieSyncSpy).toHaveBeenCalledWith(
expect(fullUrl).toBe(
`${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';

it('should return only the base url if the __tcfapi callback fails to get tcData', async () => {
// 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
);

const fullUrl = await appendGdprConsentUrl(mockUrl, mockMpInstance.Logger);
// Assert that the fallback method was called with the original URL
expect(performCookieSyncSpy).toHaveBeenCalledWith(
mockUrl,
moduleId,
mpid,
mockCookieSyncDates
);
expect(fullUrl).toBe(mockUrl);
});

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

it('should handle exceptions thrown by __tcfapi gracefully', async () => {
// 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();
try {
await appendGdprConsentUrl(mockUrl, mockMpInstance.Logger);
} catch(e) {
expect(errorLoggerSpy).toHaveBeenCalledWith('Test Error');
}
});

// Ensure the error was logged (if applicable)
expect(errorLoggerSpy).toHaveBeenCalledWith('Test Error');
describe.only('#integration test', () => {
it('should handle errors properly when calling attemptCookieSync and the callback fails', () => {
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
(window.__tcfapi as jest.Mock).mockImplementation(() => {
throw new Error('Test Error');
});
const mockMPInstance = ({
_Store: {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings],
},
_Persistence: {
getPersistence: () => ({testMPID: {}}),
},
_Consent: {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
Identity: {
getCurrentUser: jest.fn().mockReturnValue({
getMPID: () => testMPID,
}),
},
Logger: {
verbose: jest.fn(),
error: jest.fn()
},
} as unknown) as MParticleWebSDK;

const cookieSyncManager = new CookieSyncManager(mockMPInstance);
cookieSyncManager.performCookieSync = jest.fn();

cookieSyncManager.attemptCookieSync(testMPID, true);

expect(cookieSyncManager.performCookieSync).toHaveBeenCalledWith(
pixelUrlAndRedirectUrl,
'5',
testMPID,
{},
);
});
});

});
});
2 changes: 1 addition & 1 deletion test/src/tests-cookie-syncing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ describe('cookie syncing', function() {
})
});

it.only('should allow some cookie syncs to occur and others to not occur if there are multiple pixels with varying consent levels', function(done) {
it('should allow some cookie syncs to occur and others to not occur if there are multiple pixels with varying consent levels', function(done) {
// This test has 2 pixelSettings. pixelSettings1 requires consent pixelSettings2 does not. When mparticle initializes, the pixelSettings2 should fire and pixelSettings1 shouldn't.
// After the appropriate consent is saved to the user, pixelSettings1 will fire.

Expand Down
Loading