diff --git a/modules/haloIdSystem.js b/modules/haloIdSystem.js index 4a0330367f5..3011569a17d 100644 --- a/modules/haloIdSystem.js +++ b/modules/haloIdSystem.js @@ -15,6 +15,20 @@ const AU_GVLID = 561; export const storage = getStorageManager(AU_GVLID, 'halo'); +/** + * Param or default. + * @param {String} param + * @param {String} defaultVal + */ +function paramOrDefault(param, defaultVal, arg) { + if (utils.isFn(param)) { + return param(arg); + } else if (utils.isStr(param)) { + return param; + } + return defaultVal; +} + /** @type {Submodule} */ export const haloIdSubmodule = { /** @@ -42,7 +56,12 @@ export const haloIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { - const url = `https://id.halo.ad.gt/api/v1/pbhid`; + if (!utils.isPlainObject(config.params)) { + config.params = {}; + } + const url = paramOrDefault(config.params.url, + `https://id.halo.ad.gt/api/v1/pbhid`, + config.params.urlArg); const resp = function (callback) { let haloId = storage.getDataFromLocalStorage('auHaloId'); diff --git a/modules/haloIdSystem.md b/modules/haloIdSystem.md index 0be0be27f5d..f740ae58048 100644 --- a/modules/haloIdSystem.md +++ b/modules/haloIdSystem.md @@ -30,3 +30,6 @@ The below parameters apply only to the HaloID User ID Module integration. | storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"haloid"` | | 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 Halo 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 | `{"haloId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | +| params | Optional | Object | Used to store params for the id system | +| params.url | Optional | String | Set an alternate GET url for HaloId with this parameter | +| params.urlArg | Optional | Object | Optional url parameter for params.url | diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js index 39e13863ca8..d889310a7c2 100644 --- a/modules/haloRtdProvider.js +++ b/modules/haloRtdProvider.js @@ -10,7 +10,7 @@ import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import {isFn, isStr, isPlainObject, mergeDeep, logError} from '../src/utils.js'; +import {isFn, isStr, isArray, deepEqual, isPlainObject, logError} from '../src/utils.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'halo'; @@ -26,25 +26,67 @@ export const storage = getStorageManager(AU_GVLID, SUBMODULE_NAME); * @param {String} path * @param {Object} val */ -const set = (obj, path, val) => { +function set(obj, path, val) { const keys = path.split('.'); const lastKey = keys.pop(); const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, obj); lastObj[lastKey] = lastObj[lastKey] || val; -}; +} + +/** + * Deep object merging with array deduplication. + * @param {Object} target + * @param {Object} sources + */ +function mergeDeep(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); + + if (isPlainObject(target) && isPlainObject(source)) { + for (const key in source) { + if (isPlainObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else if (isArray(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: source[key] }); + } else if (isArray(target[key])) { + source[key].forEach(obj => { + let e = 1; + for (let i = 0; i < target[key].length; i++) { + if (deepEqual(target[key][i], obj)) { + e = 0; + break; + } + } + if (e) { + target[key].push(obj); + } + }); + } + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} /** * Lazy merge objects. - * @param {String} target - * @param {String} source + * @param {Object} target + * @param {Object} source */ function mergeLazy(target, source) { if (!isPlainObject(target)) { target = {}; } + if (!isPlainObject(source)) { source = {}; } + return mergeDeep(target, source); } @@ -123,6 +165,7 @@ export function getRealTimeData(bidConfig, onDone, rtdConfig, userConsent) { let haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); if (isStr(haloId)) { + (getGlobal()).refreshUserIds({submoduleNames: 'haloId'}); userIds.haloId = haloId; getRealTimeDataAsync(bidConfig, onDone, rtdConfig, userConsent, userIds); } else { diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md index 45097e48129..4a264af9e2e 100644 --- a/modules/haloRtdProvider.md +++ b/modules/haloRtdProvider.md @@ -1,12 +1,23 @@ ## Audigent Halo Real-time Data Submodule -Audigent is a next-generation data management platform and a first-of-a-kind -"data agency" containing some of the most exclusive content-consuming audiences -across desktop, mobile and social platforms. - -This real-time data module provides quality first-party data, contextual data, -site-level data and more that can be injected into bid request objects destined -for different bidders in order to optimize targeting. +Audigent is a next-generation, 1st party data management platform and the +world’s first "data agency", powering the programmatic landscape and DTC +eCommerce with actionable 1st party audience and contextual data from the +world’s most influential retailers, lifestyle publishers, content creators, +athletes and artists. + +The Halo real-time data module in Prebid has been built so that publishers +can maximize the power of their first-party audiences and contextual data. +This module provides both an integrated cookieless Halo identity with real-time +contextual and audience segmentation solution that seamlessly and easily +integrates into your existing Prebid deployment. + +Users, devices, content, cohorts and other features are identified and utilized +to augment every bid request with targeted, 1st party data-derived segments +before being submitted to supply-side platforms. Enriching the bid request with +robust 1st party audience and contextual data, Audigent's Halo RTD module +optimizes targeting, increases the number of bids, increases bid value, +and drives additional incremental revenue for publishers. ### Publisher Usage @@ -113,7 +124,3 @@ To view an example of available segments returned by Audigent's backends: and then point your browser at: `http://localhost:9999/integrationExamples/gpt/haloRtdProvider_example.html` - - - - diff --git a/test/spec/modules/haloIdSystem_spec.js b/test/spec/modules/haloIdSystem_spec.js index 8b6a67adee1..0b8fff12abe 100644 --- a/test/spec/modules/haloIdSystem_spec.js +++ b/test/spec/modules/haloIdSystem_spec.js @@ -38,5 +38,20 @@ describe('HaloIdSystem', function () { callback(callbackSpy); expect(callbackSpy.lastCall.lastArg).to.deep.equal({haloId: 'tstCachedHaloId1'}); }); + + it('allows configurable id url', function() { + const config = { + params: { + url: 'https://haloid.publync.com' + } + }; + const callbackSpy = sinon.spy(); + const callback = haloIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq('https://haloid.publync.com'); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ haloId: 'testHaloId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({haloId: 'testHaloId1'}); + }); }); }); diff --git a/test/spec/modules/haloRtdProvider_spec.js b/test/spec/modules/haloRtdProvider_spec.js index f8bf2dd3dbb..32c0338b87f 100644 --- a/test/spec/modules/haloRtdProvider_spec.js +++ b/test/spec/modules/haloRtdProvider_spec.js @@ -122,6 +122,77 @@ describe('haloRtdProvider', function() { expect(ortb2Config.site.content.data).to.deep.include.members([setConfigSiteObj1, rtdSiteObj1]); }); + it('merges ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }); + + const rtd = { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1, userObj2]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + }); + it('merges bidder-specific ortb2 data', function() { let rtdConfig = {}; let bidConfig = {}; @@ -305,6 +376,141 @@ describe('haloRtdProvider', function() { expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj2, rtdSiteObj2]); }); + it('merges bidder-specific ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { segtax: 3 }, + segment: [{ + id: '1914' + }] + }; + + const userObj3 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '2003' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + }; + + const siteObj2 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + } + ] + }; + + config.setBidderConfig({ + bidders: ['adbuzz'], + config: { + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + } + }); + + config.setBidderConfig({ + bidders: ['pubvisage'], + config: { + ortb2: { + user: { + data: [userObj3] + }, + site: { + content: { + data: [siteObj2] + } + } + } + } + }); + + const rtd = { + ortb2b: { + adbuzz: { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }, + pubvisage: { + ortb2: { + user: { + data: [userObj2, userObj3] + }, + site: { + content: { + data: [siteObj1, siteObj2] + } + } + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getBidderConfig().adbuzz.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + + ortb2Config = config.getBidderConfig().pubvisage.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj3, userObj3]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1, siteObj2]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(2); + }); + it('allows publisher defined rtd ortb2 logic', function() { const rtdConfig = { params: {