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

Criteo real time user sync #3930

Merged
merged 5 commits into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`;
Copy link
Member

Choose a reason for hiding this comment

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

this isn't going to scale out so well. Is that the only way Criteo supports (1 ID at a time?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, each bidder will have it's own client identifier. As per information we have now, this seems only option for now. There will be changes once we know more about this.

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;
})
});