Skip to content

Commit

Permalink
CriteoId User Module (prebid#4287)
Browse files Browse the repository at this point in the history
* Add CriteoId module

* Update the return type of getId in Criteo Id module

Changes:
- Use of url parsing function from url lib
- Update the return type of getId()
- Update the jsdoc to reflect the real return types

* Fix failing tests for Criteo user module

* Add CriteoIdSystem submodule to .submodule.json.
  • Loading branch information
Swiiip authored and sa1omon committed Nov 28, 2019
1 parent 8acb120 commit 259b6e0
Show file tree
Hide file tree
Showing 3 changed files with 269 additions and 1 deletion.
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"id5IdSystem",
"criteortusIdSystem",
"parrableIdSystem",
"liveIntentIdSystem"
"liveIntentIdSystem",
"criteoIdSystem"
],
"adpod": [
"freeWheelAdserverVideo",
Expand Down
132 changes: 132 additions & 0 deletions modules/criteoIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* This module adds Criteo Real Time User Sync to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/criteoIdSystem
* @requires module:modules/userId
*/

import * as utils from '../src/utils'
import * as ajax from '../src/ajax'
import * as urlLib from '../src/url'
import { getRefererInfo } from '../src/refererDetection'
import { submodule } from '../src/hook';

const bididStorageKey = 'cto_bidid';
const bundleStorageKey = 'cto_bundle';
const cookieWriteableKey = 'cto_test_cookie';
const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000;

const pastDateString = new Date(0).toString();
const expirationString = new Date(utils.timestamp() + cookiesMaxAge).toString();

function areCookiesWriteable() {
utils.setCookie(cookieWriteableKey, '1');
const canWrite = utils.getCookie(cookieWriteableKey) === '1';
utils.setCookie(cookieWriteableKey, '', pastDateString);
return canWrite;
}

function extractProtocolHost (url, returnOnlyHost = false) {
const parsedUrl = urlLib.parse(url)
return returnOnlyHost
? `${parsedUrl.hostname}`
: `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`;
}

function getFromAllStorages(key) {
return utils.getCookie(key) || utils.getDataFromLocalStorage(key);
}

function saveOnAllStorages(key, value) {
if (key && value) {
utils.setCookie(key, value, expirationString);
utils.setDataInLocalStorage(key, value);
}
}

function deleteFromAllStorages(key) {
utils.setCookie(key, '', pastDateString);
utils.removeDataFromLocalStorage(key);
}

function getCriteoDataFromAllStorages() {
return {
bundle: getFromAllStorages(bundleStorageKey),
bidId: getFromAllStorages(bididStorageKey),
}
}

function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent) {
const url = 'https://gum.criteo.com/sid/json?origin=prebid' +
`${topUrl ? '&topUrl=' + encodeURIComponent(topUrl) : ''}` +
`${domain ? '&domain=' + encodeURIComponent(domain) : ''}` +
`${bundle ? '&bundle=' + encodeURIComponent(bundle) : ''}` +
`${areCookiesWriteable ? '&cw=1' : ''}` +
`${isPublishertagPresent ? '&pbt=1' : ''}`

return url;
}

function callCriteoUserSync(parsedCriteoData) {
const cw = areCookiesWriteable();
const topUrl = extractProtocolHost(getRefererInfo().referer);
const domain = extractProtocolHost(document.location.href, true);
const isPublishertagPresent = typeof criteo_pubtag !== 'undefined'; // eslint-disable-line camelcase

const url = buildCriteoUsersyncUrl(
topUrl,
domain,
parsedCriteoData.bundle,
cw,
isPublishertagPresent
);

ajax.ajaxBuilder()(
url,
response => {
const jsonResponse = JSON.parse(response);
if (jsonResponse.bidId) {
saveOnAllStorages(bididStorageKey, jsonResponse.bidId);
} else {
deleteFromAllStorages(bididStorageKey);
}

if (jsonResponse.acwsUrl) {
const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl;
urlsToCall.forEach(url => utils.triggerPixel(url));
} else if (jsonResponse.bundle) {
saveOnAllStorages(bundleStorageKey, jsonResponse.bundle);
}
}
);
}

/** @type {Submodule} */
export const criteoIdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: 'criteo',
/**
* decode the stored id value for passing to bid requests
* @function
* @returns {{criteoId: string} | undefined}
*/
decode(bidId) {
return bidId;
},
/**
* get the Criteo Id from local storages and initiate a new user sync
* @function
* @returns {{id: {criteoId: string} | undefined}}}
*/
getId() {
let localData = getCriteoDataFromAllStorages();
callCriteoUserSync(localData);

return { id: localData.bidId ? { criteoId: localData.bidId } : undefined }
}
};

submodule('userId', criteoIdSubmodule);
135 changes: 135 additions & 0 deletions test/spec/modules/criteoIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { criteoIdSubmodule } from 'modules/criteoIdSystem';
import * as utils from 'src/utils';
import * as ajaxLib from 'src/ajax';
import * as urlLib from 'src/url';

const pastDateString = new Date(0).toString()

function mockResponse(responseText, fakeResponse = (url, callback) => callback(responseText)) {
return function() {
return fakeResponse;
}
}

describe('CriteoId module', function () {
const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000;

const nowTimestamp = new Date().getTime();

let getCookieStub;
let setCookieStub;
let getLocalStorageStub;
let setLocalStorageStub;
let removeFromLocalStorageStub;
let timeStampStub;
let parseUrlStub;
let ajaxBuilderStub;
let triggerPixelStub;

beforeEach(function (done) {
getCookieStub = sinon.stub(utils, 'getCookie');
setCookieStub = sinon.stub(utils, 'setCookie');
getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage');
setLocalStorageStub = sinon.stub(utils, 'setDataInLocalStorage');
removeFromLocalStorageStub = sinon.stub(utils, 'removeDataFromLocalStorage');
timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp);
ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockResponse('{}'));
parseUrlStub = sinon.stub(urlLib, 'parse').returns({protocol: 'https', hostname: 'testdev.com'})
triggerPixelStub = sinon.stub(utils, 'triggerPixel');
done();
});

afterEach(function () {
getCookieStub.restore();
setCookieStub.restore();
getLocalStorageStub.restore();
setLocalStorageStub.restore();
removeFromLocalStorageStub.restore();
timeStampStub.restore();
ajaxBuilderStub.restore();
triggerPixelStub.restore();
parseUrlStub.restore();
});

const storageTestCases = [
{ cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' },
{ cookie: 'bidId', localStorage: undefined, expected: 'bidId' },
{ cookie: undefined, localStorage: 'bidId', expected: 'bidId' },
{ cookie: undefined, localStorage: undefined, expected: undefined },
]

storageTestCases.forEach(testCase => it('getId() should return the bidId when it exists in local storages', function () {
getCookieStub.withArgs('cto_bidid').returns(testCase.cookie);
getLocalStorageStub.withArgs('cto_bidid').returns(testCase.localStorage);

const id = criteoIdSubmodule.getId();
expect(id).to.be.deep.equal({id: testCase.expected ? { criteoId: testCase.expected } : undefined});
}))

it('decode() should return the bidId when it exists in local storages', function () {
const id = criteoIdSubmodule.decode('testDecode');
expect(id).to.equal('testDecode')
});

it('should call user sync url with the right params', function () {
getCookieStub.withArgs('cto_test_cookie').returns('1');
getCookieStub.withArgs('cto_bundle').returns('bundle');
window.criteo_pubtag = {}

const emptyObj = '{}';
let ajaxStub = sinon.stub().callsFake((url, callback) => callback(emptyObj));
ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub))

criteoIdSubmodule.getId();
const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=https%3A%2F%2Ftestdev.com%2F&domain=testdev.com&bundle=bundle&cw=1&pbt=1`;

expect(ajaxStub.calledWith(expectedUrl)).to.be.true;

window.criteo_pubtag = undefined;
});

const responses = [
{ bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' },
{ bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' },
{ bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined },
{ bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' },
{ bundle: 'bundle', bidId: undefined, acwsUrl: undefined },
{ bundle: undefined, bidId: 'bidId', acwsUrl: undefined },
{ bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' },
{ bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] },
{ bundle: undefined, bidId: undefined, acwsUrl: undefined },
]

responses.forEach(response => describe('test user sync response behavior', function () {
const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString();

beforeEach(function (done) {
const fakeResponse = (url, callback) => {
callback(JSON.stringify(response));
setTimeout(done, 0);
}
ajaxBuilderStub.callsFake(mockResponse(undefined, fakeResponse));
criteoIdSubmodule.getId();
})

it('should save bidId if it exists', function () {
if (response.acwsUrl) {
expect(triggerPixelStub.called).to.be.true;
expect(setCookieStub.calledWith('cto_bundle')).to.be.false;
expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false;
} else if (response.bundle) {
expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs)).to.be.true;
expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true;
expect(triggerPixelStub.called).to.be.false;
}

if (response.bidId) {
expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs)).to.be.true;
expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true;
} else {
expect(setCookieStub.calledWith('cto_bidid', '', pastDateString)).to.be.true;
expect(removeFromLocalStorageStub.calledWith('cto_bidid')).to.be.true;
}
});
}));
});

0 comments on commit 259b6e0

Please sign in to comment.