diff --git a/modules/.submodules.json b/modules/.submodules.json index 214fb56f996..0841f5e9777 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -4,6 +4,7 @@ "id5IdSystem", "criteortusIdSystem", "parrableIdSystem", + "britepoolIdSystem", "liveIntentIdSystem", "criteoIdSystem" ], diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js new file mode 100644 index 00000000000..2eb24968c08 --- /dev/null +++ b/modules/britepoolIdSystem.js @@ -0,0 +1,132 @@ +/** + * This module adds BritePoolId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/britepoolIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils' +import {ajax} from '../src/ajax'; +import {submodule} from '../src/hook'; + +/** @type {Submodule} */ +export const britepoolIdSubmodule = { + /** + * Used to link submodule with config + * @type {string} + */ + name: 'britepoolId', + /** + * Decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{britepoolid:string}} + */ + decode(value) { + return (value && typeof value['primaryBPID'] === 'string') ? { 'britepoolid': value['primaryBPID'] } : null; + }, + /** + * Performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [configParams] + * @returns {function(callback:function)} + */ + getId(submoduleConfigParams, consentData) { + const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams(submoduleConfigParams, consentData); + let getterResponse = null; + if (typeof getter === 'function') { + getterResponse = getter(params); + // First let's rule out that the response is not a function + if (typeof getterResponse !== 'function') { + // Optimization to return value from getter + return { + id: britepoolIdSubmodule.normalizeValue(getterResponse) + }; + } + } + // Return for async operation + return { + callback: function(callback) { + if (errors.length > 0) { + errors.forEach(error => utils.logError(error)); + callback(); + return; + } + if (getterResponse) { + // Resolve the getter function response + try { + getterResponse(function(response) { + callback(britepoolIdSubmodule.normalizeValue(response)); + }); + } catch (error) { + if (error !== '') utils.logError(error); + callback(); + } + } else { + ajax(url, { + success: response => { + const responseObj = britepoolIdSubmodule.normalizeValue(response); + callback(responseObj ? { primaryBPID: responseObj.primaryBPID } : null); + }, + error: error => { + if (error !== '') utils.logError(error); + callback(); + } + }, JSON.stringify(params), { customHeaders: headers, contentType: 'application/json', method: 'POST', withCredentials: true }); + } + } + } + }, + /** + * Helper method to create params for our API call + * @param {SubmoduleParams} [configParams] + * @returns {object} Object with parsed out params + */ + createParams(submoduleConfigParams, consentData) { + let errors = []; + const headers = {}; + let params = Object.assign({}, submoduleConfigParams); + if (params.getter) { + // Custom getter will not require other params + if (typeof params.getter !== 'function') { + errors.push(`${MODULE_NAME} - britepoolId submodule requires getter to be a function`); + return { errors }; + } + } else { + if (params.api_key) { + // Add x-api-key into the header + headers['x-api-key'] = params.api_key; + } + } + const url = params.url || 'https://api.britepool.com/v1/britepool/id'; + const getter = params.getter; + delete params.api_key; + delete params.url; + delete params.getter; + return { + params, + headers, + url, + getter, + errors + }; + }, + /** + * Helper method to normalize a JSON value + */ + normalizeValue(value) { + let valueObj = null; + if (typeof value === 'object') { + valueObj = value; + } else if (typeof value === 'string') { + try { + valueObj = JSON.parse(value); + } catch (error) { + utils.logError(error); + } + } + return valueObj; + } +}; + +submodule('userId', britepoolIdSubmodule); diff --git a/modules/britepoolIdSystem.md b/modules/britepoolIdSystem.md new file mode 100644 index 00000000000..89287aed7ca --- /dev/null +++ b/modules/britepoolIdSystem.md @@ -0,0 +1,42 @@ +## BritePool User ID Submodule + +BritePool User ID Module. For assistance setting up your module please contact us at [prebid@britepool.com](prebid@britepool.com). + +### Prebid Params + +Individual params may be set for the BritePool User ID Submodule. At least one identifier must be set in the params. +``` +pbjs.setConfig({ + usersync: { + userIds: [{ + name: 'britepoolId', + storage: { + name: 'britepoolid', + type: 'cookie', + expires: 30 + }, + params: { + url: 'https://sandbox-api.britepool.com/v1/britepool/id', // optional + api_key: '3fdbe297-3690-4f5c-9e11-ee9186a6d77c', // provided by britepool + hash: '31c5543c1734d25c7206f5fd591525d0295bec6fe84ff82f946a34fe970a1e66', // example hash identifier (sha256) + ssid: '221aa074-57fc-453b-81f0-6c74f628cd5c' // example identifier + } + }] + } +}); +``` +## Parameter Descriptions for the `usersync` Configuration Section +The below parameters apply only to the BritePool User ID Module integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID value for the BritePool module - `"britepoolId"` | `"britepoolId"` | +| params | Required | Object | Details for BritePool initialization. | | +| params.api_key | Required | String |BritePool API Key provided by BritePool | "3fdbe297-3690-4f5c-9e11-ee9186a6d77c" | +| params.url | Optional | String |BritePool API url | "https://sandbox-api.britepool.com/v1/britepool/id" | +| params.identifier | Required | String | Where identifier in the params object is the key name. At least one identifier is required. Available Identifiers `aaid` `dtid` `idfa` `ilid` `luid` `mmid` `msid` `mwid` `rida` `ssid` `hash` | `params.ssid` `params.aaid` | +| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | | +| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` | +| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"britepoolid"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` | +| value | Optional | Object | Used only if the page has a separate mechanism for storing the BritePool ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"primaryBPID": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 9371083c8d2..912307d331f 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -698,7 +698,7 @@ const OPEN_RTB_PROTOCOL = { } const bidUserId = utils.deepAccess(bidRequests, '0.bids.0.userId'); - if (bidUserId && typeof bidUserId === 'object' && (bidUserId.tdid || bidUserId.pubcid || bidUserId.parrableid || bidUserId.lipb || bidUserId.id5id || bidUserId.criteoId)) { + if (bidUserId && typeof bidUserId === 'object' && (bidUserId.tdid || bidUserId.pubcid || bidUserId.parrableid || bidUserId.lipb || bidUserId.id5id || bidUserId.criteoId || bidUserId.britepoolid)) { utils.deepSetValue(request, 'user.ext.eids', []); if (bidUserId.tdid) { @@ -764,6 +764,15 @@ const OPEN_RTB_PROTOCOL = { }] }); } + + if (bidUserId.britepoolid) { + request.user.ext.eids.push({ + source: 'britepool.com', + uids: [{ + id: bidUserId.britepoolid + }] + }); + } } if (bidRequests && bidRequests[0].gdprConsent) { diff --git a/test/spec/modules/britepoolIdSystem_spec.js b/test/spec/modules/britepoolIdSystem_spec.js new file mode 100644 index 00000000000..d7250eeb941 --- /dev/null +++ b/test/spec/modules/britepoolIdSystem_spec.js @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import {britepoolIdSubmodule} from 'modules/britepoolIdSystem'; + +describe('BritePool Submodule', () => { + const api_key = '1111'; + const aaid = '4421ea96-34a9-45df-a4ea-3c41a48a18b1'; + const idfa = '2d1c4fac-5507-4e28-991c-ca544e992dba'; + const bpid = '279c0161-5152-487f-809e-05d7f7e653fd'; + const url_override = 'https://override'; + const getter_override = function(params) { + return JSON.stringify({ 'primaryBPID': bpid }); + }; + const getter_callback_override = function(params) { + return callback => { + callback(JSON.stringify({ 'primaryBPID': bpid })); + }; + }; + + it('sends x-api-key in header and one identifier', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid }); + assert(errors.length === 0, errors); + expect(headers['x-api-key']).to.equal(api_key); + expect(params).to.eql({ aaid }); + }); + + it('sends x-api-key in header and two identifiers', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, idfa }); + assert(errors.length === 0, errors); + expect(headers['x-api-key']).to.equal(api_key); + expect(params).to.eql({ aaid, idfa }); + }); + + it('allows call without api_key', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ aaid, idfa }); + expect(params).to.eql({ aaid, idfa }); + expect(errors.length).to.equal(0); + }); + + it('test url override', () => { + const { params, headers, url, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override }); + expect(url).to.equal(url_override); + // Making sure it did not become part of params + expect(params.url).to.be.undefined; + }); + + it('test getter override with value', () => { + const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_override }); + expect(getter).to.equal(getter_override); + // Making sure it did not become part of params + expect(params.getter).to.be.undefined; + const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_override }); + assert.deepEqual(response, { id: { 'primaryBPID': bpid } }); + }); + + it('test getter override with callback', done => { + const { params, headers, url, getter, errors } = britepoolIdSubmodule.createParams({ api_key, aaid, url: url_override, getter: getter_callback_override }); + expect(getter).to.equal(getter_callback_override); + // Making sure it did not become part of params + expect(params.getter).to.be.undefined; + const response = britepoolIdSubmodule.getId({ api_key, aaid, url: url_override, getter: getter_callback_override }); + expect(response.callback).to.not.be.undefined; + response.callback(result => { + assert.deepEqual(result, { 'primaryBPID': bpid }); + done(); + }); + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 4058f4435ca..40ffb726051 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -12,6 +12,7 @@ import events from 'src/events'; import CONSTANTS from 'src/constants.json'; import {unifiedIdSubmodule} from 'modules/userId/unifiedIdSystem'; import {pubCommonIdSubmodule} from 'modules/userId/pubCommonIdSystem'; +import {britepoolIdSubmodule} from 'modules/britepoolIdSystem'; import {id5IdSubmodule} from 'modules/id5IdSystem'; import {identityLinkSubmodule} from 'modules/identityLinkIdSystem'; import {liveIntentIdSubmodule} from 'modules/liveIntentIdSystem'; @@ -21,7 +22,7 @@ let expect = require('chai').expect; const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; describe('User ID', function() { - function getConfigMock(configArr1, configArr2, configArr3, configArr4) { + function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5) { return { userSync: { syncDelay: 0, @@ -29,7 +30,8 @@ describe('User ID', function() { (configArr1 && configArr1.length >= 3) ? getStorageMock.apply(null, configArr1) : null, (configArr2 && configArr2.length >= 3) ? getStorageMock.apply(null, configArr2) : null, (configArr3 && configArr3.length >= 3) ? getStorageMock.apply(null, configArr3) : null, - (configArr4 && configArr4.length >= 3) ? getStorageMock.apply(null, configArr4) : null + (configArr4 && configArr4.length >= 3) ? getStorageMock.apply(null, configArr4) : null, + (configArr5 && configArr5.length >= 3) ? getStorageMock.apply(null, configArr5) : null ].filter(i => i)} } } @@ -298,7 +300,7 @@ describe('User ID', function() { expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); - it('config with 1 configuration should create 1 submodule', function () { + it('config with 1 configurations should create 1 submodules', function () { setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); @@ -306,8 +308,8 @@ describe('User ID', function() { expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 1 submodules'); }); - it('config with 5 configurations should result in 5 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule]); + it('config with 6 configurations should result in 6 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule]); init(config); config.setConfig({ usersync: { @@ -326,10 +328,14 @@ describe('User ID', function() { }, { name: 'liveIntentId', storage: { name: '_li_pbid', type: 'cookie' } + }, { + name: 'britepoolId', + value: { 'primaryBPID': '279c0161-5152-487f-809e-05d7f7e653fd' } + }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 5 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('User ID - usersync config updated for 6 submodules'); }); it('config syncDelay updates module correctly', function () { @@ -816,18 +822,40 @@ describe('User ID', function() { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId and id5Id have data to pass', function(done) { + it('test hook from britepoolid cookies', function(done) { + // simulate existing browser local storage values + utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': '279c0161-5152-487f-809e-05d7f7e653fd'}), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([britepoolIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['britepoolId', 'britepoolid', 'cookie'])); + + requestBidsHook(function() { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.britepoolid'); + expect(bid.userId.britepoolid).to.equal('279c0161-5152-487f-809e-05d7f7e653fd'); + }); + }); + utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, and britepoolId have data to pass', function(done) { utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', (new Date(Date.now() + 5000).toUTCString())); + utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], ['id5Id', 'id5id', 'cookie'], - ['identityLink', 'idl_env', 'cookie'])); + ['identityLink', 'idl_env', 'cookie'], + ['britepoolId', 'britepoolid', 'cookie'])); requestBidsHook(function() { adUnits.forEach(unit => { @@ -844,21 +872,26 @@ describe('User ID', function() { // check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // also check that britepoolId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.britepoolid'); + expect(bid.userId.britepoolid).to.equal('testbritepoolid'); }); }); utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId and id5Id have their modules added before and after init', function(done) { + it('test hook when pubCommonId, unifiedId, id5Id and britepoolId have their modules added before and after init', function(done) { utils.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); setSubmoduleRegistry([]); @@ -871,11 +904,13 @@ describe('User ID', function() { attachIdSystem(unifiedIdSubmodule); attachIdSystem(id5IdSubmodule); attachIdSystem(identityLinkSubmodule); + attachIdSystem(britepoolIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], ['id5Id', 'id5id', 'cookie'], - ['identityLink', 'idl_env', 'cookie'])); + ['identityLink', 'idl_env', 'cookie'], + ['britepoolId', 'britepoolid', 'cookie'])); requestBidsHook(function() { adUnits.forEach(unit => { @@ -892,12 +927,16 @@ describe('User ID', function() { // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // also check that britepoolId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.britepoolid'); + expect(bid.userId.britepoolid).to.equal('testbritepoolid'); }); }); utils.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits}); }); @@ -907,9 +946,10 @@ describe('User ID', function() { utils.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); utils.setCookie('id5id', JSON.stringify({'ID5ID': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('idl_env', 'AiGNC8Z5ONyZKSpIPf', new Date(Date.now() + 5000).toUTCString()); + utils.setCookie('britepoolid', JSON.stringify({'primaryBPID': 'testbritepoolid'}), (new Date(Date.now() + 5000).toUTCString())); utils.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule]); init(config); config.setConfig({ @@ -923,6 +963,8 @@ describe('User ID', function() { name: 'id5Id', storage: { name: 'id5id', type: 'cookie' } }, { name: 'identityLink', storage: { name: 'idl_env', type: 'cookie' } + }, { + name: 'britepoolId', storage: { name: 'britepoolid', type: 'cookie' } }, { name: 'mockId', storage: { name: 'MOCKID', type: 'cookie' } }] @@ -958,6 +1000,9 @@ describe('User ID', function() { // also check that identityLink id data was copied to bid expect(bid).to.have.deep.nested.property('userId.idl_env'); expect(bid.userId.idl_env).to.equal('AiGNC8Z5ONyZKSpIPf'); + // also check that britepoolId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.britepoolid'); + expect(bid.userId.britepoolid).to.equal('testbritepoolid'); // check MockId data was copied to bid expect(bid).to.have.deep.nested.property('userId.mid'); expect(bid.userId.mid).to.equal('123456778'); @@ -967,6 +1012,7 @@ describe('User ID', function() { utils.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); utils.setCookie('id5id', '', EXPIRED_COOKIE_DATE); utils.setCookie('idl_env', '', EXPIRED_COOKIE_DATE); + utils.setCookie('britepoolid', '', EXPIRED_COOKIE_DATE); utils.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); done(); }, {adUnits});