diff --git a/modules/.submodules.json b/modules/.submodules.json index 4e02391129a..fc69dd276a3 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -19,7 +19,8 @@ "idxIdSystem", "fabrickIdSystem", "verizonMediaIdSystem", - "pubProvidedIdSystem" + "pubProvidedIdSystem", + "tapadIdSystem" ], "adpod": [ "freeWheelAdserverVideo", diff --git a/modules/tapadIdSystem.js b/modules/tapadIdSystem.js new file mode 100644 index 00000000000..e4dfe304585 --- /dev/null +++ b/modules/tapadIdSystem.js @@ -0,0 +1,61 @@ +import { uspDataHandler } from '../src/adapterManager.js'; +import { submodule } from '../src/hook.js'; +import * as ajax from '../src/ajax.js' +import * as utils from '../src/utils.js'; + +export const graphUrl = 'https://realtime-graph-access-zxvhwknfeq-uc.a.run.app/v1/graph'; + +export const tapadIdSubmodule = { + name: 'tapadId', + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{tapadId: string} | undefined} + */ + decode(id) { + return { tapadId: id }; + }, + /* + * @function + * @summary initiate Real Time Graph + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] + * @returns {IdResponse }} + */ + getId(config) { + const uspData = uspDataHandler.getConsentData(); + if (uspData && uspData !== '1---') { + return { id: undefined }; + } + const configParams = config.params || {}; + + if (configParams.companyId == null || isNaN(Number(configParams.companyId))) { + utils.logMessage('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + } + + return { + callback: (complete) => { + ajax.ajaxBuilder(10000)( + `${graphUrl}?company_id=${configParams.companyId}&tapad_id_type=TAPAD_ID`, + { + success: (response) => { + const responseJson = JSON.parse(response); + if (responseJson.hasOwnProperty('tapadId')) { + complete(responseJson.tapadId); + } + }, + error: (_, e) => { + if (e.status === 404) { + complete(undefined); + } + if (e.status === 403) { + utils.logMessage('Invalid Company Id. Contact prebid@tapad.com for assistance.'); + } + } + } + ); + } + } + } +} +submodule('userId', tapadIdSubmodule); diff --git a/modules/userId/eids.js b/modules/userId/eids.js index d714d09962d..80e336d206a 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -168,6 +168,10 @@ const USER_IDS_CONFIG = { 'fabrickId': { source: 'neustar.biz', atype: 1 + }, + 'tapadId': { + source: 'tapad.com', + atype: 1 } }; diff --git a/modules/userId/eids.md b/modules/userId/eids.md index fecf7e888bf..847e43545d6 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -141,6 +141,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 1 }] + }, + { + source: 'tapad.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 26dbad2f153..59b0c1f001d 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -169,6 +169,18 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('tapadId', function() { + const userId = { + tapadId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'tapad.com', + uids: [{id: 'some-random-id-value', atype: 1}] + }); + }); + it('NetId', function() { const userId = { netId: 'some-random-id-value' diff --git a/test/spec/modules/tapadIdSystem_spec.js b/test/spec/modules/tapadIdSystem_spec.js new file mode 100644 index 00000000000..bc31f1d37ba --- /dev/null +++ b/test/spec/modules/tapadIdSystem_spec.js @@ -0,0 +1,65 @@ +import { tapadIdSubmodule, graphUrl } from 'modules/tapadIdSystem.js'; +import * as utils from 'src/utils.js'; + +import { server } from 'test/mocks/xhr.js'; + +describe('TapadIdSystem', function () { + describe('getId', function() { + const config = { params: { companyId: 12345 } }; + it('should call to real time graph endpoint and handle valid response', function() { + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + expect(request.url).to.eq(`${graphUrl}?company_id=12345&tapad_id_type=TAPAD_ID`); + + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ tapadId: 'your-tapad-id' })); + expect(callbackSpy.lastCall.lastArg).to.eq('your-tapad-id'); + }); + + it('should remove stored tapadId if not found', function() { + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + + request.respond(404); + expect(callbackSpy.lastCall.lastArg).to.be.undefined; + }); + + it('should log message with invalid company id', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const request = server.requests[0]; + + request.respond(403); + expect(logMessageSpy.lastCall.lastArg).to.eq('Invalid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + + it('should log message if company id not given', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId({}).callback; + callback(callbackSpy); + + expect(logMessageSpy.lastCall.lastArg).to.eq('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + + it('should log message if company id is incorrect format', function() { + const logMessageSpy = sinon.spy(utils, 'logMessage'); + const callbackSpy = sinon.spy(); + const callback = tapadIdSubmodule.getId({ params: { companyId: 'notANumber' } }).callback; + callback(callbackSpy); + + expect(logMessageSpy.lastCall.lastArg).to.eq('Please provide a valid Company Id. Contact prebid@tapad.com for assistance.'); + logMessageSpy.restore(); + }); + }); +}) diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 981ebb5f50e..e937fa3d631 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -36,6 +36,7 @@ import {sharedIdSubmodule} from 'modules/sharedIdSystem.js'; import {haloIdSubmodule} from 'modules/haloIdSystem.js'; import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; +import {tapadIdSubmodule} from 'modules/tapadIdSystem.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -454,7 +455,7 @@ describe('User ID', function () { }); it('handles config with no usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({}); // usersync is undefined, and no logInfo message for 'User ID - usersync config updated' @@ -462,14 +463,14 @@ describe('User ID', function () { }); it('handles config with empty usersync object', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({userSync: {}}); expect(typeof utils.logInfo.args[0]).to.equal('undefined'); }); it('handles config with usersync and userIds that are empty objs', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -480,7 +481,7 @@ describe('User ID', function () { }); it('handles config with usersync and userIds with empty names or that dont match a submodule.name', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, merkleIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -497,15 +498,15 @@ describe('User ID', function () { }); it('config with 1 configurations should create 1 submodules', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig(getConfigMock(['unifiedId', 'unifiedid', 'cookie'])); expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 1 submodules'); }); - it('config with 13 configurations should result in 13 submodules add', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + it('config with 14 configurations should result in 14 submodules add', function () { + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -545,14 +546,17 @@ describe('User ID', function () { name: 'zeotapIdPlus' }, { name: 'criteo' + }, { + name: 'tapadId', + storage: {name: 'tapad_id', type: 'cookie'} }] } }); - expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 13 submodules'); + expect(utils.logInfo.args[0][0]).to.exist.and.to.contain('User ID - usersync config updated for 14 submodules'); }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -567,7 +571,7 @@ describe('User ID', function () { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -582,7 +586,7 @@ describe('User ID', function () { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -984,6 +988,29 @@ describe('User ID', function () { }, {adUnits}); }); + it('test hook from tapadIdModule cookie', function (done) { + coreStorage.setCookie('tapad_id', 'test-tapad-id', (new Date(Date.now() + 100000).toUTCString())); + + setSubmoduleRegistry([tapadIdSubmodule]); + init(config); + config.setConfig(getConfigMock(['tapadId', 'tapad_id', 'cookie'])); + + requestBidsHook(function () { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.tapadId'); + expect(bid.userId.tapadId).to.equal('test-tapad-id'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'tapad.com', + uids: [{id: 'test-tapad-id', atype: 1}] + }); + }); + }) + coreStorage.setCookie('tapad_id', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from liveIntentId html5', function (done) { // simulate existing browser local storage values localStorage.setItem('_li_pbid', JSON.stringify({'unifiedId': 'random-ls-identifier'})); @@ -1481,7 +1508,7 @@ describe('User ID', function () { coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); - setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule]); + setSubmoduleRegistry([pubCommonIdSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, sharedIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule, tapadIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1493,7 +1520,8 @@ describe('User ID', function () { ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], ['haloId', 'haloId', 'cookie'], - ['criteo', 'storage_criteo', 'cookie'])); + ['criteo', 'storage_criteo', 'cookie'], + ['tapadId', 'tapad_id', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => { @@ -1586,6 +1614,7 @@ describe('User ID', function () { attachIdSystem(zeotapIdPlusSubmodule); attachIdSystem(haloIdSubmodule); attachIdSystem(criteoIdSubmodule); + attachIdSystem(tapadIdSubmodule); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1597,7 +1626,8 @@ describe('User ID', function () { ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], ['haloId', 'haloId', 'cookie'], - ['criteo', 'storage_criteo', 'cookie'])); + ['criteo', 'storage_criteo', 'cookie'], + ['tapadId', 'tapad_id', 'cookie'])); requestBidsHook(function () { adUnits.forEach(unit => {