From aad50f26049a3892c82bbed34d75bd6155321a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Wed, 29 Sep 2021 21:08:15 +0200 Subject: [PATCH 1/7] First version of brandmetrics RTD- module --- modules/brandmetricsRtdProvider.js | 90 ++++++++++++++++++++++++++++++ modules/brandmetricsRtdProvider.md | 29 ++++++++++ 2 files changed, 119 insertions(+) create mode 100644 modules/brandmetricsRtdProvider.js create mode 100644 modules/brandmetricsRtdProvider.md diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js new file mode 100644 index 00000000000..701e6432a04 --- /dev/null +++ b/modules/brandmetricsRtdProvider.js @@ -0,0 +1,90 @@ +/** + * This module adds brandmetrics provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will set brandmetrics survey targeting to ad units of specific bidders + * @module modules/brandmetricsRtdProvider + * @requires module:modules/realTimeData + */ +import { getGlobal } from '../src/prebidGlobal.js' +import { submodule } from '../src/hook.js' +import { deepSetValue, mergeDeep, logError } from '../src/utils.js' +const MODULE_NAME = 'brandmetrics' + +function init (config, userConsent) { + return true +} + +/** +* Set targeting for brandmetrics surveys +*/ +function setSurveyTargeting (reqBidsConfigObj, callback, customConfig) { + const config = mergeDeep({ + waitForIt: false, + params: { + } + }, customConfig) + + if (config.waitForIt) { + const onBrandmetricsReady = () => { + const brandmetricsApi = window.brandmetrics.api; + if (brandmetricsApi.surveyLoadCompleted()) { + setBidTargeting(reqBidsConfigObj, brandmetricsApi); + callback(); + } else { + brandmetricsApi.addEventListener({ + event: 'surveyloaded', + handler: () => { + setBidTargeting(reqBidsConfigObj, brandmetricsApi); + callback(); + } + }) + } + }; + + window._brandmetrics = window._brandmetrics || []; + window._brandmetrics.push({ + cmd: '_addeventlistener', + val: { + event: 'ready', + handler: onBrandmetricsReady + } + }); + } else { + callback() + } +} + +function setBidTargeting (reqBidsConfigObj, brandmetricsApi) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits + + const targetingConf = brandmetricsApi.getSurveyTargeting('pb') + if (targetingConf) { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const bidder = bid.bidder + switch (bidder) { + case 'ozone': + deepSetValue(bid, 'params.customData.0.targeting.' + targetingConf.key, targetingConf.val) + break; + default: + break; + } + }) + }) + } +} + +/** @type {RtdSubmodule} */ +export const brandmetricsSubmodule = { + name: MODULE_NAME, + getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { + try { + setSurveyTargeting(reqBidsConfigObj, callback, customConfig); + } catch (e) { + logError(e) + } + }, + init: init +} + +submodule('realTimeData', brandmetricsSubmodule) diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md new file mode 100644 index 00000000000..032768f6bf7 --- /dev/null +++ b/modules/brandmetricsRtdProvider.md @@ -0,0 +1,29 @@ +# Brandmetrics Real-time Data Submodule +TODO + +## Usage +Compile the Brandmetrics RTD module into your Prebid build: +``` +gulp build --modules=rtdModule,brandmetricsRtdProvider +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Brandmetrics RTD module. + +Enable the Brandmetrics RTD in your Prebid configuration, using the below format: + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 500, // auction delay + dataProviders: [{ + name: 'brandmetrics', + waitForIt: true // should be true if there's an `auctionDelay` + }] + }, + ... +}) +``` +NOTE: A brandmetrics site- script present at the site is required at this point + +# TODO \ No newline at end of file From 4e0d21e566b06522330133ea1438603d387ba15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Wed, 24 Nov 2021 15:15:49 +0100 Subject: [PATCH 2/7] Implement brandmetricsRtdProvider --- modules/brandmetricsRtdProvider.js | 119 ++++++++---- modules/brandmetricsRtdProvider.md | 28 ++- .../modules/brandmetricsRtdProvider_spec.js | 170 ++++++++++++++++++ 3 files changed, 275 insertions(+), 42 deletions(-) create mode 100644 test/spec/modules/brandmetricsRtdProvider_spec.js diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 701e6432a04..4231ec6d9ab 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -1,7 +1,7 @@ /** * This module adds brandmetrics provider to the real time data module * The {@link module:modules/realTimeData} module is required - * The module will set brandmetrics survey targeting to ad units of specific bidders + * The module will load load the brandmetrics script and set survey- targeting to ad units of specific bidders. * @module modules/brandmetricsRtdProvider * @requires module:modules/realTimeData */ @@ -10,76 +10,119 @@ import { submodule } from '../src/hook.js' import { deepSetValue, mergeDeep, logError } from '../src/utils.js' const MODULE_NAME = 'brandmetrics' +const RECEIVED_EVENTS = []; + function init (config, userConsent) { + const moduleConfig = getMergedConfig(config); + initializeBrandmetrics(moduleConfig.params.scriptId); return true } /** -* Set targeting for brandmetrics surveys +* Add event- listeners to hook in to brandmetrics events +* @param {Object} reqBidsConfigObj +* @param {function} callback */ -function setSurveyTargeting (reqBidsConfigObj, callback, customConfig) { - const config = mergeDeep({ - waitForIt: false, - params: { - } - }, customConfig) - - if (config.waitForIt) { - const onBrandmetricsReady = () => { - const brandmetricsApi = window.brandmetrics.api; - if (brandmetricsApi.surveyLoadCompleted()) { - setBidTargeting(reqBidsConfigObj, brandmetricsApi); - callback(); - } else { - brandmetricsApi.addEventListener({ - event: 'surveyloaded', - handler: () => { - setBidTargeting(reqBidsConfigObj, brandmetricsApi); - callback(); - } - }) +function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { + const callBidTargeting = (event) => { + if (event.available && event.conf) { + const targetingConf = event.conf.displayOption || {} + if (targetingConf.type === 'pbjs') { + setBidTargeting(reqBidsConfigObj, moduleConfig, targetingConf.targetKey || 'brandmetrics_survey', event.survey.measurementId) } - }; + } + callback(); + }; + if (RECEIVED_EVENTS.length > 0) { + callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]); + } else { window._brandmetrics = window._brandmetrics || []; window._brandmetrics.push({ cmd: '_addeventlistener', val: { - event: 'ready', - handler: onBrandmetricsReady + event: 'surveyloaded', + reEmitLast: true, + handler: (ev) => { + RECEIVED_EVENTS.push(ev); + if (RECEIVED_EVENTS.length === 1) { + // Call bid targeting only for the first received event, if called subsequently, last event from the RECEIVED_EVENTS array is used + callBidTargeting(ev); + } + }, } }); - } else { - callback() } } -function setBidTargeting (reqBidsConfigObj, brandmetricsApi) { +/** + * Sets bid targeting of specific bidders + * @param {Object} reqBidsConfigObj + * @param {string} key Targeting key + * @param {string} val Targeting value + */ +function setBidTargeting (reqBidsConfigObj, moduleConfig, key, val) { const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits - - const targetingConf = brandmetricsApi.getSurveyTargeting('pb') - if (targetingConf) { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const bidder = bid.bidder + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const bidder = bid.bidder + if (moduleConfig.params.bidders.indexOf(bidder) !== -1) { switch (bidder) { case 'ozone': - deepSetValue(bid, 'params.customData.0.targeting.' + targetingConf.key, targetingConf.val) + deepSetValue(bid, 'params.customData.0.targeting.' + key, val) break; default: break; } - }) + } }) + }) +} + +/** + * Add the brandmetrics script to the page. + * @param {string} scriptId - The script- id provided by brandmetrics or brandmetrics partner + */ +function initializeBrandmetrics(scriptId) { + if (scriptId) { + const path = 'https://cdn.brandmetrics.com/survey/script/'; + const file = scriptId + '.js'; + + const el = document.createElement('script'); + el.type = 'text/javascript'; + el.async = true; + el.src = path + file; + + document.getElementsByTagName('head')[0].appendChild(el); } } +/** + * Merges a provided config with default values + * @param {Object} customConfig + * @returns + */ +function getMergedConfig(customConfig) { + return mergeDeep({ + waitForIt: false, + params: { + bidders: [], + scriptId: undefined, + } + }, customConfig); +} + /** @type {RtdSubmodule} */ export const brandmetricsSubmodule = { name: MODULE_NAME, getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { try { - setSurveyTargeting(reqBidsConfigObj, callback, customConfig); + const moduleConfig = getMergedConfig(customConfig); + if (moduleConfig.waitForIt) { + processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback); + } else { + callback(); + } } catch (e) { logError(e) } diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md index 032768f6bf7..fa0296237f2 100644 --- a/modules/brandmetricsRtdProvider.md +++ b/modules/brandmetricsRtdProvider.md @@ -1,5 +1,6 @@ # Brandmetrics Real-time Data Submodule -TODO +This module is intended to be used by brandmetrics (https://brandmetrics.com) partners and sets targeting keywords to bids if the browser is eligeble to see a brandmetrics survey. +The module hooks in to brandmetrics events and requires a brandmetrics script to be running. The module can optionally load and initialize brandmetrics by providing the 'scriptId'- parameter. ## Usage Compile the Brandmetrics RTD module into your Prebid build: @@ -18,12 +19,31 @@ pbjs.setConfig({ auctionDelay: 500, // auction delay dataProviders: [{ name: 'brandmetrics', - waitForIt: true // should be true if there's an `auctionDelay` + waitForIt: true // should be true if there's an `auctionDelay`, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } }] }, ... }) ``` -NOTE: A brandmetrics site- script present at the site is required at this point -# TODO \ No newline at end of file +## Supported bidders + +The module currently supports the following bidders: + +| Bidder | Id | +| ------ | ----- | +| Ozone | ozone | + + +## Parameters +| Name | Type | Description | Default | +| ----------------- | -------------------- | ------------------ | ------------------ | +| name | String | This should always be `brandmetrics` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (recommended) | `false` | +| params | Object | | - | +| params.bidders | String[] | An array of bidders which should targeting keys. | `[]` | +| params.scriptId | String | A script- id GUID if the brandmetrics- script should be initialized. | `undefined` | diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js new file mode 100644 index 00000000000..ac238205b45 --- /dev/null +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -0,0 +1,170 @@ +import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; +import { cloneDeep } from 'lodash'; + +const VALID_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } +}; + +const NO_BIDDERS_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000' + } +}; + +const NO_SCRIPTID_CONFIG = { + name: 'brandmetrics', + waitForIt: true +}; + +function mockSurveyLoaded(surveyConf) { + const commands = window._brandmetrics || []; + commands.forEach(command => { + if (command.cmd === '_addeventlistener') { + const conf = command.val; + if (conf.event === 'surveyloaded') { + conf.handler(surveyConf); + } + } + }); +} + +function scriptTagExists(url) { + const tags = document.getElementsByTagName('script'); + for (let i = 0; i < tags.length; i++) { + if (tags[i].src === url) { + return true; + } + } + return false; +} + +describe('BrandmetricsRTD module', () => { + beforeEach(function () { + const scriptTags = document.getElementsByTagName('script'); + for (let i = 0; i < scriptTags.length; i++) { + if (scriptTags[i].src.indexOf('brandmetrics') !== -1) { + scriptTags[i].remove(); + } + } + }); + + it('should init and return true', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG)).to.equal(true); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); + }); + + it('should init and return true even if bidders is not included', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG)).to.equal(true); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); + }); + + it('should not add any script- tag when script- id is not defined', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG)).to.equal(true); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/undefined.js')).to.equal(false); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/null.js')).to.equal(false); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); + }); +}); + +describe('getBidRequetData', () => { + /* beforeEach(function () { + window._brandmetrics = []; + }); */ + + it('should set targeting keys for specified bidders only', () => { + const conf = { + adUnits: [{ + bids: [{ + bidder: 'ozone' + }] + }, { + bids: [{ + bidder: 'unspecified' + }] + }] + }; + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { + expect(conf.adUnits[0].bids[0].params.customData[0].targeting.mockTargetKey).to.equal('mockMeasurementId'); + expect(conf.adUnits[1].bids[0].params).to.be.undefined; + }, VALID_CONFIG); + + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + }); + + it('should only set targeting keys when the brandmetrics survey- type is "pbjs"', () => { + const conf = { + adUnits: [{ + bids: [{ + bidder: 'ozone' + }] + }, { + bids: [{ + bidder: 'unspecified' + }] + }] + }; + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { + expect(conf.adUnits[0].bids[0].params).to.be.undefined; + expect(conf.adUnits[1].bids[0].params).to.be.undefined; + }, VALID_CONFIG); + + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'dfp', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + }); + + it('should use a default targeting key name if the brandmetrics- configuration does not include one', () => { + const conf = { + adUnits: [{ + bids: [{ + bidder: 'ozone' + }] + }] + }; + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { + expect(conf.adUnits[0].bids[0].params.customData[0].targeting.brandmetrics_survey).to.equal('mockMeasurementId'); + }, VALID_CONFIG); + + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + }); +}); From 64573ecf44eaba1457ca19bc3e44d7f3cb49ab1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Tue, 11 Jan 2022 16:59:10 +0100 Subject: [PATCH 3/7] Add gdpr and usp consent- checks --- modules/brandmetricsRtdProvider.js | 55 +++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 4231ec6d9ab..af5001544de 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -9,15 +9,62 @@ import { getGlobal } from '../src/prebidGlobal.js' import { submodule } from '../src/hook.js' import { deepSetValue, mergeDeep, logError } from '../src/utils.js' const MODULE_NAME = 'brandmetrics' +const RECEIVED_EVENTS = [] +const GVL_ID = 422 +const TCF_PURPOSES = [1, 7] -const RECEIVED_EVENTS = []; +let hasConsent = false; function init (config, userConsent) { - const moduleConfig = getMergedConfig(config); - initializeBrandmetrics(moduleConfig.params.scriptId); + hasConsent = checkConsent(userConsent); + + if (hasConsent) { + const moduleConfig = getMergedConfig(config); + initializeBrandmetrics(moduleConfig.params.scriptId); + } return true } +function checkConsent (userConsent) { + let consent = false; + + const gdprApplies = (userConsent && userConsent.gdpr) ? userConsent.gdpr.gdprApplies : false; + const usp = userConsent.usp + + if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + + const vendor = gdpr.vendorData.vendor; + const purpose = gdpr.vendorData.purpose; + + let vendorConsent = false; + + if (v.consents) { + vendorConsent = vendor.consents[GVL_ID]; + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID]; + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length; + + if (vendorConsent && purposesValid) { + consent = true; + } + } + } else if (userConsent.usp) { + consent = uspData.uspString[1] !== 'N' && uspData.uspString[2] !== 'Y' + } + + return consent; +} + /** * Add event- listeners to hook in to brandmetrics events * @param {Object} reqBidsConfigObj @@ -118,7 +165,7 @@ export const brandmetricsSubmodule = { getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { try { const moduleConfig = getMergedConfig(customConfig); - if (moduleConfig.waitForIt) { + if (moduleConfig.waitForIt && hasConsent) { processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback); } else { callback(); From 13c3bdced39cf23ece6f7e32cd316ac0be201556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Tue, 11 Jan 2022 17:29:18 +0100 Subject: [PATCH 4/7] Add user- consent related tests --- modules/brandmetricsRtdProvider.js | 13 ++-- .../modules/brandmetricsRtdProvider_spec.js | 64 ++++++++++++++++--- 2 files changed, 61 insertions(+), 16 deletions(-) diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index af5001544de..285b6c68170 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -28,20 +28,16 @@ function init (config, userConsent) { function checkConsent (userConsent) { let consent = false; - const gdprApplies = (userConsent && userConsent.gdpr) ? userConsent.gdpr.gdprApplies : false; - const usp = userConsent.usp - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { const gdpr = userConsent.gdpr if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor; const purpose = gdpr.vendorData.purpose; let vendorConsent = false; - if (v.consents) { + if (vendor.consents) { vendorConsent = vendor.consents[GVL_ID]; } @@ -55,11 +51,12 @@ function checkConsent (userConsent) { const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length; if (vendorConsent && purposesValid) { - consent = true; + consent = true; } - } + } } else if (userConsent.usp) { - consent = uspData.uspString[1] !== 'N' && uspData.uspString[2] !== 'Y' + const usp = userConsent.usp; + consent = usp[1] !== 'N' && usp[2] !== 'Y' } return consent; diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index ac238205b45..5a8d50e855c 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -23,6 +23,48 @@ const NO_SCRIPTID_CONFIG = { waitForIt: true }; +const USER_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: true + } + }, + purpose: { + consents: { + 1: true, + 7: true + } + } + }, + gdprApplies: true + } +}; + +const NO_TCF_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: false + } + }, + purpose: { + consents: { + 1: false, + 7: false + } + } + }, + gdprApplies: true + } +}; + +const NO_USP_CONSENT = { + usp: '1NYY' +}; + function mockSurveyLoaded(surveyConf) { const commands = window._brandmetrics || []; commands.forEach(command => { @@ -56,28 +98,34 @@ describe('BrandmetricsRTD module', () => { }); it('should init and return true', () => { - expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG)).to.equal(true); + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, USER_CONSENT)).to.equal(true); expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); }); it('should init and return true even if bidders is not included', () => { - expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG)).to.equal(true); + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG, USER_CONSENT)).to.equal(true); expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); }); it('should not add any script- tag when script- id is not defined', () => { - expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG)).to.equal(true); + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG, USER_CONSENT)).to.equal(true); expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/undefined.js')).to.equal(false); expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/null.js')).to.equal(false); expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); }); -}); -describe('getBidRequetData', () => { - /* beforeEach(function () { - window._brandmetrics = []; - }); */ + it('should not add any script- tag when there is no TCF- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_TCF_CONSENT)).to.equal(true); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); + }); + + it('should not add any script- tag when there is no usp- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(true); + expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); + }); +}); +describe('getBidRequestData', () => { it('should set targeting keys for specified bidders only', () => { const conf = { adUnits: [{ From ac607a0dd05de1a707ef7ee97ab73d5e2bf2bfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Tue, 11 Jan 2022 22:04:13 +0100 Subject: [PATCH 5/7] Set targeting keys in a more generic way --- modules/brandmetricsRtdProvider.js | 107 ++++++++---------- modules/brandmetricsRtdProvider.md | 11 +- .../modules/brandmetricsRtdProvider_spec.js | 80 ++++--------- 3 files changed, 75 insertions(+), 123 deletions(-) diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 285b6c68170..60d3c98f15e 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -5,61 +5,62 @@ * @module modules/brandmetricsRtdProvider * @requires module:modules/realTimeData */ -import { getGlobal } from '../src/prebidGlobal.js' +import { config } from '../src/config.js' import { submodule } from '../src/hook.js' -import { deepSetValue, mergeDeep, logError } from '../src/utils.js' +import { deepSetValue, mergeDeep, logError, deepAccess } from '../src/utils.js' +import {loadExternalScript} from '../src/adloader.js' const MODULE_NAME = 'brandmetrics' +const MODULE_CODE = MODULE_NAME const RECEIVED_EVENTS = [] const GVL_ID = 422 const TCF_PURPOSES = [1, 7] -let hasConsent = false; - function init (config, userConsent) { - hasConsent = checkConsent(userConsent); + const hasConsent = checkConsent(userConsent) if (hasConsent) { - const moduleConfig = getMergedConfig(config); - initializeBrandmetrics(moduleConfig.params.scriptId); + const moduleConfig = getMergedConfig(config) + initializeBrandmetrics(moduleConfig.params.scriptId) } - return true + return hasConsent } +/** + * Checks TCF and USP consents + * @param {Object} userConsent + * @returns {boolean} + */ function checkConsent (userConsent) { - let consent = false; + let consent = false if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { const gdpr = userConsent.gdpr if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor; - const purpose = gdpr.vendorData.purpose; - - let vendorConsent = false; + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + let vendorConsent = false if (vendor.consents) { - vendorConsent = vendor.consents[GVL_ID]; + vendorConsent = vendor.consents[GVL_ID] } if (vendor.legitimateInterests) { - vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID]; + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] } const purposes = TCF_PURPOSES.map(id => { return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) }) - const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length; - - if (vendorConsent && purposesValid) { - consent = true; - } + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid } } else if (userConsent.usp) { - const usp = userConsent.usp; + const usp = userConsent.usp consent = usp[1] !== 'N' && usp[2] !== 'Y' } - return consent; + return consent } /** @@ -72,30 +73,30 @@ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { if (event.available && event.conf) { const targetingConf = event.conf.displayOption || {} if (targetingConf.type === 'pbjs') { - setBidTargeting(reqBidsConfigObj, moduleConfig, targetingConf.targetKey || 'brandmetrics_survey', event.survey.measurementId) + setBidderTargeting(reqBidsConfigObj, moduleConfig, targetingConf.targetKey || 'brandmetrics_survey', event.survey.measurementId) } } - callback(); - }; + callback() + } if (RECEIVED_EVENTS.length > 0) { - callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]); + callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]) } else { - window._brandmetrics = window._brandmetrics || []; + window._brandmetrics = window._brandmetrics || [] window._brandmetrics.push({ cmd: '_addeventlistener', val: { event: 'surveyloaded', reEmitLast: true, handler: (ev) => { - RECEIVED_EVENTS.push(ev); + RECEIVED_EVENTS.push(ev) if (RECEIVED_EVENTS.length === 1) { // Call bid targeting only for the first received event, if called subsequently, last event from the RECEIVED_EVENTS array is used - callBidTargeting(ev); + callBidTargeting(ev) } }, } - }); + }) } } @@ -105,22 +106,16 @@ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { * @param {string} key Targeting key * @param {string} val Targeting value */ -function setBidTargeting (reqBidsConfigObj, moduleConfig, key, val) { - const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const bidder = bid.bidder - if (moduleConfig.params.bidders.indexOf(bidder) !== -1) { - switch (bidder) { - case 'ozone': - deepSetValue(bid, 'params.customData.0.targeting.' + key, val) - break; - default: - break; - } - } +function setBidderTargeting (reqBidsConfigObj, moduleConfig, key, val) { + const bidders = deepAccess(moduleConfig, 'params.bidders') + if (bidders && bidders.length > 0) { + const ortb2 = {} + deepSetValue(ortb2, 'ortb2.user.ext.data.' + key, val) + config.setBidderConfig({ + bidders: bidders, + config: ortb2 }) - }) + } } /** @@ -129,15 +124,11 @@ function setBidTargeting (reqBidsConfigObj, moduleConfig, key, val) { */ function initializeBrandmetrics(scriptId) { if (scriptId) { - const path = 'https://cdn.brandmetrics.com/survey/script/'; - const file = scriptId + '.js'; - - const el = document.createElement('script'); - el.type = 'text/javascript'; - el.async = true; - el.src = path + file; + const path = 'https://cdn.brandmetrics.com/survey/script/' + const file = scriptId + '.js' + const url = path + file - document.getElementsByTagName('head')[0].appendChild(el); + loadExternalScript(url, MODULE_CODE) } } @@ -153,7 +144,7 @@ function getMergedConfig(customConfig) { bidders: [], scriptId: undefined, } - }, customConfig); + }, customConfig) } /** @type {RtdSubmodule} */ @@ -161,11 +152,11 @@ export const brandmetricsSubmodule = { name: MODULE_NAME, getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { try { - const moduleConfig = getMergedConfig(customConfig); - if (moduleConfig.waitForIt && hasConsent) { - processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback); + const moduleConfig = getMergedConfig(customConfig) + if (moduleConfig.waitForIt) { + processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback) } else { - callback(); + callback() } } catch (e) { logError(e) diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md index fa0296237f2..89ee6bb75cf 100644 --- a/modules/brandmetricsRtdProvider.md +++ b/modules/brandmetricsRtdProvider.md @@ -30,20 +30,11 @@ pbjs.setConfig({ }) ``` -## Supported bidders - -The module currently supports the following bidders: - -| Bidder | Id | -| ------ | ----- | -| Ozone | ozone | - - ## Parameters | Name | Type | Description | Default | | ----------------- | -------------------- | ------------------ | ------------------ | | name | String | This should always be `brandmetrics` | - | | waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (recommended) | `false` | | params | Object | | - | -| params.bidders | String[] | An array of bidders which should targeting keys. | `[]` | +| params.bidders | String[] | An array of bidders which should receive targeting keys. | `[]` | | params.scriptId | String | A script- id GUID if the brandmetrics- script should be initialized. | `undefined` | diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 5a8d50e855c..05e6658daa1 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -1,5 +1,5 @@ import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; -import { cloneDeep } from 'lodash'; +import {config} from 'src/config.js'; const VALID_CONFIG = { name: 'brandmetrics', @@ -99,49 +99,34 @@ describe('BrandmetricsRTD module', () => { it('should init and return true', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, USER_CONSENT)).to.equal(true); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); }); it('should init and return true even if bidders is not included', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG, USER_CONSENT)).to.equal(true); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(true); }); - it('should not add any script- tag when script- id is not defined', () => { + it('should init even if script- id is not configured', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG, USER_CONSENT)).to.equal(true); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/undefined.js')).to.equal(false); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/null.js')).to.equal(false); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); }); - it('should not add any script- tag when there is no TCF- consent', () => { - expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_TCF_CONSENT)).to.equal(true); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); + it('should not init when there is no TCF- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_TCF_CONSENT)).to.equal(false); }); - it('should not add any script- tag when there is no usp- consent', () => { - expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(true); - expect(scriptTagExists('https://cdn.brandmetrics.com/survey/script/00000000-0000-0000-0000-000000000000.js')).to.equal(false); + it('should not init when there is no usp- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); }); }); describe('getBidRequestData', () => { - it('should set targeting keys for specified bidders only', () => { - const conf = { - adUnits: [{ - bids: [{ - bidder: 'ozone' - }] - }, { - bids: [{ - bidder: 'unspecified' - }] - }] - }; - - brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { - expect(conf.adUnits[0].bids[0].params.customData[0].targeting.mockTargetKey).to.equal('mockMeasurementId'); - expect(conf.adUnits[1].bids[0].params).to.be.undefined; + it('should set targeting keys for specified bidders', () => { + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.mockTargetKey).to.equal('mockMeasurementId') + }) }, VALID_CONFIG); mockSurveyLoaded({ @@ -159,21 +144,9 @@ describe('getBidRequestData', () => { }); it('should only set targeting keys when the brandmetrics survey- type is "pbjs"', () => { - const conf = { - adUnits: [{ - bids: [{ - bidder: 'ozone' - }] - }, { - bids: [{ - bidder: 'unspecified' - }] - }] - }; - - brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { - expect(conf.adUnits[0].bids[0].params).to.be.undefined; - expect(conf.adUnits[1].bids[0].params).to.be.undefined; + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { + const bidderConfig = config.getBidderConfig() + expect(Object.keys(bidderConfig).length).to.equal(0) }, VALID_CONFIG); mockSurveyLoaded({ @@ -191,23 +164,20 @@ describe('getBidRequestData', () => { }); it('should use a default targeting key name if the brandmetrics- configuration does not include one', () => { - const conf = { - adUnits: [{ - bids: [{ - bidder: 'ozone' - }] - }] - }; - - brandmetricsRTD.brandmetricsSubmodule.getBidRequestData(conf, () => { - expect(conf.adUnits[0].bids[0].params.customData[0].targeting.brandmetrics_survey).to.equal('mockMeasurementId'); + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.mockTargetKey).to.equal('brandmetrics_survfey') + }) }, VALID_CONFIG); mockSurveyLoaded({ available: true, conf: { displayOption: { - type: 'pbjs' + type: 'pbjs', } }, survey: { From 816dad0add3e293a3eda174765a6199cf7b260e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Fri, 14 Jan 2022 11:19:09 +0100 Subject: [PATCH 6/7] Test- logic updates --- .../modules/brandmetricsRtdProvider_spec.js | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 05e6658daa1..3cac5a3d559 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -119,6 +119,10 @@ describe('BrandmetricsRTD module', () => { }); describe('getBidRequestData', () => { + beforeEach(function () { + config.resetConfig() + }) + it('should set targeting keys for specified bidders', () => { brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { const bidderConfig = config.getBidderConfig() @@ -144,11 +148,6 @@ describe('getBidRequestData', () => { }); it('should only set targeting keys when the brandmetrics survey- type is "pbjs"', () => { - brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { - const bidderConfig = config.getBidderConfig() - expect(Object.keys(bidderConfig).length).to.equal(0) - }, VALID_CONFIG); - mockSurveyLoaded({ available: true, conf: { @@ -161,18 +160,13 @@ describe('getBidRequestData', () => { measurementId: 'mockMeasurementId' } }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + const bidderConfig = config.getBidderConfig() + expect(Object.keys(bidderConfig).length).to.equal(0) }); it('should use a default targeting key name if the brandmetrics- configuration does not include one', () => { - brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { - const bidderConfig = config.getBidderConfig() - const expected = VALID_CONFIG.params.bidders - - expected.forEach(exp => { - expect(bidderConfig[exp].ortb2.user.ext.data.mockTargetKey).to.equal('brandmetrics_survfey') - }) - }, VALID_CONFIG); - mockSurveyLoaded({ available: true, conf: { @@ -184,5 +178,14 @@ describe('getBidRequestData', () => { measurementId: 'mockMeasurementId' } }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.brandmetrics_survey).to.equal('mockMeasurementId') + }) }); }); From 43142fde1b0ebd217455784c0da293dab21ee226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Hasselstr=C3=B6m?= Date: Thu, 20 Jan 2022 09:39:44 +0100 Subject: [PATCH 7/7] Add brandmetrics to approved external js- loaders --- src/adloader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/adloader.js b/src/adloader.js index 9039fa14c4c..4e043e362bf 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -8,7 +8,8 @@ const _approvedLoadExternalJSList = [ 'criteo', 'outstream', 'adagio', - 'browsi' + 'browsi', + 'brandmetrics' ] /**