diff --git a/allowedModules.js b/allowedModules.js index 0d4a36308c3..171ebebede2 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -13,7 +13,8 @@ module.exports = { ...sharedWhiteList, 'criteo-direct-rsa-validate', 'jsencrypt', - 'crypto-js' + 'crypto-js', + 'live-connect' ], 'src': [ ...sharedWhiteList, diff --git a/integrationExamples/gpt/liveIntentId_example.html b/integrationExamples/gpt/liveIntentId_example.html new file mode 100755 index 00000000000..8484b99d1c4 --- /dev/null +++ b/integrationExamples/gpt/liveIntentId_example.html @@ -0,0 +1,69 @@ + + + + + + + + + + + +

Prebid.js LiveIntent Id Test

+ + diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 5652357569d..60e262f87f5 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -4,15 +4,72 @@ * @module modules/liveIntentIdSystem * @requires module:modules/userId */ -import * as utils from '../src/utils' -import {ajax} from '../src/ajax'; -import {submodule} from '../src/hook'; +import * as utils from '../src/utils'; +import { submodule } from '../src/hook'; +import { LiveConnect } from 'live-connect-js/cjs/live-connect'; +import { uspDataHandler } from '../src/adapterManager'; const MODULE_NAME = 'liveIntentId'; -const LIVE_CONNECT_DUID_KEY = '_li_duid'; -const DOMAIN_USER_ID_QUERY_PARAM_KEY = 'duid'; -const DEFAULT_LIVEINTENT_IDENTITY_URL = 'https://idx.liadm.com'; -const DEFAULT_PREBID_SOURCE = 'prebid'; + +let eventFired = false; +let liveConnect = null; + +/** + * This function is used in tests + */ +export function reset() { + eventFired = false; + liveConnect = null; +} + +function initializeLiveConnect(configParams) { + if (liveConnect) { + return liveConnect; + } + + const publisherId = configParams && configParams.publisherId; + if (!publisherId && typeof publisherId !== 'string') { + utils.logError(`${MODULE_NAME} - publisherId must be defined, not a '${publisherId}'`); + return; + } + + const identityResolutionConfig = { + source: 'prebid', + publisherId: publisherId + }; + if (configParams.url) { + identityResolutionConfig.url = configParams.url + } + if (configParams.partner) { + identityResolutionConfig.source = configParams.partner + } + if (configParams.storage && configParams.storage.expires) { + identityResolutionConfig.expirationDays = configParams.storage.expires; + } + + const liveConnectConfig = { + identifiersToResolve: configParams.identifiersToResolve || [], + wrapperName: 'prebid', + identityResolutionConfig: identityResolutionConfig + }; + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + liveConnectConfig.usPrivacyString = usPrivacyString; + } + if (configParams.appId) { + liveConnectConfig.appId = configParams.appId; + } + + liveConnect = LiveConnect(liveConnectConfig); + return liveConnect; +} + +function tryFireEvent() { + if (!eventFired && liveConnect) { + liveConnect.fire(); + eventFired = true; + } +} /** @type {Submodule} */ export const liveIntentIdSubmodule = { @@ -28,14 +85,21 @@ export const liveIntentIdSubmodule = { * `publisherId` params. * @function * @param {{unifiedId:string}} value + * @param {SubmoduleParams|undefined} [configParams] * @returns {{lipb:Object}} */ - decode(value) { + decode(value, configParams) { function composeIdObject(value) { - const base = {'lipbid': value['unifiedId']}; + const base = { 'lipbid': value['unifiedId'] }; delete value.unifiedId; - return {'lipb': {...base, ...value}}; + return { 'lipb': { ...base, ...value } }; } + + if (configParams) { + initializeLiveConnect(configParams); + tryFireEvent(); + } + return (value && typeof value['unifiedId'] === 'string') ? composeIdObject(value) : undefined; }, @@ -46,52 +110,12 @@ export const liveIntentIdSubmodule = { * @returns {IdResponse|undefined} */ getId(configParams) { - const publisherId = configParams && configParams.publisherId; - if (!publisherId && typeof publisherId !== 'string') { - utils.logError(`${MODULE_NAME} - publisherId must be defined, not a '${publisherId}'`); + const liveConnect = initializeLiveConnect(configParams); + if (!liveConnect) { return; } - let baseUrl = DEFAULT_LIVEINTENT_IDENTITY_URL; - let source = DEFAULT_PREBID_SOURCE; - if (configParams.url) { - baseUrl = configParams.url - } - if (configParams.partner) { - source = configParams.partner - } - - const additionalIdentifierNames = configParams.identifiersToResolve || []; - - const additionalIdentifiers = additionalIdentifierNames.concat([LIVE_CONNECT_DUID_KEY]).reduce((obj, identifier) => { - const value = utils.getCookie(identifier) || utils.getDataFromLocalStorage(identifier); - const key = identifier.replace(LIVE_CONNECT_DUID_KEY, DOMAIN_USER_ID_QUERY_PARAM_KEY); - if (value) { - if (typeof value === 'object') { - obj[key] = JSON.stringify(value); - } else { - obj[key] = value; - } - } - return obj - }, {}); - - const queryString = utils.parseQueryStringParameters(additionalIdentifiers) - const url = `${baseUrl}/idex/${source}/${publisherId}?${queryString}`; - - const result = function (callback) { - ajax(url, response => { - let responseObj = {}; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - utils.logError(error); - } - } - callback(responseObj); - }, undefined, { method: 'GET', withCredentials: true }); - }; - return {callback: result}; + tryFireEvent(); + return { callback: liveConnect.resolve }; } }; diff --git a/modules/userId/index.js b/modules/userId/index.js index cbe5e4de55f..f3db23e77bf 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -37,6 +37,7 @@ * @summary decode a stored value for passing to bid requests * @name Submodule#decode * @param {Object|string} value + * @param {SubmoduleParams|undefined} configParams * @return {(Object|undefined)} */ @@ -73,6 +74,7 @@ * @property {(string|undefined)} pid - placement id url param value * @property {(string|undefined)} publisherId - the unique identifier of the publisher in question * @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query + * @property {(string|undefined)} appId - the unique identifier of the application in question */ /** @@ -411,7 +413,7 @@ function initSubmodules(submodules, consentData) { if (storedId) { // cache decoded value (this is copied to every adUnit bid) - submodule.idObj = submodule.submodule.decode(storedId); + submodule.idObj = submodule.submodule.decode(storedId, submodule.config); } } else if (submodule.config.value) { // cache decoded value (this is copied to every adUnit bid) @@ -420,7 +422,7 @@ function initSubmodules(submodules, consentData) { const response = submodule.submodule.getId(submodule.config.params, consentData, undefined); if (utils.isPlainObject(response)) { if (typeof response.callback === 'function') { submodule.callback = response.callback; } - if (response.id) { submodule.idObj = submodule.submodule.decode(response.id); } + if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } } } carry.push(submodule); diff --git a/package.json b/package.json index cc5b4e4e9f7..8aa01ce93ca 100755 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.5", "jsencrypt": "^3.0.0-rc.1", - "just-clone": "^1.0.2" + "just-clone": "^1.0.2", + "live-connect-js": "^1.0.11" } } diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 4c0e2bb25e6..6ef556fd8ea 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,140 +1,214 @@ -import { liveIntentIdSubmodule } from 'modules/liveIntentIdSystem'; +import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule } from 'modules/liveIntentIdSystem'; import * as utils from 'src/utils'; -import { server } from 'test/mocks/xhr'; +import * as liveConnect from 'live-connect-js/cjs/live-connect'; +import { uspDataHandler } from '../../../src/adapterManager'; + +const assert = require('assert'); + +const PUBLISHER_ID = '89899'; describe('LiveIntentId', function() { - let getCookieStub; - let getDataFromLocalStorageStub; let logErrorStub; + let lcStub; + let consentDataStub; + const fireStub = sinon.stub(); + const resolveStub = sinon.stub(); + const lcClient = { fire: fireStub, resolve: resolveStub }; - const defaultConfigParams = {'publisherId': '89899'}; - const responseHeader = { 'Content-Type': 'application/json' } + const defaultConfigParams = { publisherId: PUBLISHER_ID }; - beforeEach(function () { - getCookieStub = sinon.stub(utils, 'getCookie'); - getDataFromLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + beforeEach(function() { logErrorStub = sinon.stub(utils, 'logError'); + lcStub = sinon.stub(liveConnect, 'LiveConnect'); + lcStub.returns(lcClient); + consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); }); - afterEach(function () { - getCookieStub.restore(); - getDataFromLocalStorageStub.restore(); + afterEach(function() { logErrorStub.restore(); + lcStub.restore(); + consentDataStub.restore(); + fireStub.resetHistory(); + resolveStub.resetHistory(); + resetLiveIntentIdSubmodule(); }); - it('should log an error if no configParams were passed', function() { + it('should log an error if no configParams were passed when getId', function() { liveIntentIdSubmodule.getId(); expect(logErrorStub.calledOnce).to.be.true; }); - it('should log an error if publisherId configParam was not passed', function() { + it('should log an error if publisherId configParam was not passed when getId', function() { liveIntentIdSubmodule.getId({}); expect(logErrorStub.calledOnce).to.be.true; }); - it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { - getCookieStub.returns(null); - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com'}}).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should call the default url of the LiveIntent Identity Exchange endpoint, with a partner', function() { - getCookieStub.returns(null); - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId({...defaultConfigParams, ...{'url': 'https://dummy.liveintent.com', 'partner': 'rubicon'}}).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should call the LiveIntent Identity Exchange endpoint, with no additional query params', function() { - getCookieStub.returns(null); - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; - }); - - it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { - getCookieStub.withArgs('_li_duid').returns('li-fpc'); - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?duid=li-fpc&'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; + it('should initialize LiveConnect with the config params when getId', function() { + liveIntentIdSubmodule.getId({ + ...defaultConfigParams, + ...{ + appId: 'a-0001', + identifiersToResolve: ['id1', 'id2'], + url: 'https://dummy.liveintent.com', + storage: { + expires: 3 + } + } + }); + + expect(lcStub.calledOnce).to.be.true; + assert.deepEqual(lcStub.getCall(0).args[0], { + wrapperName: 'prebid', + appId: 'a-0001', + identifiersToResolve: ['id1', 'id2'], + identityResolutionConfig: { + source: 'prebid', + publisherId: PUBLISHER_ID, + url: 'https://dummy.liveintent.com', + expirationDays: 3 + } + }); }); - it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { - getCookieStub.withArgs('_li_duid').returns('li-fpc'); - getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); - let configParams = { + it('should initialize LiveConnect with a source if it is passed in params when getId', function() { + liveIntentIdSubmodule.getId({ ...defaultConfigParams, ...{ - 'identifiersToResolve': ['_thirdPC'] + partner: 'test-partner' } - }; - - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=third-pc&duid=li-fpc&'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) - ); - expect(callBackSpy.calledOnce).to.be.true; + }); + + expect(lcStub.calledOnce).to.be.true; + expect(lcStub.getCall(0).args[0].identityResolutionConfig.source).to.be.eq('test-partner'); + }); + + it('should initialize LiveConnect with a us privacy string when getId', function() { + consentDataStub.returns('1YNY'); + + liveIntentIdSubmodule.getId(defaultConfigParams); + + expect(lcStub.calledOnce).to.be.true; + expect(lcStub.getCall(0).args[0].usPrivacyString).to.be.eq('1YNY'); + }); + + it('should return function that resolves an identifier when getId', function() { + const result = liveIntentIdSubmodule.getId(defaultConfigParams); + + assert.deepEqual(result, { callback: resolveStub }); + }); + + it('should fire an event when getId', function() { + liveIntentIdSubmodule.getId(defaultConfigParams); + + expect(fireStub.calledOnce).to.be.true; + }); + + it('should log an error if publisherId configParam was not passed when decode', function() { + liveIntentIdSubmodule.decode({}, {}); + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('should initialize LiveConnect with the config params when decode', function() { + liveIntentIdSubmodule.decode({}, { + ...defaultConfigParams, + ...{ + appId: 'a-0001', + identifiersToResolve: ['id1', 'id2'], + url: 'https://dummy.liveintent.com', + storage: { + expires: 3 + } + } + }); + + expect(lcStub.calledOnce).to.be.true; + assert.deepEqual(lcStub.getCall(0).args[0], { + wrapperName: 'prebid', + appId: 'a-0001', + identifiersToResolve: ['id1', 'id2'], + identityResolutionConfig: { + source: 'prebid', + publisherId: PUBLISHER_ID, + url: 'https://dummy.liveintent.com', + expirationDays: 3 + } + }); }); - it('should include an additional identifier value to resolve even if it is an object', function() { - getCookieStub.returns(null); - getDataFromLocalStorageStub.withArgs('_thirdPC').returns({'key': 'value'}); - let configParams = { + it('should initialize LiveConnect with a source if it is passed in params when decode', function() { + liveIntentIdSubmodule.decode({}, { ...defaultConfigParams, ...{ - 'identifiersToResolve': ['_thirdPC'] + partner: 'test-partner' + } + }); + + expect(lcStub.calledOnce).to.be.true; + expect(lcStub.getCall(0).args[0].identityResolutionConfig.source).to.be.eq('test-partner'); + }); + + it('should initialize LiveConnect with a us privacy string when decode', function() { + consentDataStub.returns('1YNY'); + + liveIntentIdSubmodule.decode({}, defaultConfigParams); + + expect(lcStub.calledOnce).to.be.true; + expect(lcStub.getCall(0).args[0].usPrivacyString).to.be.eq('1YNY'); + }); + + it('should not initialize LiveConnect when decode and the config is not set', function() { + liveIntentIdSubmodule.decode({}); + + expect(lcStub.called).to.be.false; + }); + + it('should return a decoded identifier', function() { + const result = liveIntentIdSubmodule.decode( + { + unifiedId: 'id123', + additionalData: 'data' + } + ); + + assert.deepEqual(result, { + lipb: { + lipbid: 'id123', + additionalData: 'data' + } + }); + }); + + it('should not return a decoded identifier when the unifiedId is not present in the value', function() { + const result = liveIntentIdSubmodule.decode( + { + additionalData: 'data' } - }; - - let callBackSpy = sinon.spy(); - let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; - submoduleCallback(callBackSpy); - let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D&'); - request.respond( - 200, - responseHeader, - JSON.stringify({}) ); - expect(callBackSpy.calledOnce).to.be.true; + + expect(result).to.be.undefined; + }); + + it('should fire an event when decode', function() { + liveIntentIdSubmodule.decode({}, defaultConfigParams); + + expect(fireStub.calledOnce).to.be.true; + }); + + it('should fire an event only once', function() { + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + + expect(fireStub.calledOnce).to.be.true; + }); + + it('should initialize LiveConnect only once', function() { + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + liveIntentIdSubmodule.getId(defaultConfigParams); + liveIntentIdSubmodule.decode({}, defaultConfigParams); + + expect(lcStub.calledOnce).to.be.true; }); });