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
181 changes: 180 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,34 @@ 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);
}
window.addEventListener('load', function () {
firePixel(clientId, cookieExpDays);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the firePixel callback within the load event be within the else block of the document.readyState? May help to prevent rare cases where both can fire as readyState is marked 'complete' right before the window 'load' event fires.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like you mentioned, it is pretty rare since they ideally must happen at the same time. Regardless, I added an else block to be safe.


return { id: fpa ? { quantcastId: fpa } : undefined }
}
};
Expand Down