Skip to content

Commit

Permalink
Quantcast: Block bids without purpose 1 consent (prebid#5046)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
dpapworth-qc authored and iggyfisk committed Jun 22, 2020
1 parent a544ab9 commit bf999a5
Show file tree
Hide file tree
Showing 2 changed files with 290 additions and 1 deletion.
48 changes: 48 additions & 0 deletions modules/quantcastBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import find from 'core-js/library/fn/array/find.js';
const BIDDER_CODE = 'quantcast';
const DEFAULT_BID_FLOOR = 0.0000000001;

const QUANTCAST_VENDOR_ID = '11';
// Check other required purposes on server
const PURPOSE_DATA_COLLECT = '1';

export const QUANTCAST_DOMAIN = 'qcx.quantserve.com';
export const QUANTCAST_TEST_DOMAIN = 's2s-canary.quantserve.com';
export const QUANTCAST_NET_REVENUE = true;
Expand Down Expand Up @@ -73,6 +77,35 @@ function getDomain(url) {
return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0];
}

function checkTCFv1(vendorData) {
let vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID];
let purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT];

return !!(vendorConsent && purposeConsent);
}

function checkTCFv2(tcData) {
if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') {
// special purpose 1 treatment for Germany
return true;
}

let restrictions = tcData.publisher ? tcData.publisher.restrictions : {};
let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT]
? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID]
: null;

if (qcRestriction === 0 || qcRestriction === 2) {
// Not allowed by publisher, or requires legitimate interest
return false;
}

let vendorConsent = tcData.vendor && tcData.vendor.consents && tcData.vendor.consents[QUANTCAST_VENDOR_ID];
let purposeConsent = tcData.purpose && tcData.purpose.consents && tcData.purpose.consents[PURPOSE_DATA_COLLECT];

return !!(vendorConsent && purposeConsent);
}

/**
* The documentation for Prebid.js Adapter 1.0 can be found at link below,
* http://prebid.org/dev-docs/bidder-adapter-1.html
Expand Down Expand Up @@ -110,6 +143,21 @@ export const spec = {
const page = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href');
const domain = getDomain(page);

// Check for GDPR consent for purpose 1, and drop request if consent has not been given
// Remaining consent checks are performed server-side.
if (gdprConsent.gdprApplies) {
if (gdprConsent.vendorData) {
if (gdprConsent.apiVersion === 1 && !checkTCFv1(gdprConsent.vendorData)) {
utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v1`);
return;
}
if (gdprConsent.apiVersion === 2 && !checkTCFv2(gdprConsent.vendorData)) {
utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`);
return;
}
}
}

let bidRequestsList = [];

bids.forEach(bid => {
Expand Down
243 changes: 242 additions & 1 deletion test/spec/modules/quantcastBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,254 @@ describe('Quantcast adapter', function () {
});

it('propagates GDPR consent string and signal', function () {
const bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentString' } }
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString'
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
const parsed = JSON.parse(requests[0].data);

expect(parsed.gdprSignal).to.equal(1);
expect(parsed.gdprConsent).to.equal('consentString');
});

it('allows TCF v1 request with consent for purpose 1', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendorConsents: {
'11': true
},
purposeConsents: {
'1': true
}
},
apiVersion: 1
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
const parsed = JSON.parse(requests[0].data);

expect(parsed.gdprSignal).to.equal(1);
expect(parsed.gdprConsent).to.equal('consentString');
});

it('blocks TCF v1 request without vendor consent', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendorConsents: {
'11': false
},
purposeConsents: {
'1': true
}
},
apiVersion: 1
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('blocks TCF v1 request without consent for purpose 1', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendorConsents: {
'11': true
},
purposeConsents: {
'1': false
}
},
apiVersion: 1
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('allows TCF v2 request from Germany for purpose 1', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
publisherCC: 'DE',
purposeOneTreatment: true
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
const parsed = JSON.parse(requests[0].data);

expect(parsed.gdprSignal).to.equal(1);
expect(parsed.gdprConsent).to.equal('consentString');
});

it('allows TCF v2 request when Quantcast has consent for purpose 1', function() {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendor: {
consents: {
'11': true
}
},
purpose: {
consents: {
'1': true
}
}
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
const parsed = JSON.parse(requests[0].data);

expect(parsed.gdprSignal).to.equal(1);
expect(parsed.gdprConsent).to.equal('consentString');
});

it('blocks TCF v2 request when no consent for Quantcast', function() {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendor: {
consents: {
'11': false
}
},
purpose: {
consents: {
'1': true
}
}
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('blocks TCF v2 request when no consent for purpose 1', function() {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendor: {
consents: {
'11': true
}
},
purpose: {
consents: {
'1': false
}
}
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('blocks TCF v2 request when Quantcast not allowed by publisher', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendor: {
consents: {
'11': true
}
},
purpose: {
consents: {
'1': true
}
},
publisher: {
restrictions: {
'1': {
'11': 0
}
}
}
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('blocks TCF v2 request when legitimate interest required', function () {
const bidderRequest = {
gdprConsent: {
gdprApplies: true,
consentString: 'consentString',
vendorData: {
vendor: {
consents: {
'11': true
}
},
purpose: {
consents: {
'1': true
}
},
publisher: {
restrictions: {
'1': {
'11': 2
}
}
}
},
apiVersion: 2
}
};

const requests = qcSpec.buildRequests([bidRequest], bidderRequest);

expect(requests).to.equal(undefined);
});

it('propagates US Privacy/CCPA consent information', function () {
const bidderRequest = { uspConsent: 'consentString' }
const requests = qcSpec.buildRequests([bidRequest], bidderRequest);
Expand Down

0 comments on commit bf999a5

Please sign in to comment.