Skip to content
64 changes: 64 additions & 0 deletions src/cookieConsentManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Cookie consent flags control SDK behavior based on user consent preferences for Rokt integration.
* @see https://docs.rokt.com/developers/integration-guides/web/cookie-consent-flags/
*/
export interface ICookieConsentFlags {
/**
* When true, indicates the user has opted out of functional cookies/tracking.
* Default: false (functional tracking is allowed)
*/
noFunctional: boolean;

/**
* When true, indicates the user has opted out of targeting cookies/tracking.
* This flag is used to block cookie syncs and other targeting-related features.
* Default: false (targeting is allowed)
*/
noTargeting: boolean;
}

export interface ICookieConsentManager {
/**
* Targeting is allowed when noTargeting is false (default)
*/
getNoTargeting: () => boolean;
Comment thread
rmi22186 marked this conversation as resolved.

/**
* Functional tracking is allowed when noFunctional is false (default)
*/
getNoFunctional: () => boolean;
}

/**
* CookieConsentManager handles storage and access of consent flags (noFunctional, noTargeting)
* that are passed via launcherOptions during SDK initialization.
*
* These flags allow Rokt integration to respect user privacy choices.
*
* Default behavior: Both flags default to false, meaning all features are allowed
* unless explicitly opted out by the user.
*/
export default class CookieConsentManager implements ICookieConsentManager {
constructor(private flags: ICookieConsentFlags) {
this.flags = {
noFunctional: flags.noFunctional === true,
noTargeting: flags.noTargeting === true,
};
}

/**
* Returns true if third-party targeting is disabled.
* Targeting is allowed when noTargeting is false (default).
*/
getNoTargeting(): boolean {
return this.flags.noTargeting;
}

/**
* Returns true if functional tracking is disabled.
* Functional tracking is allowed when noFunctional is false (default).
*/
getNoFunctional(): boolean {
return this.flags.noFunctional;
}
}
6 changes: 6 additions & 0 deletions src/cookieSyncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const PARTNER_MODULE_IDS = {
Lotame: 58,
TradeDesk: 103,
VerizonMedia: 155,
Rokt: 1277,
} as const;

export type CookieSyncDates = Dictionary<number>;
Expand Down Expand Up @@ -108,6 +109,11 @@ export default function CookieSyncManager(
return;
}

// For Rokt, block cookie sync when noTargeting privacy flag is true
if (moduleId === PARTNER_MODULE_IDS.Rokt && mpInstance._CookieConsentManager.getNoTargeting()) {
return;
}

const { isEnabledForUserConsent } = mpInstance._Consent;

if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) {
Expand Down
6 changes: 6 additions & 0 deletions src/mp-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { IPersistence } from './persistence.interfaces';
import ForegroundTimer from './foregroundTimeTracker';
import RoktManager, { IRoktOptions } from './roktManager';
import filteredMparticleUser from './filteredMparticleUser';
import CookieConsentManager, { ICookieConsentManager } from './cookieConsentManager';

export interface IErrorLogMessage {
message?: string;
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
_IntegrationCapture: IntegrationCapture;
_NativeSdkHelpers: INativeSdkHelpers;
_Persistence: IPersistence;
_CookieConsentManager: ICookieConsentManager;
_RoktManager: RoktManager;
_SessionManager: ISessionManager;
_ServerModel: IServerModel;
Expand Down Expand Up @@ -1554,6 +1556,10 @@ function runPreConfigFetchInitialization(mpInstance, apiKey, config) {
window.mParticle.Store = mpInstance._Store;
mpInstance.Logger.verbose(StartingInitialization);

// Initialize CookieConsentManager with privacy flags from launcherOptions
const { noFunctional, noTargeting } = config?.launcherOptions ?? {};
mpInstance._CookieConsentManager = new CookieConsentManager({ noFunctional, noTargeting });

// Check to see if localStorage is available before main configuration runs
// since we will need this for the current implementation of user persistence
// TODO: Refactor this when we refactor User Identity Persistence
Expand Down
45 changes: 45 additions & 0 deletions test/jest/cookieConsentManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import CookieConsentManager from '../../src/cookieConsentManager';

describe('CookieConsentManager', () => {
describe('#constructor', () => {
it('should default flags to false when not provided', () => {
const cookieConsentManager = new CookieConsentManager({ noFunctional: undefined, noTargeting: undefined });

expect(cookieConsentManager.getNoFunctional()).toBe(false);
expect(cookieConsentManager.getNoTargeting()).toBe(false);
});

it('should set flags when passed', () => {
const cookieConsentManager = new CookieConsentManager({ noFunctional: true, noTargeting: true });

expect(cookieConsentManager.getNoFunctional()).toBe(true);
expect(cookieConsentManager.getNoTargeting()).toBe(true);
});

it('should ignore non-boolean values', () => {
const cookieConsentManager = new CookieConsentManager({
noFunctional: 'true' as unknown as boolean,
noTargeting: 1 as unknown as boolean,
});

expect(cookieConsentManager.getNoFunctional()).toBe(false);
expect(cookieConsentManager.getNoTargeting()).toBe(false);
});
});

describe('#getNoFunctional', () => {
it('should return the noFunctional flag value', () => {
const cookieConsentManager = new CookieConsentManager({ noFunctional: true, noTargeting: undefined });

expect(cookieConsentManager.getNoFunctional()).toBe(true);
});
});

describe('#getNoTargeting', () => {
it('should return the noTargeting flag value', () => {
const cookieConsentManager = new CookieConsentManager({ noTargeting: true, noFunctional: undefined });

expect(cookieConsentManager.getNoTargeting()).toBe(true);
});
});
});
131 changes: 131 additions & 0 deletions test/jest/cookieSyncManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CookieSyncManager, {
PARTNER_MODULE_IDS
} from '../../src/cookieSyncManager';
import { IMParticleWebSDKInstance } from '../../src/mp-instance';
import CookieConsentManager from '../../src/cookieConsentManager';
import { testMPID } from '../src/config/constants';

const pixelSettings: IPixelConfiguration = {
Expand All @@ -20,6 +21,15 @@ const pixelSettings: IPixelConfiguration = {
redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%',
};

const roktPixelSettings: IPixelConfiguration = {
name: 'Rokt',
moduleId: PARTNER_MODULE_IDS.Rokt,
pixelUrl: 'https://rokt.com/pixel',
redirectUrl: '?redirect=https://redirect.com&mpid=%%mpid%%',
frequencyCap: 14,
settings: {},
} as IPixelConfiguration;

const pixelUrlAndRedirectUrl = 'https://test.com%3Fredirect%3Dhttps%3A%2F%2Fredirect.com%26mpid%3DtestMPID';

describe('CookieSyncManager', () => {
Expand Down Expand Up @@ -384,6 +394,127 @@ describe('CookieSyncManager', () => {
expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled();
});

describe('Rokt noTargeting privacy flag', () => {
it('should block cookie sync when noTargeting is true', () => {
const mockMPInstance = ({
_Store: {
webviewBridgeEnabled: false,
pixelConfigurations: [roktPixelSettings],
},
_Persistence: {
getPersistence: () => ({ testMPID: { csd: {} } }),
},
_Consent: {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoTargeting: 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(mockMPInstance._CookieConsentManager.getNoTargeting).toHaveBeenCalled();
expect(cookieSyncManager.performCookieSync).not.toHaveBeenCalled();
});

it('should allow cookie sync when noTargeting is false', () => {
const mockMPInstance = ({
_Store: {
webviewBridgeEnabled: false,
pixelConfigurations: [roktPixelSettings],
},
_Persistence: {
getPersistence: () => ({ testMPID: { csd: {} } }),
},
_Consent: {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoTargeting: jest.fn().mockReturnValue(false),
},
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(mockMPInstance._CookieConsentManager.getNoTargeting).toHaveBeenCalled();
expect(cookieSyncManager.performCookieSync).toHaveBeenCalled();
});

it('should allow cookie sync when noTargeting is false by default', () => {
const cookieConsentManager = new CookieConsentManager({ noTargeting: undefined, noFunctional: undefined }); // Defaults to noTargeting: false

const mockMPInstance = ({
_Store: {
webviewBridgeEnabled: false,
pixelConfigurations: [roktPixelSettings],
},
_Persistence: {
getPersistence: () => ({ testMPID: { csd: {} } }),
},
_Consent: {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: cookieConsentManager,
Identity: {
getCurrentUser: jest.fn().mockReturnValue({ getMPID: () => testMPID }),
},
} as unknown) as IMParticleWebSDKInstance;

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

cookieSyncManager.attemptCookieSync(testMPID, true);

// Default noTargeting is false, so cookie sync should be allowed
expect(cookieConsentManager.getNoTargeting()).toBe(false);
expect(cookieSyncManager.performCookieSync).toHaveBeenCalled();
});

it('should not check noTargeting for non-Rokt partners', () => {
const mockMPInstance = ({
_Store: {
webviewBridgeEnabled: false,
pixelConfigurations: [pixelSettings], // Uses non-Rokt pixelSettings (moduleId: 5)
},
_Persistence: {
getPersistence: () => ({ testMPID: { csd: {} } }),
},
_Consent: {
isEnabledForUserConsent: jest.fn().mockReturnValue(true),
},
_CookieConsentManager: {
getNoTargeting: 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);

// Should not check noTargeting for non-Rokt partners
expect(mockMPInstance._CookieConsentManager.getNoTargeting).not.toHaveBeenCalled();
// Should still perform cookie sync
expect(cookieSyncManager.performCookieSync).toHaveBeenCalled();
});
});

it('should return early if requiresConsent and mpidIsNotInCookies are both true', () => {
const myPixelSettings: IPixelConfiguration = {
pixelUrl: 'https://test.com',
Expand Down
13 changes: 13 additions & 0 deletions test/jest/roktManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,19 @@ describe('RoktManager', () => {
);
expect(roktManager['mappedEmailShaIdentityType']).toBe('other5');
});

it('should pass through Rokt privacy flags (noTargeting and noFunctional) from launcher options', () => {
Comment thread
jaissica12 marked this conversation as resolved.
roktManager.init(
{} as IKitConfigs,
{} as IMParticleUser,
mockMPInstance.Identity,
mockMPInstance._Store,
mockMPInstance.Logger,
{ launcherOptions: { noTargeting: true, noFunctional: false } },
);
expect(roktManager['launcherOptions'].noTargeting).toBe(true);
expect(roktManager['launcherOptions'].noFunctional).toBe(false);
});
});

describe('#attachKit', () => {
Expand Down
Loading
Loading