Skip to content

Commit

Permalink
Criteo real time user sync (prebid#3930)
Browse files Browse the repository at this point in the history
* add criteo rtus submodule and user id changes

* update appnexus adapter to include criteo user id

* updated to submodules pattern
  • Loading branch information
jaiminpanchal27 authored and sa1omon committed Nov 28, 2019
1 parent 53f64e1 commit 66bb9fc
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 2 deletions.
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"userId": [
"digiTrustIdSystem",
"id5IdSystem"
"id5IdSystem",
"criteortusIdSystem"
],
"adpod": [
"freeWheelAdserverVideo"
Expand Down
10 changes: 10 additions & 0 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ export const spec = {
});
}

const rtusId = utils.deepAccess(bidRequests[0], `userId.criteortus.${BIDDER_CODE}.userid`);
if (rtusId) {
let tpuids = [];
tpuids.push({
'provider': 'criteo',
'user_id': rtusId
});
payload.tpuids = tpuids;
}

const request = formatRequest(payload, bidderRequest);
return request;
},
Expand Down
105 changes: 105 additions & 0 deletions modules/criteortusIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* This module adds Criteo Real Time User Sync to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/criteortusIdSystem
* @requires module:modules/userId
*/

import * as utils from '../src/utils'
import { ajax } from '../src/ajax';
import { submodule } from '../src/hook';

const key = '__pbjs_criteo_rtus';

/** @type {Submodule} */
export const criteortusIdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: 'criteortus',
/**
* decode the stored id value for passing to bid requests
* @function
* @returns {{criteortus:Object}}
*/
decode() {
let uid = utils.getCookie(key);
try {
uid = JSON.parse(uid);
return { 'criteortus': uid };
} catch (error) {
utils.logError('Error in parsing criteo rtus data', error);
}
},
/**
* 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) {
if (!configParams || !utils.isPlainObject(configParams.clientIdentifier)) {
utils.logError('User ID - Criteo rtus requires client identifier to be defined');
return;
}

let uid = utils.getCookie(key);
if (uid) {
return uid;
} else {
let userIds = {};
return function(callback) {
let bidders = Object.keys(configParams.clientIdentifier);

function afterAllResponses() {
// criteo rtus user id expires in 1 hour
const expiresStr = (new Date(Date.now() + (60 * 60 * 1000))).toUTCString();
utils.setCookie(key, JSON.stringify(userIds), expiresStr);
callback(userIds);
}

const onResponse = utils.delayExecution(afterAllResponses, bidders.length);

bidders.forEach((bidder) => {
let url = `https://gum.criteo.com/sync?c=${configParams.clientIdentifier[bidder]}&r=3`;
const getSuccessHandler = (bidder) => {
return function onSuccess(response) {
if (response) {
try {
response = JSON.parse(response);
userIds[bidder] = response;
onResponse();
} catch (error) {
utils.logError(error);
}
}
}
}

const getFailureHandler = (bidder) => {
return function onFailure(error) {
utils.logError(`Criteo RTUS server call failed for ${bidder}`, error);
onResponse();
}
}

ajax(
url,
{
success: getSuccessHandler(bidder),
error: getFailureHandler(bidder)
},
undefined,
Object.assign({
method: 'GET',
withCredentials: true
})
);
})
}
}
}
};

submodule('userId', criteortusIdSubmodule);
13 changes: 12 additions & 1 deletion modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ function processSubmoduleCallbacks(submodules) {
submodule.callback = undefined;
// if valid, id data should be saved to cookie/html storage
if (idObj) {
setStoredValue(submodule.config.storage, idObj, submodule.config.storage.expires);
if (submodule.config.storage) {
setStoredValue(submodule.config.storage, idObj, submodule.config.storage.expires);
}
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(idObj);
} else {
Expand Down Expand Up @@ -299,6 +301,13 @@ function initSubmodules(submodules, consentData) {
} else if (submodule.config.value) {
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.config.value;
} else {
const result = submodule.submodule.getId(submodule.config.params, consentData);
if (typeof result === 'function') {
submodule.callback = result;
} else {
submodule.idObj = submodule.submodule.decode();
}
}
carry.push(submodule);
return carry;
Expand Down Expand Up @@ -332,6 +341,8 @@ function getValidSubmoduleConfigs(configRegistry, submoduleRegistry, activeStora
carry.push(config);
} else if (utils.isPlainObject(config.value)) {
carry.push(config);
} else if (!config.storage && !config.value) {
carry.push(config);
}
return carry;
}, []);
Expand Down
16 changes: 16 additions & 0 deletions test/spec/modules/appnexusBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,22 @@ describe('AppNexusAdapter', function () {
rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',')
});
});

it('should populate tpids array when userId is available', function () {
const bidRequest = Object.assign({}, bidRequests[0], {
userId: {
criteortus: {
appnexus: {
userid: 'sample-userid'
}
}
}
});

const request = spec.buildRequests([bidRequest]);
const payload = JSON.parse(request.data);
expect(payload.tpuids).to.deep.equal([{provider: 'criteo', user_id: 'sample-userid'}]);
});
})

describe('interpretResponse', function () {
Expand Down
88 changes: 88 additions & 0 deletions test/spec/modules/criteortusIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { criteortusIdSubmodule } from 'modules/criteortusIdSystem';
import * as utils from 'src/utils';

describe('Criteo RTUS', function() {
let xhr;
let requests;
let getCookieStub;
let logErrorStub;

beforeEach(function () {
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = request => requests.push(request);
getCookieStub = sinon.stub(utils, 'getCookie');
logErrorStub = sinon.stub(utils, 'logError');
});

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

it('should log error when configParams are not passed', function() {
criteortusIdSubmodule.getId();
expect(logErrorStub.calledOnce).to.be.true;
})

it('should call criteo endpoint to get user id', function() {
getCookieStub.returns(null);
let configParams = {
clientIdentifier: {
'sampleBidder': 1
}
}

let response = { 'status': 'ok', 'userid': 'sample-userid' }
let callBackSpy = sinon.spy();
let submoduleCallback = criteortusIdSubmodule.getId(configParams);
submoduleCallback(callBackSpy);
requests[0].respond(
200,
{ 'Content-Type': 'text/plain' },
JSON.stringify(response)
);
expect(callBackSpy.calledOnce).to.be.true;
expect(callBackSpy.calledWith({'sampleBidder': response})).to.be.true;
})

it('should get uid from cookie and not call endpoint', function() {
let response = {'appnexus': {'status': 'ok', 'userid': 'sample-userid'}}
getCookieStub.returns(JSON.stringify(response));
let configParams = {
clientIdentifier: {
'sampleBidder': 1
}
}
let uid = criteortusIdSubmodule.getId(configParams);
expect(requests.length).to.equal(0);
})

it('should call criteo endpoint for multiple bidders', function() {
getCookieStub.returns(null);
let configParams = {
clientIdentifier: {
'sampleBidder': 1,
'sampleBidder2': 2
}
}

let response = { 'status': 'ok', 'userid': 'sample-userid' }
let callBackSpy = sinon.spy();
let submoduleCallback = criteortusIdSubmodule.getId(configParams);
submoduleCallback(callBackSpy);
requests[0].respond(
200,
{ 'Content-Type': 'text/plain' },
JSON.stringify(response)
);
expect(callBackSpy.calledOnce).to.be.false;
requests[1].respond(
200,
{ 'Content-Type': 'text/plain' },
JSON.stringify(response)
);
expect(callBackSpy.calledOnce).to.be.true;
})
});

0 comments on commit 66bb9fc

Please sign in to comment.