Skip to content

Commit 8f9712c

Browse files
authored
GDPR (consentManagement): fix actionTimeout behavior (#9600)
* GDPR (consentManagement): fix `actionTimeout` behavior * Add test case for actionTimeout = 0
1 parent 74a1ffc commit 8f9712c

File tree

2 files changed

+78
-84
lines changed

2 files changed

+78
-84
lines changed

modules/consentManagement.js

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,12 @@ const CMP_VERSION = 2;
1818

1919
export let userCMP;
2020
export let consentTimeout;
21-
export let actionTimeout;
2221
export let gdprScope;
2322
export let staticConsentData;
23+
let actionTimeout;
2424

2525
let consentData;
2626
let addedConsentHook = false;
27-
let provisionalConsent;
28-
let onTimeout;
29-
let timer = null;
30-
let actionTimer = null;
3127

3228
// add new CMPs here, with their dedicated lookup function
3329
const cmpCallMap = {
@@ -43,20 +39,14 @@ function lookupStaticConsentData({onSuccess, onError}) {
4339
processCmpData(staticConsentData, {onSuccess, onError})
4440
}
4541

46-
export function setActionTimeout(timeout = setTimeout) {
47-
clearTimeout(timer);
48-
timer = null;
49-
actionTimer = timeout(onTimeout, actionTimeout);
50-
}
51-
5242
/**
5343
* This function handles interacting with an IAB compliant CMP to obtain the consent information of the user.
5444
* Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
5545
* based on the appropriate result.
5646
* @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP
5747
* @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging)
5848
*/
59-
function lookupIabConsent({onSuccess, onError}) {
49+
function lookupIabConsent({onSuccess, onError, onEvent}) {
6050
function findCMP() {
6151
let f = window;
6252
let cmpFrame;
@@ -90,11 +80,9 @@ function lookupIabConsent({onSuccess, onError}) {
9080
function cmpResponseCallback(tcfData, success) {
9181
logInfo('Received a response from CMP', tcfData);
9282
if (success) {
83+
onEvent(tcfData);
9384
if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') {
9485
processCmpData(tcfData, {onSuccess, onError});
95-
} else {
96-
provisionalConsent = tcfData;
97-
if (!isNaN(actionTimeout) && actionTimer === null && timer != null) setActionTimeout();
9886
}
9987
} else {
10088
onError('CMP unable to register callback function. Please check CMP setup.');
@@ -173,20 +161,27 @@ function lookupIabConsent({onSuccess, onError}) {
173161
* @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra
174162
* error arguments that will be undefined if there's no error.
175163
*/
176-
export function loadConsentData(cb, callMap = cmpCallMap, timeout = setTimeout) {
164+
function loadConsentData(cb) {
177165
let isDone = false;
166+
let timer = null;
167+
let onTimeout, provisionalConsent;
168+
let cmpLoaded = false;
178169

179-
function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) {
170+
function resetTimeout(timeout) {
180171
if (timer != null) {
181172
clearTimeout(timer);
182-
timer = null;
183173
}
184-
185-
if (actionTimer != null) {
186-
clearTimeout(actionTimer);
187-
actionTimer = null;
174+
if (!isDone && timeout != null) {
175+
if (timeout === 0) {
176+
onTimeout()
177+
} else {
178+
timer = setTimeout(onTimeout, timeout);
179+
}
188180
}
181+
}
189182

183+
function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) {
184+
resetTimeout(null);
190185
isDone = true;
191186
gdprDataHandler.setConsentData(consentData);
192187
if (typeof cb === 'function') {
@@ -203,32 +198,30 @@ export function loadConsentData(cb, callMap = cmpCallMap, timeout = setTimeout)
203198
onSuccess: (data) => done(data, false),
204199
onError: function (msg, ...extraArgs) {
205200
done(null, true, msg, ...extraArgs);
201+
},
202+
onEvent: function (consentData) {
203+
provisionalConsent = consentData;
204+
if (cmpLoaded) return;
205+
cmpLoaded = true;
206+
if (actionTimeout != null) {
207+
resetTimeout(actionTimeout);
208+
}
206209
}
207210
}
208211

209-
callMap[userCMP](callbacks);
210-
211-
if (!isDone) {
212-
onTimeout = () => {
213-
const continueToAuction = (data) => {
214-
done(data, false, 'CMP did not load, continuing auction...');
215-
}
216-
processCmpData(provisionalConsent, {
217-
onSuccess: continueToAuction,
218-
onError: () => continueToAuction(storeConsentData(undefined))
219-
})
212+
onTimeout = () => {
213+
const continueToAuction = (data) => {
214+
done(data, false, `${cmpLoaded ? 'Timeout waiting for user action on CMP' : 'CMP did not load'}, continuing auction...`);
220215
}
216+
processCmpData(provisionalConsent, {
217+
onSuccess: continueToAuction,
218+
onError: () => continueToAuction(storeConsentData(undefined)),
219+
})
220+
}
221221

222-
if (consentTimeout === 0) {
223-
onTimeout();
224-
} else {
225-
if (timer != null) {
226-
clearTimeout(timer);
227-
timer = null;
228-
}
229-
230-
timer = timeout(onTimeout, consentTimeout);
231-
}
222+
cmpCallMap[userCMP](callbacks);
223+
if (!(actionTimeout != null && cmpLoaded)) {
224+
resetTimeout(consentTimeout);
232225
}
233226
}
234227

@@ -352,17 +345,15 @@ export function setConsentConfig(config) {
352345
logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`);
353346
}
354347

355-
if (isNumber(config.actionTimeout)) {
356-
actionTimeout = config.actionTimeout;
357-
}
358-
359348
if (isNumber(config.timeout)) {
360349
consentTimeout = config.timeout;
361350
} else {
362351
consentTimeout = DEFAULT_CONSENT_TIMEOUT;
363352
logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`);
364353
}
365354

355+
actionTimeout = isNumber(config.actionTimeout) ? config.actionTimeout : null;
356+
366357
// if true, then gdprApplies should be set to true
367358
gdprScope = config.defaultGdprScope === true;
368359

test/spec/modules/consentManagement_spec.js

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,46 @@ describe('consentManagement', function () {
685685
});
686686
});
687687

688+
it('should timeout after actionTimeout from the first CMP event', (done) => {
689+
mockTcfEvent({
690+
eventStatus: 'cmpuishown',
691+
tcString: 'mock-consent-string',
692+
vendorData: {}
693+
});
694+
setConsentConfig({
695+
timeout: 1000,
696+
actionTimeout: 100,
697+
cmpApi: 'iab',
698+
defaultGdprScope: true
699+
});
700+
let hookRan = false;
701+
requestBidsHook(() => {
702+
hookRan = true;
703+
}, {});
704+
setTimeout(() => {
705+
expect(hookRan).to.be.true;
706+
done();
707+
}, 200)
708+
});
709+
710+
it('should still pick up consent data when actionTimeout is 0', (done) => {
711+
mockTcfEvent({
712+
eventStatus: 'tcloaded',
713+
tcString: 'mock-consent-string',
714+
vendorData: {}
715+
});
716+
setConsentConfig({
717+
timeout: 1000,
718+
actionTimeout: 0,
719+
cmpApi: 'iab',
720+
defaultGdprScope: true
721+
});
722+
requestBidsHook(() => {
723+
expect(gdprDataHandler.getConsentData().consentString).to.eql('mock-consent-string');
724+
done();
725+
}, {})
726+
})
727+
688728
Object.entries({
689729
'null': null,
690730
'empty': '',
@@ -737,41 +777,4 @@ describe('consentManagement', function () {
737777
});
738778
});
739779
});
740-
741-
describe('actionTimeout', function () {
742-
afterEach(function () {
743-
config.resetConfig();
744-
resetConsentData();
745-
});
746-
747-
it('should set actionTimeout if present', () => {
748-
setConsentConfig({
749-
gdpr: { timeout: 5000, actionTimeout: 5500 }
750-
});
751-
752-
expect(userCMP).to.be.equal('iab');
753-
expect(consentTimeout).to.be.equal(5000);
754-
expect(actionTimeout).to.be.equal(5500);
755-
});
756-
757-
it('should utilize actionTimeout duration on initial user visit when user action is pending', () => {
758-
const cb = () => {};
759-
const cmpCallMap = {
760-
'iab': () => {},
761-
'static': () => {}
762-
};
763-
const timeout = sinon.spy();
764-
765-
setConsentConfig({
766-
gdpr: { timeout: 5000, actionTimeout: 5500 }
767-
});
768-
loadConsentData(cb, cmpCallMap, timeout);
769-
770-
sinon.assert.calledWith(timeout, sinon.match.any, 5000);
771-
772-
setActionTimeout();
773-
774-
timeout.lastCall.lastArg === 5500;
775-
});
776-
});
777780
});

0 commit comments

Comments
 (0)