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

PBID-91: Store third-party cookie support state in compound cookie and expire after ttl #18

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.trunc((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
146 changes: 144 additions & 2 deletions test/spec/modules/parrableIdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 +58,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 Down Expand Up @@ -125,7 +131,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 +247,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 () {

Choose a reason for hiding this comment

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

I think we need a parallel block

describe('when reading tpcSupport status from cookie');

and it tests sending the tpc support to the backend, and then when the tpcUntil expires it should test suppressing the status from the cookie.

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 +671,7 @@ describe('Parrable ID System', function() {
});
});

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

const partnersTestCase = [
Expand Down