Skip to content

Commit

Permalink
ParrableIdSystem : Store third-party cookie support status in first-p…
Browse files Browse the repository at this point in the history
…arty cookie (#6689)

* Add tpcSupport, read it from xxhr and store it in compound cookie

* Parse a cookie boolean number (0, 1) as a boolean value

* Improve conditions

* Add tests

* Tests passing

* Read the cookie splitting parrableId and params (tpc, tpcUntil)

* Adapt tests

* Revert linting in test task

* Convert Date.now to seconds on reading cookie

* Add tests

* Replace Math.trunc with Math.floor

Co-authored-by: Victor <victorigualada@gmail.com>
  • Loading branch information
2 people authored and idettman committed May 21, 2021
1 parent 945bc7a commit 7afcf8a
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 18 deletions.
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
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 () {
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

0 comments on commit 7afcf8a

Please sign in to comment.