From bdb0fec524f4a1b3854e385675e8f084aa440ab3 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 21 Jul 2021 20:54:39 -0500 Subject: [PATCH 1/6] halo allow id url config, rtd dedupe array merge, doc update --- modules/haloIdSystem.js | 21 ++++++- modules/haloRtdProvider.js | 52 +++++++++++++++-- modules/haloRtdProvider.md | 25 +++++--- test/spec/modules/haloIdSystem_spec.js | 15 +++++ test/spec/modules/haloRtdProvider_spec.js | 71 +++++++++++++++++++++++ 5 files changed, 171 insertions(+), 13 deletions(-) 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/haloRtdProvider.js b/modules/haloRtdProvider.js index 39e13863ca8..83768cfb0e4 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); } diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md index 45097e48129..a081c9e3d9d 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 for Prebid.org has been created for publishers +so that they 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 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..fe1e526da93 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 = {}; From 1d3e7b0a6eb721ca3f1676c0631d54143ede01eb Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 21 Jul 2021 21:12:24 -0500 Subject: [PATCH 2/6] add per-bidder dedupe test --- test/spec/modules/haloRtdProvider_spec.js | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/test/spec/modules/haloRtdProvider_spec.js b/test/spec/modules/haloRtdProvider_spec.js index fe1e526da93..32c0338b87f 100644 --- a/test/spec/modules/haloRtdProvider_spec.js +++ b/test/spec/modules/haloRtdProvider_spec.js @@ -376,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: { From 839ec0f7e987187d50f123566c1087f76b39fbf9 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 21 Jul 2021 22:08:26 -0500 Subject: [PATCH 3/6] refresh pbjs haloId when available --- modules/haloRtdProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/haloRtdProvider.js b/modules/haloRtdProvider.js index 83768cfb0e4..d889310a7c2 100644 --- a/modules/haloRtdProvider.js +++ b/modules/haloRtdProvider.js @@ -165,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 { From b7d2b7273233d6b14113602e9e3cedc08fd92438 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 21 Jul 2021 22:29:53 -0500 Subject: [PATCH 4/6] doc update --- modules/haloIdSystem.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/haloIdSystem.md b/modules/haloIdSystem.md index 0be0be27f5d..6bd6598a64e 100644 --- a/modules/haloIdSystem.md +++ b/modules/haloIdSystem.md @@ -30,3 +30,5 @@ 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 | From 07ff275b287b2acfba2c75e52bcc1b7124cc49e3 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 27 Jul 2021 19:49:58 -0500 Subject: [PATCH 5/6] update docs --- modules/haloIdSystem.md | 1 + modules/haloRtdProvider.md | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/haloIdSystem.md b/modules/haloIdSystem.md index 6bd6598a64e..f740ae58048 100644 --- a/modules/haloIdSystem.md +++ b/modules/haloIdSystem.md @@ -32,3 +32,4 @@ The below parameters apply only to the HaloID User ID Module integration. | 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.md b/modules/haloRtdProvider.md index a081c9e3d9d..e1d0da43309 100644 --- a/modules/haloRtdProvider.md +++ b/modules/haloRtdProvider.md @@ -6,11 +6,11 @@ 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 for Prebid.org has been created for publishers -so that they 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. +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 From e9624edcf65a8e3fd58e7a83e699856078425306 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 27 Jul 2021 21:13:55 -0500 Subject: [PATCH 6/6] doc update --- modules/haloRtdProvider.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/haloRtdProvider.md b/modules/haloRtdProvider.md index e1d0da43309..4a264af9e2e 100644 --- a/modules/haloRtdProvider.md +++ b/modules/haloRtdProvider.md @@ -124,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` - - - -