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

ParrableIdSystem: Store third-party cookie support status in first-party cookie #6741

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 45 additions & 16 deletions modules/parrableIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,36 @@ function deserializeParrableId(parrableIdStr) {

values.forEach(function(value) {
const pair = value.split(':');
// unpack a value of 1 as true
parrableId[pair[0]] = +pair[1] === 1 ? true : pair[1];
if (+pair[1] === 1 || (pair[1] !== null && +pair[1] === 0)) { // unpack a value of 0 or 1 as boolean
parrableId[pair[0]] = Boolean(+pair[1]);
} else if (!isNaN(pair[1])) { // convert to number if is a number
parrableId[pair[0]] = +pair[1]
} else {
parrableId[pair[0]] = pair[1]
}
});

return parrableId;
}

function serializeParrableId(parrableId) {
function serializeParrableId(parrableIdAndParams) {
let components = [];

if (parrableId.eid) {
components.push('eid:' + parrableId.eid);
if (parrableIdAndParams.eid) {
components.push('eid:' + parrableIdAndParams.eid);
}
if (parrableId.ibaOptout) {
if (parrableIdAndParams.ibaOptout) {
components.push('ibaOptout:1');
}
if (parrableId.ccpaOptout) {
if (parrableIdAndParams.ccpaOptout) {
components.push('ccpaOptout:1');
}
if (parrableIdAndParams.tpcSupport !== undefined) {
const tpcSupportComponent = parrableIdAndParams.tpcSupport === true ? 'tpc:1' : 'tpc:0';
const tpcUntil = `tpcUntil:${parrableIdAndParams.tpcUntil}`;
components.push(tpcSupportComponent);
components.push(tpcUntil);
}

return components.join(',');
}
Expand Down Expand Up @@ -84,14 +95,21 @@ function encodeBase64UrlSafe(base64) {
function readCookie() {
const parrableIdStr = storage.getCookie(PARRABLE_COOKIE_NAME);
if (parrableIdStr) {
return deserializeParrableId(decodeURIComponent(parrableIdStr));
const parsedCookie = deserializeParrableId(decodeURIComponent(parrableIdStr));
const { tpc, tpcUntil, ...parrableId } = parsedCookie;
let { eid, ibaOptout, ccpaOptout, ...params } = parsedCookie;

if ((Date.now() / 1000) >= tpcUntil) {
params.tpc = undefined;
}
return { parrableId, params };
}
return null;
}

function writeCookie(parrableId) {
if (parrableId) {
const parrableIdStr = encodeURIComponent(serializeParrableId(parrableId));
function writeCookie(parrableIdAndParams) {
if (parrableIdAndParams) {
const parrableIdStr = encodeURIComponent(serializeParrableId(parrableIdAndParams));
storage.setCookie(PARRABLE_COOKIE_NAME, parrableIdStr, getExpirationDate(), 'lax');
}
}
Expand Down Expand Up @@ -175,10 +193,14 @@ function shouldFilterImpression(configParams, parrableId) {
return isBlocked() || !isAllowed();
}

function epochFromTtl(ttl) {
return Math.floor((Date.now() / 1000) + ttl);
}

function fetchId(configParams, gdprConsentData) {
if (!isValidConfig(configParams)) return;

let parrableId = readCookie();
let { parrableId, params } = readCookie() || {};
if (!parrableId) {
parrableId = readLegacyCookies();
migrateLegacyCookies(parrableId);
Expand All @@ -188,12 +210,13 @@ function fetchId(configParams, gdprConsentData) {
return null;
}

const eid = (parrableId) ? parrableId.eid : null;
const eid = parrableId ? parrableId.eid : null;
const refererInfo = getRefererInfo();
const tpcSupport = params ? params.tpc : null
const uspString = uspDataHandler.getConsentData();
const gdprApplies = (gdprConsentData && typeof gdprConsentData.gdprApplies === 'boolean' && gdprConsentData.gdprApplies);
const gdprConsentString = (gdprConsentData && gdprApplies && gdprConsentData.consentString) || '';
const partners = configParams.partners || configParams.partner
const partners = configParams.partners || configParams.partner;
const trackers = typeof partners === 'string'
? partners.split(',')
: partners;
Expand All @@ -203,7 +226,8 @@ function fetchId(configParams, gdprConsentData) {
trackers,
url: refererInfo.referer,
prebidVersion: '$prebid.version$',
isIframe: utils.inIframe()
isIframe: utils.inIframe(),
tpcSupport
};

const searchParams = {
Expand All @@ -229,6 +253,7 @@ function fetchId(configParams, gdprConsentData) {
const callbacks = {
success: response => {
let newParrableId = parrableId ? utils.deepClone(parrableId) : {};
let newParams = {};
if (response) {
try {
let responseObj = JSON.parse(response);
Expand All @@ -242,12 +267,16 @@ function fetchId(configParams, gdprConsentData) {
if (responseObj.ibaOptout === true) {
newParrableId.ibaOptout = true;
}
if (responseObj.tpcSupport !== undefined) {
newParams.tpcSupport = responseObj.tpcSupport;
newParams.tpcUntil = epochFromTtl(responseObj.tpcSupportTtl);
}
}
} catch (error) {
utils.logError(error);
cb();
}
writeCookie(newParrableId);
writeCookie({ ...newParrableId, ...newParams });
cb(newParrableId);
} else {
utils.logError('parrableId: ID fetch returned an empty result');
Expand Down
149 changes: 146 additions & 3 deletions test/spec/modules/parrableIdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { server } from 'test/mocks/xhr.js';
const storage = newStorageManager();

const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT';
const EXPIRE_COOKIE_TIME = 20000;
const P_COOKIE_NAME = '_parrable_id';
const P_COOKIE_EID = '01.1563917337.test-eid';
const P_XHR_EID = '01.1588030911.test-new-eid'
Expand All @@ -21,6 +22,7 @@ const P_CONFIG_MOCK = {
partners: 'parrable_test_partner_123,parrable_test_partner_456'
}
};
const RESPONSE_HEADERS = { 'Content-Type': 'application/json' };

function getConfigMock() {
return {
Expand Down Expand Up @@ -57,6 +59,11 @@ function serializeParrableId(parrableId) {
if (parrableId.ccpaOptout) {
str += ',ccpaOptout:1';
}
if (parrableId.tpc !== undefined) {
const tpcSupportComponent = parrableId.tpc === true ? 'tpc:1' : 'tpc:0';
str += `,${tpcSupportComponent}`;
str += `,tpcUntil:${parrableId.tpcUntil}`;
}
return str;
}

Expand All @@ -65,7 +72,7 @@ function writeParrableCookie(parrableId) {
storage.setCookie(
P_COOKIE_NAME,
cookieValue,
(new Date(Date.now() + 5000).toUTCString()),
(new Date(Date.now() + EXPIRE_COOKIE_TIME).toUTCString()),
'lax'
);
}
Expand Down Expand Up @@ -125,7 +132,6 @@ describe('Parrable ID System', function() {
{ 'Content-Type': 'text/plain' },
JSON.stringify({ eid: P_XHR_EID })
);

expect(callbackSpy.lastCall.lastArg).to.deep.equal({
eid: P_XHR_EID
});
Expand Down Expand Up @@ -242,6 +248,143 @@ describe('Parrable ID System', function() {
})
});
});

describe('third party cookie support status', function () {
let logErrorStub;
let callbackSpy = sinon.spy();

beforeEach(function() {
logErrorStub = sinon.stub(utils, 'logError');
});

afterEach(function () {
callbackSpy.resetHistory();
removeParrableCookie();
});

afterEach(function() {
logErrorStub.restore();
});

describe('when getting tpcSupport from XHR response', function () {
let request;
let dateNowStub;
const dateNowMock = Date.now();
const tpcSupportTtl = 1;

before(() => {
dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock);
});

after(() => {
dateNowStub.restore();
});

it('should set tpcSupport: true and tpcUntil in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];

request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, tpcSupport: true, tpcSupportTtl })
);

expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID + ',tpc:1,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl))
);
});

it('should set tpcSupport: false and tpcUntil in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];
request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, tpcSupport: false, tpcSupportTtl })
);

expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID + ',tpc:0,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl))
);
});

it('should not set tpcSupport in the cookie', function () {
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];

request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID })
);

expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID)
);
});
});

describe('when getting tpcSupport from cookie', function () {
let request;
let dateNowStub;
const dateNowMock = Date.now();
const tpcSupportTtl = dateNowMock;
const tpcUntilExpired = 1;

before(() => {
dateNowStub = sinon.stub(Date, 'now').returns(dateNowMock);
});

after(() => {
dateNowStub.restore();
});

it('should send tpcSupport in the XHR', function () {
writeParrableCookie({
eid: P_COOKIE_EID,
tpc: true,
tpcUntil: (dateNowMock / 1000) + 1
});
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];

let queryParams = utils.parseQS(request.url.split('?')[1]);
let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data)));

expect(data.tpcSupport).to.equal(true);
});

it('should unset tpcSupport from cookie when tpcUntil reached', function () {
writeParrableCookie({
eid: P_COOKIE_EID,
tpcSupport: true,
tpcUntil: tpcUntilExpired
});
let { callback } = parrableIdSubmodule.getId(P_CONFIG_MOCK);
callback(callbackSpy);
request = server.requests[0];

request.respond(
200,
RESPONSE_HEADERS,
JSON.stringify({ eid: P_XHR_EID, tpcSupport: false, tpcSupportTtl })
);

let queryParams = utils.parseQS(request.url.split('?')[1]);
let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data)));

expect(data.tpcSupport).to.equal(undefined);
expect(storage.getCookie(P_COOKIE_NAME)).to.equal(
encodeURIComponent('eid:' + P_XHR_EID + ',tpc:0,tpcUntil:' + Math.floor((dateNowMock / 1000) + tpcSupportTtl))
);
});
});
});
});

describe('parrableIdSystem.decode()', function() {
Expand Down Expand Up @@ -529,7 +672,7 @@ describe('Parrable ID System', function() {
});
});

describe('partners parsing', () => {
describe('partners parsing', function () {
let callbackSpy = sinon.spy();

const partnersTestCase = [
Expand Down