-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Hugo Duthil
committed
Oct 24, 2019
1 parent
77ae055
commit cf3db2b
Showing
2 changed files
with
284 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/** | ||
* 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 { 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, ''); | ||
return canWrite; | ||
} | ||
|
||
function extractProtocolHost (url, returnOnlyHost = false) { | ||
var a = document.createElement('a'); | ||
a.href = url; | ||
return returnOnlyHost | ||
? `${a.hostname}` | ||
: `${a.protocol}//${a.hostname}${a.port ? ':' + a.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 {{criteo:Object}} | ||
*/ | ||
decode() { | ||
const localData = getCriteoDataFromAllStorages(); | ||
|
||
if (localData.bidId) { | ||
return { 'criteoId': localData.bidId } | ||
} | ||
}, | ||
/** | ||
* performs action to obtain id and return a value in the callback's response argument | ||
* @function | ||
* @param {SubmoduleParams} [configParams] | ||
* @returns {function(callback:function)} | ||
*/ | ||
getId(configParams) { | ||
let localData = getCriteoDataFromAllStorages(); | ||
callCriteoUserSync(localData); | ||
|
||
if (localData.bidId) { | ||
return { 'criteoId': localData.bidId } | ||
} | ||
} | ||
}; | ||
|
||
submodule('userId', criteoIdSubmodule); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { getRefererInfo } from 'src/refererDetection' | ||
import { criteoIdSubmodule } from 'modules/criteoIdSystem'; | ||
import * as utils from 'src/utils'; | ||
import * as ajaxLib from 'src/ajax'; | ||
|
||
const pastDateString = new Date(0).toString() | ||
|
||
function extractProtocolHost (url, returnOnlyHost = false) { | ||
var a = document.createElement('a'); | ||
a.href = url; | ||
return returnOnlyHost | ||
? `${a.hostname}` | ||
: `${a.protocol}//${a.hostname}${a.port ? ':' + a.port : ''}/`; | ||
} | ||
|
||
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 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('{}')); | ||
triggerPixelStub = sinon.stub(utils, 'triggerPixel'); | ||
done(); | ||
}); | ||
|
||
afterEach(function () { | ||
getCookieStub.restore(); | ||
setCookieStub.restore(); | ||
getLocalStorageStub.restore(); | ||
setLocalStorageStub.restore(); | ||
removeFromLocalStorageStub.restore(); | ||
timeStampStub.restore(); | ||
ajaxBuilderStub.restore(); | ||
triggerPixelStub.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(testCase.expected ? { criteoId: testCase.expected } : undefined); | ||
})) | ||
|
||
storageTestCases.forEach(testCase => it('decode() 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.decode(); | ||
expect(id).to.be.deep.equal(testCase.expected ? { criteoId: testCase.expected } : undefined); | ||
})) | ||
|
||
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 topUrl = extractProtocolHost(getRefererInfo().referer) | ||
const domain = extractProtocolHost(document.location.href, true) | ||
const expectedUrl = `https://gum.criteo.com/sid/json?origin=prebid&topUrl=${encodeURIComponent(topUrl)}&domain=${encodeURIComponent(domain)}&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; | ||
} | ||
}); | ||
})); | ||
}); |