Skip to content

Commit c7cc349

Browse files
authored
Quantcast: Block bids without purpose 1 consent (#5046)
* Add TCF version to bid request. * Block requests where GDPR consent has not been given. * Removed GDPR apiVersion from bid request. * Added purpose consent check. * Added separate functions for TCF v1 and TCF v2 consent checks. * Check TCF v2 consent before sending bid. * Make TCF v1 consent check strict. * Improved comments and tests.
1 parent bddf7e9 commit c7cc349

File tree

2 files changed

+290
-1
lines changed

2 files changed

+290
-1
lines changed

modules/quantcastBidAdapter.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import find from 'core-js/library/fn/array/find.js';
77
const BIDDER_CODE = 'quantcast';
88
const DEFAULT_BID_FLOOR = 0.0000000001;
99

10+
const QUANTCAST_VENDOR_ID = '11';
11+
// Check other required purposes on server
12+
const PURPOSE_DATA_COLLECT = '1';
13+
1014
export const QUANTCAST_DOMAIN = 'qcx.quantserve.com';
1115
export const QUANTCAST_TEST_DOMAIN = 's2s-canary.quantserve.com';
1216
export const QUANTCAST_NET_REVENUE = true;
@@ -73,6 +77,35 @@ function getDomain(url) {
7377
return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0];
7478
}
7579

80+
function checkTCFv1(vendorData) {
81+
let vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID];
82+
let purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT];
83+
84+
return !!(vendorConsent && purposeConsent);
85+
}
86+
87+
function checkTCFv2(tcData) {
88+
if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') {
89+
// special purpose 1 treatment for Germany
90+
return true;
91+
}
92+
93+
let restrictions = tcData.publisher ? tcData.publisher.restrictions : {};
94+
let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT]
95+
? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID]
96+
: null;
97+
98+
if (qcRestriction === 0 || qcRestriction === 2) {
99+
// Not allowed by publisher, or requires legitimate interest
100+
return false;
101+
}
102+
103+
let vendorConsent = tcData.vendor && tcData.vendor.consents && tcData.vendor.consents[QUANTCAST_VENDOR_ID];
104+
let purposeConsent = tcData.purpose && tcData.purpose.consents && tcData.purpose.consents[PURPOSE_DATA_COLLECT];
105+
106+
return !!(vendorConsent && purposeConsent);
107+
}
108+
76109
/**
77110
* The documentation for Prebid.js Adapter 1.0 can be found at link below,
78111
* http://prebid.org/dev-docs/bidder-adapter-1.html
@@ -110,6 +143,21 @@ export const spec = {
110143
const page = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href');
111144
const domain = getDomain(page);
112145

146+
// Check for GDPR consent for purpose 1, and drop request if consent has not been given
147+
// Remaining consent checks are performed server-side.
148+
if (gdprConsent.gdprApplies) {
149+
if (gdprConsent.vendorData) {
150+
if (gdprConsent.apiVersion === 1 && !checkTCFv1(gdprConsent.vendorData)) {
151+
utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v1`);
152+
return;
153+
}
154+
if (gdprConsent.apiVersion === 2 && !checkTCFv2(gdprConsent.vendorData)) {
155+
utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`);
156+
return;
157+
}
158+
}
159+
}
160+
113161
let bidRequestsList = [];
114162

115163
bids.forEach(bid => {

test/spec/modules/quantcastBidAdapter_spec.js

Lines changed: 242 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,13 +348,254 @@ describe('Quantcast adapter', function () {
348348
});
349349

350350
it('propagates GDPR consent string and signal', function () {
351-
const bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentString' } }
351+
const bidderRequest = {
352+
gdprConsent: {
353+
gdprApplies: true,
354+
consentString: 'consentString'
355+
}
356+
};
357+
358+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
359+
const parsed = JSON.parse(requests[0].data);
360+
361+
expect(parsed.gdprSignal).to.equal(1);
362+
expect(parsed.gdprConsent).to.equal('consentString');
363+
});
364+
365+
it('allows TCF v1 request with consent for purpose 1', function () {
366+
const bidderRequest = {
367+
gdprConsent: {
368+
gdprApplies: true,
369+
consentString: 'consentString',
370+
vendorData: {
371+
vendorConsents: {
372+
'11': true
373+
},
374+
purposeConsents: {
375+
'1': true
376+
}
377+
},
378+
apiVersion: 1
379+
}
380+
};
381+
382+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
383+
const parsed = JSON.parse(requests[0].data);
384+
385+
expect(parsed.gdprSignal).to.equal(1);
386+
expect(parsed.gdprConsent).to.equal('consentString');
387+
});
388+
389+
it('blocks TCF v1 request without vendor consent', function () {
390+
const bidderRequest = {
391+
gdprConsent: {
392+
gdprApplies: true,
393+
consentString: 'consentString',
394+
vendorData: {
395+
vendorConsents: {
396+
'11': false
397+
},
398+
purposeConsents: {
399+
'1': true
400+
}
401+
},
402+
apiVersion: 1
403+
}
404+
};
405+
406+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
407+
408+
expect(requests).to.equal(undefined);
409+
});
410+
411+
it('blocks TCF v1 request without consent for purpose 1', function () {
412+
const bidderRequest = {
413+
gdprConsent: {
414+
gdprApplies: true,
415+
consentString: 'consentString',
416+
vendorData: {
417+
vendorConsents: {
418+
'11': true
419+
},
420+
purposeConsents: {
421+
'1': false
422+
}
423+
},
424+
apiVersion: 1
425+
}
426+
};
427+
428+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
429+
430+
expect(requests).to.equal(undefined);
431+
});
432+
433+
it('allows TCF v2 request from Germany for purpose 1', function () {
434+
const bidderRequest = {
435+
gdprConsent: {
436+
gdprApplies: true,
437+
consentString: 'consentString',
438+
vendorData: {
439+
publisherCC: 'DE',
440+
purposeOneTreatment: true
441+
},
442+
apiVersion: 2
443+
}
444+
};
445+
352446
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
353447
const parsed = JSON.parse(requests[0].data);
448+
354449
expect(parsed.gdprSignal).to.equal(1);
355450
expect(parsed.gdprConsent).to.equal('consentString');
356451
});
357452

453+
it('allows TCF v2 request when Quantcast has consent for purpose 1', function() {
454+
const bidderRequest = {
455+
gdprConsent: {
456+
gdprApplies: true,
457+
consentString: 'consentString',
458+
vendorData: {
459+
vendor: {
460+
consents: {
461+
'11': true
462+
}
463+
},
464+
purpose: {
465+
consents: {
466+
'1': true
467+
}
468+
}
469+
},
470+
apiVersion: 2
471+
}
472+
};
473+
474+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
475+
const parsed = JSON.parse(requests[0].data);
476+
477+
expect(parsed.gdprSignal).to.equal(1);
478+
expect(parsed.gdprConsent).to.equal('consentString');
479+
});
480+
481+
it('blocks TCF v2 request when no consent for Quantcast', function() {
482+
const bidderRequest = {
483+
gdprConsent: {
484+
gdprApplies: true,
485+
consentString: 'consentString',
486+
vendorData: {
487+
vendor: {
488+
consents: {
489+
'11': false
490+
}
491+
},
492+
purpose: {
493+
consents: {
494+
'1': true
495+
}
496+
}
497+
},
498+
apiVersion: 2
499+
}
500+
};
501+
502+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
503+
504+
expect(requests).to.equal(undefined);
505+
});
506+
507+
it('blocks TCF v2 request when no consent for purpose 1', function() {
508+
const bidderRequest = {
509+
gdprConsent: {
510+
gdprApplies: true,
511+
consentString: 'consentString',
512+
vendorData: {
513+
vendor: {
514+
consents: {
515+
'11': true
516+
}
517+
},
518+
purpose: {
519+
consents: {
520+
'1': false
521+
}
522+
}
523+
},
524+
apiVersion: 2
525+
}
526+
};
527+
528+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
529+
530+
expect(requests).to.equal(undefined);
531+
});
532+
533+
it('blocks TCF v2 request when Quantcast not allowed by publisher', function () {
534+
const bidderRequest = {
535+
gdprConsent: {
536+
gdprApplies: true,
537+
consentString: 'consentString',
538+
vendorData: {
539+
vendor: {
540+
consents: {
541+
'11': true
542+
}
543+
},
544+
purpose: {
545+
consents: {
546+
'1': true
547+
}
548+
},
549+
publisher: {
550+
restrictions: {
551+
'1': {
552+
'11': 0
553+
}
554+
}
555+
}
556+
},
557+
apiVersion: 2
558+
}
559+
};
560+
561+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
562+
563+
expect(requests).to.equal(undefined);
564+
});
565+
566+
it('blocks TCF v2 request when legitimate interest required', function () {
567+
const bidderRequest = {
568+
gdprConsent: {
569+
gdprApplies: true,
570+
consentString: 'consentString',
571+
vendorData: {
572+
vendor: {
573+
consents: {
574+
'11': true
575+
}
576+
},
577+
purpose: {
578+
consents: {
579+
'1': true
580+
}
581+
},
582+
publisher: {
583+
restrictions: {
584+
'1': {
585+
'11': 2
586+
}
587+
}
588+
}
589+
},
590+
apiVersion: 2
591+
}
592+
};
593+
594+
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
595+
596+
expect(requests).to.equal(undefined);
597+
});
598+
358599
it('propagates US Privacy/CCPA consent information', function () {
359600
const bidderRequest = { uspConsent: 'consentString' }
360601
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

0 commit comments

Comments
 (0)