Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4584dbb
Prebid Core: Cosnent Handler reset functionality
pm-nitin-shirsat Aug 4, 2025
5411509
Prebid Consent Management: Add reset
pm-nitin-shirsat Aug 5, 2025
5f95c4b
Consent Management Reset: Remove add event listener if it is listenin…
pm-nitin-shirsat Aug 5, 2025
99d6a23
Merge pull request #1134 from prebid/master
pm-nitin-shirsat Aug 8, 2025
2cf4ef5
Consent management changes working
pm-nitin-shirsat Aug 12, 2025
1e23353
Consent Management: add enabled flag before enabling the module. Prov…
pm-nitin-shirsat Aug 13, 2025
0b8ffed
Consent Management: logInfo to logWarn
pm-nitin-shirsat Aug 13, 2025
37e2b3b
Consent Manegement reset fix
pm-nitin-shirsat Aug 21, 2025
e1503e1
Merge pull request #1140 from PubMatic-OpenWrap/prebid/consent-manage…
pm-nitin-shirsat Aug 21, 2025
3c1cbd7
Merge pull request #1141 from PubMatic-OpenWrap/UOE-12892
pm-nitin-shirsat Aug 21, 2025
7b0cfaf
merge remote-tracking branch 'upstream/master' into prebid/consent-ma…
pm-nitin-shirsat Oct 8, 2025
9e654e5
Add gdpr test cases
pm-nitin-shirsat Oct 8, 2025
94fedc0
Merge branch 'master' into prebid/consent-management-reset
pm-nitin-shirsat Oct 9, 2025
ae6630d
Merge branch 'master' into prebid/consent-management-reset
patmmccann Oct 9, 2025
c49ecb8
Move the cmp event listener removal functions to libraries
pm-nitin-shirsat Oct 14, 2025
359297a
Merge remote-tracking branch 'upstream/master' into prebid/consent-ma…
pm-nitin-shirsat Oct 14, 2025
2cde8a2
Merge branch 'master' into prebid/consent-management-reset
pm-nitin-shirsat Oct 15, 2025
e4a7017
Merge branch 'master' into prebid/consent-management-reset
dgirardi Oct 21, 2025
a847827
Merge branch 'master' into prebid/consent-management-reset
patmmccann Oct 22, 2025
5287ff0
Merge branch 'master' into prebid/consent-management-reset
dgirardi Oct 23, 2025
bf71a6e
Merge remote-tracking branch 'upstream/master' into prebid/consent-ma…
pm-nitin-shirsat Oct 27, 2025
a5430ef
Remove stray comment
pm-nitin-shirsat Oct 27, 2025
79d66db
Fix test cases issues
pm-nitin-shirsat Oct 27, 2025
9aa1ae5
Merge branch 'master' into prebid/consent-management-reset
pm-nitin-shirsat Oct 28, 2025
ec3790f
Merge branch 'master' into prebid/consent-management-reset
pm-nitin-shirsat Oct 30, 2025
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
121 changes: 121 additions & 0 deletions libraries/cmp/cmpEventUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Shared utilities for CMP event listener management
* Used by TCF and GPP consent management modules
*/

import { logError, logInfo } from "../../src/utils.js";

export interface CmpEventManager {
cmpApi: any;
listenerId: number | undefined;
setCmpApi(cmpApi: any): void;
getCmpApi(): any;
setCmpListenerId(listenerId: number | undefined): void;
getCmpListenerId(): number | undefined;
removeCmpEventListener(): void;
resetCmpApis(): void;
}

/**
* Base CMP event manager implementation
*/
export abstract class BaseCmpEventManager implements CmpEventManager {
cmpApi: any = null;
listenerId: number | undefined = undefined;

setCmpApi(cmpApi: any): void {
this.cmpApi = cmpApi;
}

getCmpApi(): any {
return this.cmpApi;
}

setCmpListenerId(listenerId: number | undefined): void {
this.listenerId = listenerId;
}

getCmpListenerId(): number | undefined {
return this.listenerId;
}

resetCmpApis(): void {
this.cmpApi = null;
this.listenerId = undefined;
}

/**
* Helper method to get base removal parameters
* Can be used by subclasses that need to remove event listeners
*/
protected getRemoveListenerParams(): Record<string, any> | null {
const cmpApi = this.getCmpApi();
const listenerId = this.getCmpListenerId();

// Comprehensive validation for all possible failure scenarios
if (cmpApi && typeof cmpApi === 'function' && listenerId !== undefined && listenerId !== null) {
return {
command: "removeEventListener",
callback: () => this.resetCmpApis(),
parameter: listenerId
};
}
return null;
}

/**
* Abstract method - each subclass implements its own removal logic
*/
abstract removeCmpEventListener(): void;
}

/**
* TCF-specific CMP event manager
*/
export class TcfCmpEventManager extends BaseCmpEventManager {
private getConsentData: () => any;

constructor(getConsentData?: () => any) {
super();
this.getConsentData = getConsentData || (() => null);
}

removeCmpEventListener(): void {
const params = this.getRemoveListenerParams();
if (params) {
const consentData = this.getConsentData();
params.apiVersion = consentData?.apiVersion || 2;
logInfo('Removing TCF CMP event listener');
this.getCmpApi()(params);
}
}
}

/**
* GPP-specific CMP event manager
* GPP doesn't require event listener removal, so this is empty
*/
export class GppCmpEventManager extends BaseCmpEventManager {
removeCmpEventListener(): void {
const params = this.getRemoveListenerParams();
if (params) {
logInfo('Removing GPP CMP event listener');
this.getCmpApi()(params);
}
}
}

/**
* Factory function to create appropriate CMP event manager
*/
export function createCmpEventManager(type: 'tcf' | 'gpp', getConsentData?: () => any): CmpEventManager {
switch (type) {
case 'tcf':
return new TcfCmpEventManager(getConsentData);
case 'gpp':
return new GppCmpEventManager();
default:
logError(`Unknown CMP type: ${type}`);
return null;
}
}
28 changes: 28 additions & 0 deletions libraries/consentManagement/cmUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export interface BaseCMConfig {
* for the user to interact with the CMP.
*/
actionTimeout?: number;
/**
* Flag to enable or disable the consent management module.
* When set to false, the module will be reset and disabled.
* Defaults to true when not specified.
*/
enabled?: boolean;
}

export interface IABCMConfig {
Expand All @@ -136,6 +142,7 @@ export function configParser(
parseConsentData,
getNullConsent,
cmpHandlers,
cmpEventCleanup,
DEFAULT_CMP = 'iab',
DEFAULT_CONSENT_TIMEOUT = 10000
} = {} as any
Expand Down Expand Up @@ -167,6 +174,19 @@ export function configParser(
getHook('requestBids').getHooks({hook: requestBidsHook}).remove();
buildActivityParams.getHooks({hook: attachActivityParams}).remove();
requestBidsHook = null;
logInfo(`${displayName} consentManagement module has been deactivated...`);
}
}

function resetConsentDataHandler() {
reset();
// Call module-specific CMP event cleanup if provided
if (typeof cmpEventCleanup === 'function') {
try {
cmpEventCleanup();
} catch (e) {
logError(`Error during CMP event cleanup for ${displayName}:`, e);
}
}
}

Expand All @@ -177,6 +197,14 @@ export function configParser(
reset();
return {};
}

// Check if module is explicitly disabled
if (cmConfig?.enabled === false) {
logWarn(msg(`config enabled is set to false, disabling consent manager module`));
resetConsentDataHandler();
return {};
}

let cmpHandler;
if (isStr(cmConfig.cmpApi)) {
cmpHandler = cmConfig.cmpApi;
Expand Down
27 changes: 26 additions & 1 deletion modules/consentManagementGpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import {enrichFPD} from '../src/fpd/enrichment.js';
import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js';
import {PbPromise, defer} from '../src/utils/promise.js';
import {type CMConfig, configParser} from '../libraries/consentManagement/cmUtils.js';
import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js';
import {CONSENT_GPP} from "../src/consentHandler.ts";

export let consentConfig = {} as any;

// CMP event manager instance for GPP
let gppCmpEventManager: CmpEventManager | null = null;

type RelevantCMPData = {
applicableSections: number[]
gppString: string;
Expand Down Expand Up @@ -101,6 +105,13 @@ export class GPPClient {
logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`);
}
this.initialized = true;

// Initialize CMP event manager and set CMP API
if (!gppCmpEventManager) {
gppCmpEventManager = createCmpEventManager('gpp');
}
gppCmpEventManager.setCmpApi(this.cmp);

this.cmp({
command: 'addEventListener',
callback: (event, success) => {
Expand All @@ -120,6 +131,10 @@ export class GPPClient {
if (gppDataHandler.getConsentData() != null && event?.pingData != null && !this.isCMPReady(event.pingData)) {
gppDataHandler.setConsentData(null);
}

if (event?.listenerId !== null && event?.listenerId !== undefined) {
gppCmpEventManager?.setCmpListenerId(event?.listenerId);
}
}
});
}
Expand Down Expand Up @@ -218,13 +233,23 @@ export function resetConsentData() {
GPPClient.INST = null;
}

export function removeCmpListener() {
// Clean up CMP event listeners before resetting
if (gppCmpEventManager) {
gppCmpEventManager.removeCmpEventListener();
gppCmpEventManager = null;
}
resetConsentData();
}

const parseConfig = configParser({
namespace: 'gpp',
displayName: 'GPP',
consentDataHandler: gppDataHandler,
parseConsentData,
getNullConsent: () => toConsentData(null),
cmpHandlers: cmpCallMap
cmpHandlers: cmpCallMap,
cmpEventCleanup: removeCmpListener
});

export function setConsentConfig(config) {
Expand Down
26 changes: 25 additions & 1 deletion modules/consentManagementTcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js';
import {enrichFPD} from '../src/fpd/enrichment.js';
import {cmpClient} from '../libraries/cmp/cmpClient.js';
import {configParser} from '../libraries/consentManagement/cmUtils.js';
import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js';
import {CONSENT_GDPR} from "../src/consentHandler.ts";
import type {CMConfig} from "../libraries/consentManagement/cmUtils.ts";

Expand All @@ -24,6 +25,9 @@ const cmpCallMap = {
'iab': lookupIabConsent,
};

// CMP event manager instance for TCF
export let tcfCmpEventManager: CmpEventManager | null = null;

/**
* @see https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework
* @see https://github.com/InteractiveAdvertisingBureau/iabtcf-es/tree/master/modules/core#iabtcfcore
Expand Down Expand Up @@ -87,6 +91,9 @@ function lookupIabConsent(setProvisionalConsent) {

if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') {
try {
if (tcfData.listenerId !== null && tcfData.listenerId !== undefined) {
tcfCmpEventManager?.setCmpListenerId(tcfData.listenerId);
}
gdprDataHandler.setConsentData(parseConsentData(tcfData));
resolve();
} catch (e) {
Expand All @@ -113,6 +120,12 @@ function lookupIabConsent(setProvisionalConsent) {
logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...');
}

// Initialize CMP event manager and set CMP API
if (!tcfCmpEventManager) {
tcfCmpEventManager = createCmpEventManager('tcf', () => gdprDataHandler.getConsentData());
}
tcfCmpEventManager.setCmpApi(cmp);

cmp({
command: 'addEventListener',
callback: cmpResponseCallback
Expand Down Expand Up @@ -159,14 +172,25 @@ export function resetConsentData() {
gdprDataHandler.reset();
}

export function removeCmpListener() {
// Clean up CMP event listeners before resetting
if (tcfCmpEventManager) {
tcfCmpEventManager.removeCmpEventListener();
tcfCmpEventManager = null;
}
resetConsentData();
}

const parseConfig = configParser({
namespace: 'gdpr',
displayName: 'TCF',
consentDataHandler: gdprDataHandler,
cmpHandlers: cmpCallMap,
parseConsentData,
getNullConsent: () => toConsentData(null)
getNullConsent: () => toConsentData(null),
cmpEventCleanup: removeCmpListener
} as any)

/**
* A configuration function that initializes some module variables, as well as add a hook into the requestBids function
*/
Expand Down
21 changes: 15 additions & 6 deletions src/consentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,19 @@ export class ConsentHandler<T> {
}

getConsentData(): T {
return this.#data;
if (this.#enabled) {
return this.#data;
}
return null;
}

get hash() {
if (this.#dirty) {
this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data))
this.#hash = cyrb53Hash(
JSON.stringify(
this.#data && this.hashFields ? this.hashFields.map((f) => this.#data[f]) : this.#data
)
);
this.#dirty = false;
}
return this.#hash;
Expand All @@ -132,16 +139,18 @@ class UspConsentHandler extends ConsentHandler<ConsentDataFor<typeof CONSENT_USP
}

class GdprConsentHandler extends ConsentHandler<ConsentDataFor<typeof CONSENT_GDPR>> {
hashFields = ['gdprApplies', 'consentString']
hashFields = ["gdprApplies", "consentString"];
getConsentMeta() {
const consentData = this.getConsentData();
if (consentData && consentData.vendorData && this.generatedTime) {
return {
gdprApplies: consentData.gdprApplies as boolean,
consentStringSize: (isStr(consentData.vendorData.tcString)) ? consentData.vendorData.tcString.length : 0,
consentStringSize: isStr(consentData.vendorData.tcString)
? consentData.vendorData.tcString.length
: 0,
generatedAt: this.generatedTime,
apiVersion: consentData.apiVersion
}
apiVersion: consentData.apiVersion,
};
}
}
}
Expand Down
Loading