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

QuantcastId : Add support for firing pixel in quantcastIdSystem submodule #7107

Merged
merged 17 commits into from
Jul 14, 2021
182 changes: 181 additions & 1 deletion modules/quantcastIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,159 @@

import {submodule} from '../src/hook.js'
import { getStorageManager } from '../src/storageManager.js';
import { triggerPixel, logInfo } from '../src/utils.js';
import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js';

const QUANTCAST_FPA = '__qca';
const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days)
const DAY_MS = 86400000;
const PREBID_PCODE = 'p-KceJUEvXN48CE';
const QSERVE_URL = 'https://pixel.quantserve.com/pixel';
const QUANTCAST_VENDOR_ID = '11';
const PURPOSE_DATA_COLLECT = '1';
const PURPOSE_PRODUCT_IMPROVEMENT = '10';
const QC_TCF_REQUIRED_PURPOSES = [PURPOSE_DATA_COLLECT, PURPOSE_PRODUCT_IMPROVEMENT];
const QC_TCF_CONSENT_FIRST_PURPOSES = [PURPOSE_DATA_COLLECT];
const QC_TCF_CONSENT_ONLY_PUPROSES = [PURPOSE_DATA_COLLECT];
const GDPR_PRIVACY_STRING = gdprDataHandler.getConsentData();
const US_PRIVACY_STRING = uspDataHandler.getConsentData();

export const storage = getStorageManager();

export function firePixel(clientId, cookieExpDays = DEFAULT_COOKIE_EXP_DAYS) {
// check for presence of Quantcast Measure tag _qevent obj and publisher provided clientID
if (!window._qevents && clientId && clientId != '') {
var fpa = storage.getCookie(QUANTCAST_FPA);
var fpan = '0';
var domain = quantcastIdSubmodule.findRootDomain();
var now = new Date();
var usPrivacyParamString = '';
var firstPartyParamStrings;
var gdprParamStrings;

if (!fpa) {
var et = now.getTime();
var expires = new Date(et + (cookieExpDays * DAY_MS)).toGMTString();
var rand = Math.round(Math.random() * 2147483647);
fpa = `B0-${rand}-${et}`;
fpan = '1';
storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null);
}

firstPartyParamStrings = `&fpan=${fpan}&fpa=${fpa}`;
gdprParamStrings = '&gdpr=0';
if (GDPR_PRIVACY_STRING && typeof GDPR_PRIVACY_STRING.gdprApplies === 'boolean' && GDPR_PRIVACY_STRING.gdprApplies) {
gdprParamStrings = `gdpr=1&gdpr_consent=${GDPR_PRIVACY_STRING.consentString}`;
}
if (US_PRIVACY_STRING && typeof US_PRIVACY_STRING === 'string') {
usPrivacyParamString = `&us_privacy=${US_PRIVACY_STRING}`;
}

let url = QSERVE_URL +
'?d=' + domain +
'&client_id=' + clientId +
'&a=' + PREBID_PCODE +
usPrivacyParamString +
gdprParamStrings +
firstPartyParamStrings;

triggerPixel(url);
}
};

export function hasGDPRConsent(gdprConsent) {
// Check for GDPR consent for purpose 1 and 10, and drop request if consent has not been given
// Remaining consent checks are performed server-side.
if (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) {
if (!gdprConsent.vendorData) {
return false;
}
if (gdprConsent.apiVersion === 1) {
// We are not supporting TCF v1
return false;
}
if (gdprConsent.apiVersion === 2) {
return checkTCFv2(gdprConsent.vendorData);
}
}
return true;
}

export function checkTCFv2(vendorData, requiredPurposes = QC_TCF_REQUIRED_PURPOSES) {
var gdprApplies = vendorData.gdprApplies;
var purposes = vendorData.purpose;
var vendors = vendorData.vendor;
var qcConsent = vendors && vendors.consents && vendors.consents[QUANTCAST_VENDOR_ID];
var qcInterest = vendors && vendors.legitimateInterests && vendors.legitimateInterests[QUANTCAST_VENDOR_ID];
var restrictions = vendorData.publisher ? vendorData.publisher.restrictions : {};

if (!gdprApplies) {
return true;
}

return requiredPurposes.map(function(purpose) {
var purposeConsent = purposes.consents ? purposes.consents[purpose] : false;
var purposeInterest = purposes.legitimateInterests ? purposes.legitimateInterests[purpose] : false;

var qcRestriction = restrictions && restrictions[purpose]
? restrictions[purpose][QUANTCAST_VENDOR_ID]
: null;

if (qcRestriction === 0) {
return false;
}

// Seek consent or legitimate interest based on our default legal
// basis for the purpose, falling back to the other if possible.
if (
// we have positive vendor consent
qcConsent &&
// there is positive purpose consent
purposeConsent &&
// publisher does not require legitimate interest
qcRestriction !== 2 &&
// purpose is a consent-first purpose or publisher has explicitly restricted to consent
(QC_TCF_CONSENT_FIRST_PURPOSES.indexOf(purpose) != -1 || qcRestriction === 1)
) {
return true;
} else if (
// publisher does not require consent
qcRestriction !== 1 &&
// we have legitimate interest for vendor
qcInterest &&
// there is legitimate interest for purpose
purposeInterest &&
// purpose's legal basis does not require consent
QC_TCF_CONSENT_ONLY_PUPROSES.indexOf(purpose) == -1 &&
// purpose is a legitimate-interest-first purpose or publisher has explicitly restricted to legitimate interest
(QC_TCF_CONSENT_FIRST_PURPOSES.indexOf(purpose) == -1 || qcRestriction === 2)
) {
return true;
}

return false;
}).reduce(function(a, b) {
return a && b;
}, true);
}

/**
* tests if us_privacy consent string is present, us_privacy applies, and notice_given / do-not-sell is set to yes
* @returns {boolean}
*/
export function hasCCPAConsent(usPrivacyConsent) {
if (
usPrivacyConsent &&
typeof usPrivacyConsent === 'string' &&
usPrivacyConsent.length == 4 &&
usPrivacyConsent.charAt(1) == 'Y' &&
usPrivacyConsent.charAt(2) == 'Y'
) {
return false
}
return true;
}

/** @type {Submodule} */
export const quantcastIdSubmodule = {
/**
Expand All @@ -20,6 +168,12 @@ export const quantcastIdSubmodule = {
*/
name: 'quantcastId',

/**
* Vendor id of Quantcast
* @type {Number}
*/
gvlid: QUANTCAST_VENDOR_ID,

/**
* decode the stored id value for passing to bid requests
* @function
Expand All @@ -34,9 +188,35 @@ export const quantcastIdSubmodule = {
* @function
* @returns {{id: {quantcastId: string} | undefined}}}
*/
getId() {
getId(config) {
// Consent signals are currently checked on the server side.
let fpa = storage.getCookie(QUANTCAST_FPA);

const coppa = coppaDataHandler.getCoppa();

if (coppa || !hasCCPAConsent(US_PRIVACY_STRING) || !hasGDPRConsent(GDPR_PRIVACY_STRING)) {
var expired = new Date(0).toUTCString();
var domain = quantcastIdSubmodule.findRootDomain();
logInfo('QuantcastId: Necessary consent not present for Id, exiting QuantcastId');
storage.setCookie(QUANTCAST_FPA, '', expired, '/', domain, null);
return undefined;
}

const configParams = (config && config.params) || {};
const storageParams = (config && config.storage) || {};

var clientId = configParams.clientId || '';
var cookieExpDays = storageParams.expires || DEFAULT_COOKIE_EXP_DAYS;

// Callbacks on Event Listeners won't trigger if the event is already complete so this check is required
if (document.readyState === 'complete') {
firePixel(clientId, cookieExpDays);
} else {
window.addEventListener('load', function () {
firePixel(clientId, cookieExpDays);
});
}

return { id: fpa ? { quantcastId: fpa } : undefined }
}
};
Expand Down
46 changes: 46 additions & 0 deletions modules/quantcastIdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#### Overview

```
Module Name: Quantcast Id System
Module Type: Id System
Maintainer: asig@quantcast.com
```

#### Description

The Prebid Quantcast ID module stores a Quantcast ID in a first party cookie. The ID is then made available in the bid request. The ID from the cookie added in the bidstream allows Quantcast to more accurately bid on publisher inventories without third party cookies, which can result in better monetization across publisher sites from Quantcast. And, it’s free to use! For easier integration, you can work with one of our SSP partners, like PubMatic, who can facilitate the legal process as well as the software integration for you.

Add it to your Prebid.js package with:

`gulp build --modules=userId,quantcastIdSystem`

Quantcast’s privacy policies for the services rendered can be found at
https://www.quantcast.com/privacy/

Publishers deploying the module are responsible for ensuring legally required notices and choices for users.

The Quantcast ID module will only perform any action and return an ID in situations where:
1. the publisher has not set a ‘coppa' flag on the prebid configuration on their site (see [pbjs.setConfig.coppa](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-coppa))
2. there is not a IAB us-privacy string indicating the digital property has provided user notice and the user has made a choice to opt out of sale
3. if GDPR applies, an IAB TCF v2 string exists indicating that Quantcast does not have consent for purpose 1 (cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you), or an established legal basis (by default legitimate interest) for purpose 10 (your data can be used to improve existing systems and software, and to develop new products).

#### Quantcast ID Configuration

| Param under userSync.userIds[] | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| name | Required | String | `"quantcastId"` | `"quantcastId"` |
| params | Optional | Object | Details for Quantcast initialization. | |
| params.ClientID | Optional | String | Optional parameter for Quantcast prebid managed service partners. The parameter is not required for websites with Quantcast Measure tag. Reach out to Quantcast for ClientID if you are not an existing Quantcast prebid managed service partner: quantcast-idsupport@quantcast.com. | |


#### Quantcast ID Example

```js
pbjs.setConfig({
userSync: {
userIds: [{
name: "quantcastId"
}]
}
});
```
6 changes: 6 additions & 0 deletions modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ pbjs.setConfig({
params: {
token: "Registered token or default sharedid.org token" // Default sharedid.org token: "A3dHTSoNUMjjERBLlrvJSelNnwWUCwVQhZ5tNQ+sll7y+LkPPVZXtB77u2y7CweRIxiYaGwGXNlW1/dFp8VMEgIAAAB+eyJvcmlnaW4iOiJodHRwczovL3NoYXJlZGlkLm9yZzo0NDMiLCJmZWF0dXJlIjoiSW50ZXJlc3RDb2hvcnRBUEkiLCJleHBpcnkiOjE2MjYyMjA3OTksImlzU3ViZG9tYWluIjp0cnVlLCJpc1RoaXJkUGFydHkiOnRydWV9"
}
},{
name: 'quantcastId',
storage: {
type: 'cookie',
expires : 30
}
}],
syncDelay: 5000,
auctionDelay: 1000
Expand Down