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

Fire pixel without measure #3

Merged
merged 10 commits into from
Jun 26, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 165 additions & 1 deletion modules/quantcastIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,156 @@

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_TIME = 392; // (13 months - 2 days)
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
const PREBID_PCODE = 'p-KceJUEvXN48CE'; // Not associated with a real account
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
const DOMAIN_QSERVE = 'https://pixel.quantserve.com/pixel';
Copy link

Choose a reason for hiding this comment

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

QSERVE_URL ? This isn't really a domain.

Copy link

Choose a reason for hiding this comment

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

Question: Do we want to use a protocol relative URL instead? I mean maybe not these days, but it is common practice.
Note: If we did, it would be QSERVE_URL = "//pixel.quantserve.com/pixel";

Copy link
Owner Author

@sachinsurfs sachinsurfs Jun 23, 2021

Choose a reason for hiding this comment

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

Hmm, the advantage of this is that the URL would have the same protocol as the site it is on (versus always being https), right? I think that makes sense.

Copy link

@mhammond-quantcast mhammond-quantcast Jun 23, 2021

Choose a reason for hiding this comment

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

i think we need to use secure these days for certain cookie based stuff to work in Chrome? (might be mistaken about that). I guess that's less of a concern here, although i suppose these requests will set 3rd party cookies where they can.

My vote would just be to keep this secure i think. Does that ever cause issues if done on a non-secure site?

We should use QSERVE_URL as Scott suggests though.

const QUANTCAST_VENDOR_ID = '11';
const PURPOSE_DATA_COLLECT = '1';
const PURPOSE_PRODUCT_IMPROVEMENT = '10';

var clientId;
var cookieExpTime;

export const storage = getStorageManager();

export function firePixel() {
// check for presence of Quantcast Measure tag _qevent obj
if (!window._qevents) {
const gdprPrivacyString = gdprDataHandler.getConsentData();
const usPrivacyString = uspDataHandler.getConsentData();

var fpa = storage.getCookie(QUANTCAST_FPA);
var fpan = '0';
var now = new Date();
var domain = quantcastIdSubmodule.findRootDomain();
var et = now.getTime();
var tzo = now.getTimezoneOffset();
var usPrivacyParamString = '';
var firstPartyParamStrings;
var gdprParamStrings;

if (!(hasGDPRConsent(gdprPrivacyString) && hasCCPAConsent(usPrivacyString))) {
var expired = new Date(0).toUTCString();
fpan = 'u';
fpa = '';
storage.setCookie(QUANTCAST_FPA, fpa, expired, '/', domain, null);
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
} else if (!fpa) {
var expires = new Date(now.getTime() + (cookieExpTime * 86400000)).toGMTString();

Choose a reason for hiding this comment

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

somewhere we need to be checking we have a valid clientId before we set fpa or call quantserve

Choose a reason for hiding this comment

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

(please add tests to cover this)

Copy link

Choose a reason for hiding this comment

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

So this isn't your fault and this is in suit with the current status-quo in Prebid.js, but this is a little maddening.

> git grep '* 24'
modules/apstreamBidAdapter.js:    var daysSinceApEpoch = Math.floor(timeDiff / (1000 * 3600 * 24));
modules/categoryTranslation.js:    if (!mappingData || timestamp() > mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) {
modules/criteoIdSystem.js:const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000;
modules/id5IdSystem.js:  return (new Date(Date.now() + (1000 * 60 * 60 * 24 * expDays))).toUTCString();
modules/lotamePanoramaIdSystem.js:const DAY_MS = 60 * 60 * 24 * 1000;
modules/parrableIdSystem.js:const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000;
modules/pubCommonIdSystem.js:      const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
modules/userId/index.js:    const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
modules/userId/index.js:    const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString();

Copy link
Owner Author

Choose a reason for hiding this comment

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

@ssmccoy - Yeah, this could have been one common const somewhere.

Btw, Any objections to also change 86400000 to 60 * 60 * 24 * 1000? I think it is at least a little clearer.

fpa = 'B0-' + Math.round(Math.random() * 2147483647) + '-' + et;
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
fpan = '1';
storage.setCookie(QUANTCAST_FPA, fpa, expires, '/', domain, null);
}

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

let url = DOMAIN_QSERVE +
Copy link
Owner Author

@sachinsurfs sachinsurfs Jun 22, 2021

Choose a reason for hiding this comment

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

I just had removed 'et' and 'tzo' in the previous commit - b31c0f9

Only cause the general consensus currently is that we want to keep the number of parameters we send through prebid minimum in our first iteration unless we have strong justification for needing it

'?d=' + domain +
'&et=' + et +
'&tzo=' + tzo +
'&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) {
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
return checkTCFv1(gdprConsent.vendorData);
}
if (gdprConsent.apiVersion === 2) {
return checkTCFv2(gdprConsent.vendorData);
}
}
return true;
}

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

return !!(vendorConsent && purposeConsent);
}

export function checkTCFv2(vendorData) {
var vendorConsent = vendorData.vendor && vendorData.vendor.consents && vendorData.vendor.consents[QUANTCAST_VENDOR_ID];
var vendorInterest = vendorData.vendor.legitimateInterests && vendorData.vendor.legitimateInterests[QUANTCAST_VENDOR_ID];
var restrictions = vendorData.publisher ? vendorData.publisher.restrictions : {};

// Restrictions for purpose 1
var qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT]
? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID]
: null;

var purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_DATA_COLLECT];

// No consent, not allowed by publisher or requires legitimate interest
if (!vendorConsent || !purposeConsent || qcRestriction === 0 || qcRestriction === 2) {
return false;
}

// Restrictions for purpose 10
qcRestriction = restrictions && restrictions[PURPOSE_PRODUCT_IMPROVEMENT]
? restrictions[PURPOSE_PRODUCT_IMPROVEMENT][QUANTCAST_VENDOR_ID]
: null;

sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
// Not allowed by publisher
if (qcRestriction === 0) {
return false;
}

// publisher has explicitly restricted to consent
if (qcRestriction === 1) {
purposeConsent = vendorData.purpose && vendorData.purpose.consents && vendorData.purpose.consents[PURPOSE_PRODUCT_IMPROVEMENT];

// No consent, or requires legitimate interest
if (!vendorConsent || !purposeConsent) {
return false;
}
} else if (qcRestriction === 2) {
let purposeInterest = vendorData.purpose.LegitimateInterests && vendorData.purpose.LegitimateInterests[PURPOSE_PRODUCT_IMPROVEMENT];

// No legitimate interest, not allowed by publisher or requires legitimate interest
if (!vendorInterest || !purposeInterest) {
return false;
}
}

return true;
}

/**
* tests if us_privacy consent string is present, us_privacy applies, and do-not-sell is not set
* @returns {boolean}
*/
function hasCCPAConsent(usPrivacyConsent) {
// TODO : Needs to be revisited
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
if (usPrivacyConsent && usPrivacyConsent !== '1---') {
return false
}
return true;
}

/** @type {Submodule} */
export const quantcastIdSubmodule = {
/**
Expand All @@ -34,9 +179,28 @@ 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();

Choose a reason for hiding this comment

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

is coppaDataHandler always available?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yes, coppaDataHandler is a Prebid utility function. It checks for Coppa configuration parameter that the publisher is expected to pass. When not present, it returns undefined.

if (coppa) {
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
logInfo('QuantcastId: IDs not provided for coppa requests, exiting QuantcastId');
return;
sachinsurfs marked this conversation as resolved.
Show resolved Hide resolved
}

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

clientId = configParams.clientId || '';
cookieExpTime = storageParams.expires || DEFAULT_COOKIE_EXP_TIME;

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

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