Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quantcast: Block bids without purpose 1 consent #5046

Merged
merged 8 commits into from
Apr 14, 2020
48 changes: 48 additions & 0 deletions modules/quantcastBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.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 @@ -72,6 +76,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 @@ -107,6 +140,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