From 6121b4ed320ce8da712deeaa5fac75dc4d131e81 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 25 Aug 2019 14:59:21 +0300 Subject: [PATCH 01/40] real time data module, browsi sub module for real time data, new hook bidsBackCallback, fix for config unsubscribe --- modules/browsiProvider.js | 215 +++++++++++++++++++++++ modules/realTimeData.md | 30 ++++ modules/realTimeDataModule.js | 191 ++++++++++++++++++++ src/auction.js | 52 +++--- src/config.js | 5 +- test/spec/modules/realTimeModule_spec.js | 158 +++++++++++++++++ 6 files changed, 627 insertions(+), 24 deletions(-) create mode 100644 modules/browsiProvider.js create mode 100644 modules/realTimeData.md create mode 100644 modules/realTimeDataModule.js create mode 100644 test/spec/modules/realTimeModule_spec.js diff --git a/modules/browsiProvider.js b/modules/browsiProvider.js new file mode 100644 index 00000000000..0ae39fe66dd --- /dev/null +++ b/modules/browsiProvider.js @@ -0,0 +1,215 @@ +/** + * This module adds browsi provider to the eal time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch predictions from browsi server + * The module will place browsi bootstrap script on page + * @module modules/browsiProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} siteKey + * @property {string} pubKey + * @property {string} url + * @property {string} keyName + */ + +import {config} from '../src/config.js'; +import * as utils from '../src/utils'; +import {submodule} from '../src/hook'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +/** @type {ModuleParams} */ +let _moduleParams = {}; + +export let _resolvePromise = null; +const _waitForData = new Promise(resolve => _resolvePromise = resolve); + +/** + * add browsi script to page + * @param {string} bptUrl + */ +export function addBrowsiTag(bptUrl) { + let script = document.createElement('script'); + script.async = true; + script.setAttribute('data-sitekey', _moduleParams.siteKey); + script.setAttribute('data-pubkey', _moduleParams.pubKey); + script.setAttribute('src', bptUrl); + document.head.appendChild(script); + return script; +} + +/** + * collect required data from page + * send data to browsi server to get predictions + */ +function collectData() { + const win = window.top; + let historicalData = null; + try { + historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) + } catch (e) { + utils.logError('unable to parse __brtd'); + } + + let predictorData = { + ...{ + sk: _moduleParams.siteKey, + sw: (win.screen && win.screen.width) || -1, + sh: (win.screen && win.screen.height) || -1, + }, + ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), + ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), + ...(document.referrer ? {r: document.referrer} : {}), + ...(document.title ? {at: document.title} : {}) + }; + getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); +} + +/** + * filter server data according to adUnits received + * @param {adUnit[]} adUnits + * @return {Object} filtered data + * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} + */ +function sendDataToModule(adUnits) { + return _waitForData + .then((_predictions) => { + if (!_predictions) { + resolve({}) + } + const slots = getAllSlots(); + if (!slots) { + resolve({}) + } + let dataToResolve = adUnits.reduce((rp, cau) => { + const adUnitCode = cau && cau.code; + if (!adUnitCode) { return rp } + const predictionData = _predictions[adUnitCode]; + if (!predictionData) { return rp } + + if (predictionData.p) { + if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { + return rp; + } + rp[adUnitCode] = getKVObject(predictionData.p); + } + return rp; + }, {}); + return (dataToResolve); + }) + .catch(() => { + return ({}); + }); +} + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +function getAllSlots() { + return utils.isGptPubadsDefined && window.googletag.pubads().getSlots(); +} +/** + * get prediction and return valid object for key value set + * @param {number} p + * @return {Object} key:value + */ +function getKVObject(p) { + const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); + let prObject = {}; + prObject[(_moduleParams['keyName'].toString())] = prValue.toString(); + return prObject; +} +/** + * check if placement id matches one of given ad units + * @param {number} id placement id + * @param {Object[]} allSlots google slots on page + * @param {string[]} whitelist ad units + * @return {boolean} + */ +export function isIdMatchingAdUnit(id, allSlots, whitelist) { + if (!whitelist || !whitelist.length) { + return true; + } + const slot = allSlots.filter(s => s.getSlotElementId() === id); + const slotAdUnits = slot.map(s => s.getAdUnitPath()); + return slotAdUnits.some(a => whitelist.indexOf(a) !== -1); +} + +/** + * XMLHttpRequest to get data form browsi server + * @param {string} url server url with query params + */ +function getPredictionsFromServer(url) { + const xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { + try { + var data = JSON.parse(xmlhttp.responseText); + _resolvePromise(data.p); + addBrowsiTag(data.u); + } catch (err) { + utils.logError('unable to parse data'); + } + } + }; + xmlhttp.onloadend = function() { + if (xmlhttp.status === 404) { + _resolvePromise(false); + utils.logError('unable to get prediction data'); + } + }; + xmlhttp.open('GET', url, true); + xmlhttp.onerror = function() { _resolvePromise(false) }; + xmlhttp.send(); +} + +/** + * serialize object and return query params string + * @param {Object} obj + * @return {string} + */ +function serialize(obj) { + var str = []; + for (var p in obj) { + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); + } + } + return str.join('&'); +} + +/** @type {RtdSubmodule} */ +export const browsiSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: 'browsi', + /** + * get data and send back to realTimeData module + * @function + * @param {adUnit[]} adUnits + * @returns {Promise} + */ + getData: sendDataToModule +}; + +export function init(config) { + const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { + _moduleParams = realTimeData.params || {}; + if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url && _moduleParams.keyName && + realTimeData.name && realTimeData.name.toLowerCase() === 'browsi') { + confListener(); + collectData(); + } else { + utils.logError('missing params for Browsi provider'); + } + }); +} + +submodule('realTimeData', browsiSubmodule); +init(config); diff --git a/modules/realTimeData.md b/modules/realTimeData.md new file mode 100644 index 00000000000..0dcdb123dc4 --- /dev/null +++ b/modules/realTimeData.md @@ -0,0 +1,30 @@ +## Real Time Data Configuration Example + +Example showing config using `browsi` sub module +``` + pbjs.setConfig({ + "realTimeData": { + "name": "browsi", + "primary_only": false, + "params": { + "url": "testUrl.com", + "siteKey": "testKey", + "pubKey": "testPub", + "keyName":"bv" + } + } + }); +``` + +Example showing real time data object received form `browsi` sub module +``` +{ + "slotPlacementId":{ + "key":"value", + "key2":"value" + }, + "slotBPlacementId":{ + "dataKey":"dataValue", + } +} +``` diff --git a/modules/realTimeDataModule.js b/modules/realTimeDataModule.js new file mode 100644 index 00000000000..7361d7e8517 --- /dev/null +++ b/modules/realTimeDataModule.js @@ -0,0 +1,191 @@ +/** + * This module adds Real time data support to prebid.js + * @module modules/realTimeData + */ + +/** + * @interface RtdSubmodule + */ + +/** + * @function + * @summary return teal time data + * @name RtdSubmodule#getData + * @param {adUnit[]} adUnits + * @return {Promise} + */ + +/** + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#name + * @type {string} + */ + +/** + * @interface ModuleConfig + */ + +/** + * @property + * @summary sub module name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary timeout + * @name ModuleConfig#timeout + * @type {number} + */ + +/** + * @property + * @summary params for provide (sub module) + * @name ModuleConfig#params + * @type {Object} + */ + +/** + * @property + * @summary primary ad server only + * @name ModuleConfig#primary_only + * @type {boolean} + */ + +import {getGlobal} from '../src/prebidGlobal'; +import {config} from '../src/config.js'; +import {targeting} from '../src/targeting'; +import {getHook, module} from '../src/hook'; +import * as utils from '../src/utils'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; +/** @type {RtdSubmodule[]} */ +let subModules = []; +/** @type {RtdSubmodule | null} */ +let _subModule = null; +/** @type {ModuleConfig} */ +let _moduleConfig; + +/** + * enable submodule in User ID + * @param {RtdSubmodule} submodule + */ +export function attachRealTimeDataProvider(submodule) { + subModules.push(submodule); +} +/** + * get registered sub module + * @returns {RtdSubmodule} + */ +function getSubModule() { + if (!_moduleConfig.name) { + return null; + } + const subModule = subModules.filter(m => m.name === _moduleConfig.name)[0] || null; + if (!subModule) { + throw new Error('unable to use real time data module without provider'); + } + return subModules.filter(m => m.name === _moduleConfig.name)[0] || null; +} + +export function init(config) { + const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { + if (!realTimeData.name) { + utils.logError('missing parameters for real time module'); + return; + } + confListener(); // unsubscribe config listener + _moduleConfig = realTimeData; + // get submodule + _subModule = getSubModule(); + // delay bidding process only if primary ad server only is false + if (_moduleConfig['primary_only']) { + getHook('bidsBackCallback').before(setTargetsAfterRequestBids); + } else { + getGlobal().requestBids.before(requestBidsHook); + } + }); +} + +/** + * get data from sub module + * @returns {Promise} promise race - will return submodule config or false if time out + */ +function getProviderData(adUnits) { + // promise for timeout + const timeOutPromise = new Promise((resolve) => { + setTimeout(() => { + resolve(false); + }, _moduleConfig.timeout || DEF_TIMEOUT) + }); + + return Promise.race([ + timeOutPromise, + _subModule.getData(adUnits) + ]); +} + +/** + * run hook after bids request and before callback + * get data from provider and set key values to primary ad server + * @param {function} next - next hook function + * @param {AdUnit[]} adUnits received from auction + */ +export function setTargetsAfterRequestBids(next, adUnits) { + getProviderData(adUnits).then(data => { + if (data && Object.keys(data).length) { // utils.isEmpty + setDataForPrimaryAdServer(data); + } + next(adUnits); + } + ); +} + +/** + * run hook before bids request + * get data from provider and set key values to primary ad server & bidders + * @param {function} fn - hook function + * @param {Object} reqBidsConfigObj - request bids object + */ +export function requestBidsHook(fn, reqBidsConfigObj) { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { + if (data && Object.keys(data).length) { + setDataForPrimaryAdServer(data); + addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, data); + } + return fn.call(this, reqBidsConfigObj.adUnits); + }); +} + +/** + * set data to primary ad server + * @param {Object} data - key values to set + */ +function setDataForPrimaryAdServer(data) { + if (!utils.isGptPubadsDefined()) { + utils.logError('window.googletag is not defined on the page'); + return; + } + targeting.setTargetingForGPT(data, null); +} + +/** + * @param {AdUnit[]} adUnits + * @param {Object} data - key values to set + */ +function addIdDataToAdUnitBids(adUnits, data) { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const rd = data[adUnit.code] || {}; + bid = Object.assign(bid, rd); + }); + }); +} + +init(config); +module('realTimeData', attachRealTimeDataProvider); diff --git a/src/auction.js b/src/auction.js index a1e8c33adfb..748affa0201 100644 --- a/src/auction.js +++ b/src/auction.js @@ -154,29 +154,31 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a _auctionEnd = Date.now(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); - try { - if (_callback != null) { - const adUnitCodes = _adUnitCodes; - const bids = _bidsReceived - .filter(utils.bind.call(adUnitsFilter, this, adUnitCodes)) - .reduce(groupByPlacement, {}); - _callback.apply($$PREBID_GLOBAL$$, [bids, timedOut]); - _callback = null; - } - } catch (e) { - utils.logError('Error executing bidsBackHandler', null, e); - } finally { - // Calling timed out bidders - if (timedOutBidders.length) { - adapterManager.callTimedOutBidders(adUnits, timedOutBidders, _timeout); - } - // Only automatically sync if the publisher has not chosen to "enableOverride" - let userSyncConfig = config.getConfig('userSync') || {}; - if (!userSyncConfig.enableOverride) { - // Delay the auto sync by the config delay - syncUsers(userSyncConfig.syncDelay); + bidsBackCallback(_adUnitCodes, function () { + try { + if (_callback != null) { + const adUnitCodes = _adUnitCodes; + const bids = _bidsReceived + .filter(utils.bind.call(adUnitsFilter, this, adUnitCodes)) + .reduce(groupByPlacement, {}); + _callback.apply($$PREBID_GLOBAL$$, [bids, timedOut]); + _callback = null; + } + } catch (e) { + utils.logError('Error executing bidsBackHandler', null, e); + } finally { + // Calling timed out bidders + if (timedOutBidders.length) { + adapterManager.callTimedOutBidders(adUnits, timedOutBidders, _timeout); + } + // Only automatically sync if the publisher has not chosen to "enableOverride" + let userSyncConfig = config.getConfig('userSync') || {}; + if (!userSyncConfig.enableOverride) { + // Delay the auto sync by the config delay + syncUsers(userSyncConfig.syncDelay); + } } - } + }) } } @@ -328,6 +330,12 @@ export const addBidResponse = hook('async', function(adUnitCode, bid) { this.dispatch.call(this.bidderRequest, adUnitCode, bid); }, 'addBidResponse'); +export const bidsBackCallback = hook('async', function (adUnits, callback) { + if (callback) { + callback(); + } +}, 'bidsBackCallback'); + export function auctionCallbacks(auctionDone, auctionInstance) { let outstandingBidsAdded = 0; let allAdapterCalledDone = false; diff --git a/src/config.js b/src/config.js index 7645da18d8f..40831d7de6b 100644 --- a/src/config.js +++ b/src/config.js @@ -306,11 +306,12 @@ export function newConfig() { return; } - listeners.push({ topic, callback }); + const nl = { topic, callback }; + listeners.push(nl); // save and call this function to remove the listener return function unsubscribe() { - listeners.splice(listeners.indexOf(listener), 1); + listeners.splice(listeners.indexOf(nl), 1); }; } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js new file mode 100644 index 00000000000..34ae0c49aa9 --- /dev/null +++ b/test/spec/modules/realTimeModule_spec.js @@ -0,0 +1,158 @@ +import { + init, + requestBidsHook, + attachRealTimeDataProvider, + setTargetsAfterRequestBids +} from 'modules/realTimeDataModule'; +import { + init as browsiInit, + addBrowsiTag, + isIdMatchingAdUnit +} from 'modules/browsiProvider'; +import {config} from 'src/config'; +import {browsiSubmodule, _resolvePromise} from 'modules/browsiProvider'; +import {makeSlot} from '../integration/faker/googletag'; + +let expect = require('chai').expect; + +describe('Real time module', function() { + const conf = { + 'realTimeData': { + 'name': 'browsi', + 'primary_only': false, + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + } + }; + + const predictions = + { + 'browsiAd_2': { + 'w': [ + '/57778053/Browsi_Demo_Low', + '/57778053/Browsi_Demo_300x250' + ], + 'p': 0.07 + }, + 'browsiAd_1': { + 'w': [], + 'p': 0.06 + }, + 'browsiAd_3': { + 'w': [], + 'p': 0.53 + }, + 'browsiAd_4': { + 'w': [ + '/57778053/Browsi_Demo' + ], + 'p': 0.85 + } + }; + + function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: {}, native: {}}, + sizes: [[300, 200], [300, 600]], + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] + }; + } + + function createSlots() { + const slot1 = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + const slot2 = makeSlot({code: '/57778053/Browsi_Demo_Low', divId: 'browsiAd_2'}); + return [ + slot1, + slot2 + ]; + } + + before(function() { + + }); + + describe('Real time module with browsi provider', function() { + afterEach(function () { + $$PREBID_GLOBAL$$.requestBids.removeAll(); + }); + + it('check module using bidsBackCallback', function () { + let adUnits1 = [getAdUnitMock('browsiAd_1')]; + _resolvePromise(predictions); + attachRealTimeDataProvider(browsiSubmodule); + init(config); + browsiInit(config); + config.setConfig(conf); + + // set slot + const slots = createSlots(); + window.googletag.pubads().setSlots(slots); + + setTargetsAfterRequestBids(afterBidHook, {adUnits: adUnits1}); + function afterBidHook() { + slots.map(s => { + let targeting = []; + s.getTargeting().map(value => { + console.log('in slots map'); + let temp = []; + temp.push(Object.keys(value).toString()); + temp.push(value[Object.keys(value)]); + targeting.push(temp); + }); + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + }); + } + }); + + it('check module using requestBidsHook', function () { + let adUnits1 = [getAdUnitMock('browsiAd_1')]; + + // set slot + const slotsB = createSlots(); + window.googletag.pubads().setSlots(slotsB); + + requestBidsHook(afterBidHook, {adUnits: adUnits1}); + function afterBidHook(adUnits) { + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.property('bv'); + }); + }); + + slotsB.map(s => { + let targeting = []; + s.getTargeting().map(value => { + let temp = []; + temp.push(Object.keys(value).toString()); + temp.push(value[Object.keys(value)]); + targeting.push(temp); + }); + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + }); + } + }); + + it('check browsi sub module', function () { + const script = addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + + const slots = createSlots(); + const test1 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_Low']); // false + const test4 = isIdMatchingAdUnit('browsiAd_1', slots, []); // true + + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }) + }); +}); From 1a80b14d1e43bc7b2b30081423dc3388d2f281ad Mon Sep 17 00:00:00 2001 From: omerdotan Date: Mon, 9 Sep 2019 12:18:32 +0300 Subject: [PATCH 02/40] change timeout&primary ad server only to auctionDelay update docs --- modules/{ => rtdModules}/browsiProvider.js | 6 ++--- .../index.js} | 22 +++++++-------- modules/rtdModules/provider.md | 27 +++++++++++++++++++ modules/{ => rtdModules}/realTimeData.md | 2 +- test/spec/modules/realTimeModule_spec.js | 8 +++--- 5 files changed, 45 insertions(+), 20 deletions(-) rename modules/{ => rtdModules}/browsiProvider.js (97%) rename modules/{realTimeDataModule.js => rtdModules/index.js} (90%) create mode 100644 modules/rtdModules/provider.md rename modules/{ => rtdModules}/realTimeData.md (94%) diff --git a/modules/browsiProvider.js b/modules/rtdModules/browsiProvider.js similarity index 97% rename from modules/browsiProvider.js rename to modules/rtdModules/browsiProvider.js index 0ae39fe66dd..d582390f1b7 100644 --- a/modules/browsiProvider.js +++ b/modules/rtdModules/browsiProvider.js @@ -15,9 +15,9 @@ * @property {string} keyName */ -import {config} from '../src/config.js'; -import * as utils from '../src/utils'; -import {submodule} from '../src/hook'; +import {config} from '../../src/config.js'; +import * as utils from '../../src/utils'; +import {submodule} from '../../src/hook'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/realTimeDataModule.js b/modules/rtdModules/index.js similarity index 90% rename from modules/realTimeDataModule.js rename to modules/rtdModules/index.js index 7361d7e8517..2bd89e9bf4e 100644 --- a/modules/realTimeDataModule.js +++ b/modules/rtdModules/index.js @@ -54,16 +54,14 @@ * @type {boolean} */ -import {getGlobal} from '../src/prebidGlobal'; -import {config} from '../src/config.js'; -import {targeting} from '../src/targeting'; -import {getHook, module} from '../src/hook'; -import * as utils from '../src/utils'; +import {getGlobal} from '../../src/prebidGlobal'; +import {config} from '../../src/config.js'; +import {targeting} from '../../src/targeting'; +import {getHook, module} from '../../src/hook'; +import * as utils from '../../src/utils'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ let subModules = []; /** @type {RtdSubmodule | null} */ @@ -95,7 +93,7 @@ function getSubModule() { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.name) { + if (!realTimeData.name || typeof (realTimeData.auctionDelay) == 'undefined') { utils.logError('missing parameters for real time module'); return; } @@ -121,7 +119,7 @@ function getProviderData(adUnits) { const timeOutPromise = new Promise((resolve) => { setTimeout(() => { resolve(false); - }, _moduleConfig.timeout || DEF_TIMEOUT) + }, _moduleConfig.auctionDelay) }); return Promise.race([ @@ -180,10 +178,10 @@ function setDataForPrimaryAdServer(data) { */ function addIdDataToAdUnitBids(adUnits, data) { adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { + adUnit.bids = adUnit.bids.map(bid => { const rd = data[adUnit.code] || {}; - bid = Object.assign(bid, rd); - }); + return Object.assign(bid, rd); + }) }); } diff --git a/modules/rtdModules/provider.md b/modules/rtdModules/provider.md new file mode 100644 index 00000000000..c7c296b2b67 --- /dev/null +++ b/modules/rtdModules/provider.md @@ -0,0 +1,27 @@ +New provider must include the following: + +1. sub module object: +``` +export const subModuleName = { + name: String, + getData: Function +}; +``` + +2. Promise that returns the real time data according to this structure: +``` +{ + "slotPlacementId":{ + "key":"value", + "key2":"value" + }, + "slotBPlacementId":{ + "dataKey":"dataValue", + } +} +``` + +3. Hook to Real Time Data module: +``` +submodule('realTimeData', subModuleName); +``` diff --git a/modules/realTimeData.md b/modules/rtdModules/realTimeData.md similarity index 94% rename from modules/realTimeData.md rename to modules/rtdModules/realTimeData.md index 0dcdb123dc4..ee0d5a86bda 100644 --- a/modules/realTimeData.md +++ b/modules/rtdModules/realTimeData.md @@ -5,7 +5,7 @@ Example showing config using `browsi` sub module pbjs.setConfig({ "realTimeData": { "name": "browsi", - "primary_only": false, + "auctionDelay": 1000, "params": { "url": "testUrl.com", "siteKey": "testKey", diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 34ae0c49aa9..f093af9f467 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -3,14 +3,14 @@ import { requestBidsHook, attachRealTimeDataProvider, setTargetsAfterRequestBids -} from 'modules/realTimeDataModule'; +} from 'modules/rtdModules/index'; import { init as browsiInit, addBrowsiTag, isIdMatchingAdUnit -} from 'modules/browsiProvider'; +} from 'modules/rtdModules/browsiProvider'; import {config} from 'src/config'; -import {browsiSubmodule, _resolvePromise} from 'modules/browsiProvider'; +import {browsiSubmodule, _resolvePromise} from 'modules/rtdModules/browsiProvider'; import {makeSlot} from '../integration/faker/googletag'; let expect = require('chai').expect; @@ -19,7 +19,7 @@ describe('Real time module', function() { const conf = { 'realTimeData': { 'name': 'browsi', - 'primary_only': false, + 'auctionDelay': 1500, 'params': { 'url': 'testUrl.com', 'siteKey': 'testKey', From 3b85815b92f7320814f1d7d55d92bc4aebc94f93 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 18 Sep 2019 16:03:42 +0300 Subject: [PATCH 03/40] support multiple providers --- ...browsiProvider.js => browsiRtdProvider.js} | 48 +++++----- modules/{rtdModules => rtdModule}/index.js | 88 ++++++++++--------- modules/{rtdModules => rtdModule}/provider.md | 0 modules/rtdModule/realTimeData.md | 32 +++++++ modules/rtdModules/realTimeData.md | 30 ------- test/spec/modules/realTimeModule_spec.js | 76 ++++++++++++---- 6 files changed, 166 insertions(+), 108 deletions(-) rename modules/{rtdModules/browsiProvider.js => browsiRtdProvider.js} (82%) rename modules/{rtdModules => rtdModule}/index.js (69%) rename modules/{rtdModules => rtdModule}/provider.md (100%) create mode 100644 modules/rtdModule/realTimeData.md delete mode 100644 modules/rtdModules/realTimeData.md diff --git a/modules/rtdModules/browsiProvider.js b/modules/browsiRtdProvider.js similarity index 82% rename from modules/rtdModules/browsiProvider.js rename to modules/browsiRtdProvider.js index d582390f1b7..ca87af17887 100644 --- a/modules/rtdModules/browsiProvider.js +++ b/modules/browsiRtdProvider.js @@ -12,12 +12,12 @@ * @property {string} siteKey * @property {string} pubKey * @property {string} url - * @property {string} keyName + * @property {?string} keyName */ -import {config} from '../../src/config.js'; -import * as utils from '../../src/utils'; -import {submodule} from '../../src/hook'; +import {config} from '../src/config.js'; +import * as utils from '../src/utils'; +import {submodule} from '../src/hook'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -47,6 +47,7 @@ export function addBrowsiTag(bptUrl) { */ function collectData() { const win = window.top; + const doc = win.document; let historicalData = null; try { historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) @@ -59,6 +60,7 @@ function collectData() { sk: _moduleParams.siteKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, + url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`) }, ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), @@ -76,13 +78,14 @@ function collectData() { */ function sendDataToModule(adUnits) { return _waitForData - .then((_predictions) => { - if (!_predictions) { - resolve({}) + .then((_predictionsData) => { + const _predictions = _predictionsData.p; + if (!_predictions || !Object.keys(_predictions).length) { + return ({}) } const slots = getAllSlots(); if (!slots) { - resolve({}) + return ({}) } let dataToResolve = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; @@ -94,13 +97,13 @@ function sendDataToModule(adUnits) { if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { return rp; } - rp[adUnitCode] = getKVObject(predictionData.p); + rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); } return rp; }, {}); return (dataToResolve); }) - .catch(() => { + .catch((e) => { return ({}); }); } @@ -115,12 +118,13 @@ function getAllSlots() { /** * get prediction and return valid object for key value set * @param {number} p + * @param {string?} keyName * @return {Object} key:value */ -function getKVObject(p) { +function getKVObject(p, keyName) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[(_moduleParams['keyName'].toString())] = prValue.toString(); + prObject[((_moduleParams['keyName'] || keyName).toString())] = prValue.toString(); return prObject; } /** @@ -149,7 +153,7 @@ function getPredictionsFromServer(url) { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { try { var data = JSON.parse(xmlhttp.responseText); - _resolvePromise(data.p); + _resolvePromise({p: data.p, kn: data.kn}); addBrowsiTag(data.u); } catch (err) { utils.logError('unable to parse data'); @@ -158,12 +162,12 @@ function getPredictionsFromServer(url) { }; xmlhttp.onloadend = function() { if (xmlhttp.status === 404) { - _resolvePromise(false); + _resolvePromise({}); utils.logError('unable to get prediction data'); } }; xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { _resolvePromise(false) }; + xmlhttp.onerror = function() { _resolvePromise({}) }; xmlhttp.send(); } @@ -173,8 +177,8 @@ function getPredictionsFromServer(url) { * @return {string} */ function serialize(obj) { - var str = []; - for (var p in obj) { + let str = []; + for (let p in obj) { if (obj.hasOwnProperty(p)) { str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); } @@ -200,9 +204,13 @@ export const browsiSubmodule = { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - _moduleParams = realTimeData.params || {}; - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url && _moduleParams.keyName && - realTimeData.name && realTimeData.name.toLowerCase() === 'browsi') { + try { + _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( + pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + } catch (e) { + _moduleParams = {}; + } + if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { confListener(); collectData(); } else { diff --git a/modules/rtdModules/index.js b/modules/rtdModule/index.js similarity index 69% rename from modules/rtdModules/index.js rename to modules/rtdModule/index.js index 2bd89e9bf4e..e137232e1ac 100644 --- a/modules/rtdModules/index.js +++ b/modules/rtdModule/index.js @@ -35,8 +35,8 @@ /** * @property - * @summary timeout - * @name ModuleConfig#timeout + * @summary auction delay + * @name ModuleConfig#auctionDelay * @type {number} */ @@ -47,13 +47,6 @@ * @type {Object} */ -/** - * @property - * @summary primary ad server only - * @name ModuleConfig#primary_only - * @type {boolean} - */ - import {getGlobal} from '../../src/prebidGlobal'; import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting'; @@ -64,8 +57,6 @@ import * as utils from '../../src/utils'; const MODULE_NAME = 'realTimeData'; /** @type {RtdSubmodule[]} */ let subModules = []; -/** @type {RtdSubmodule | null} */ -let _subModule = null; /** @type {ModuleConfig} */ let _moduleConfig; @@ -76,33 +67,17 @@ let _moduleConfig; export function attachRealTimeDataProvider(submodule) { subModules.push(submodule); } -/** - * get registered sub module - * @returns {RtdSubmodule} - */ -function getSubModule() { - if (!_moduleConfig.name) { - return null; - } - const subModule = subModules.filter(m => m.name === _moduleConfig.name)[0] || null; - if (!subModule) { - throw new Error('unable to use real time data module without provider'); - } - return subModules.filter(m => m.name === _moduleConfig.name)[0] || null; -} export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.name || typeof (realTimeData.auctionDelay) == 'undefined') { + if (!realTimeData.dataProviders || typeof (realTimeData.auctionDelay) == 'undefined') { utils.logError('missing parameters for real time module'); return; } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; - // get submodule - _subModule = getSubModule(); - // delay bidding process only if primary ad server only is false - if (_moduleConfig['primary_only']) { + // delay bidding process only if auctionDelay > 0 + if (!_moduleConfig.auctionDelay > 0) { getHook('bidsBackCallback').before(setTargetsAfterRequestBids); } else { getGlobal().requestBids.before(requestBidsHook); @@ -115,17 +90,18 @@ export function init(config) { * @returns {Promise} promise race - will return submodule config or false if time out */ function getProviderData(adUnits) { + const promises = subModules.map(sm => sm.getData(adUnits)); + // promise for timeout const timeOutPromise = new Promise((resolve) => { setTimeout(() => { - resolve(false); + resolve({}); }, _moduleConfig.auctionDelay) }); - return Promise.race([ - timeOutPromise, - _subModule.getData(adUnits) - ]); + return Promise.all(promises.map(p => { + return Promise.race([p, timeOutPromise]); + })); } /** @@ -136,14 +112,43 @@ function getProviderData(adUnits) { */ export function setTargetsAfterRequestBids(next, adUnits) { getProviderData(adUnits).then(data => { - if (data && Object.keys(data).length) { // utils.isEmpty - setDataForPrimaryAdServer(data); + if (data && Object.keys(data).length) { + const _mergedData = deepMerge(data); + if (Object.keys(_mergedData).length) { + setDataForPrimaryAdServer(_mergedData); + } } next(adUnits); } ); } +/** + * deep merge array of objects + * @param {array} arr - objects array + * @return {Object} merged object + */ +export function deepMerge(arr) { + if (!arr.length) { + return {}; + } + return arr.reduce((merged, obj) => { + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + if (!merged.hasOwnProperty(key)) merged[key] = obj[key]; + else { + // duplicate key - merge values + const dp = obj[key]; + for (let dk in dp) { + if (dp.hasOwnProperty(dk)) merged[key][dk] = dp[dk]; + } + } + } + } + return merged; + }, {}); +} + /** * run hook before bids request * get data from provider and set key values to primary ad server & bidders @@ -153,10 +158,13 @@ export function setTargetsAfterRequestBids(next, adUnits) { export function requestBidsHook(fn, reqBidsConfigObj) { getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { if (data && Object.keys(data).length) { - setDataForPrimaryAdServer(data); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, data); + const _mergedData = deepMerge(data); + if (Object.keys(_mergedData).length) { + setDataForPrimaryAdServer(_mergedData); + addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); + } } - return fn.call(this, reqBidsConfigObj.adUnits); + return fn.call(this, reqBidsConfigObj); }); } diff --git a/modules/rtdModules/provider.md b/modules/rtdModule/provider.md similarity index 100% rename from modules/rtdModules/provider.md rename to modules/rtdModule/provider.md diff --git a/modules/rtdModule/realTimeData.md b/modules/rtdModule/realTimeData.md new file mode 100644 index 00000000000..6fb5f98ce31 --- /dev/null +++ b/modules/rtdModule/realTimeData.md @@ -0,0 +1,32 @@ +## Real Time Data Configuration Example + +Example showing config using `browsi` sub module +``` + pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + dataProviders[{ + "name": "browsi", + "params": { + "url": "testUrl.com", + "siteKey": "testKey", + "pubKey": "testPub", + "keyName":"bv" + } + }] + } + }); +``` + +Example showing real time data object received form `browsi` real time data provider +``` +{ + "slotPlacementId":{ + "key":"value", + "key2":"value" + }, + "slotBPlacementId":{ + "dataKey":"dataValue", + } +} +``` diff --git a/modules/rtdModules/realTimeData.md b/modules/rtdModules/realTimeData.md deleted file mode 100644 index ee0d5a86bda..00000000000 --- a/modules/rtdModules/realTimeData.md +++ /dev/null @@ -1,30 +0,0 @@ -## Real Time Data Configuration Example - -Example showing config using `browsi` sub module -``` - pbjs.setConfig({ - "realTimeData": { - "name": "browsi", - "auctionDelay": 1000, - "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", - "keyName":"bv" - } - } - }); -``` - -Example showing real time data object received form `browsi` sub module -``` -{ - "slotPlacementId":{ - "key":"value", - "key2":"value" - }, - "slotBPlacementId":{ - "dataKey":"dataValue", - } -} -``` diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index f093af9f467..23c99f77a15 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,16 +1,16 @@ import { init, requestBidsHook, - attachRealTimeDataProvider, - setTargetsAfterRequestBids -} from 'modules/rtdModules/index'; + setTargetsAfterRequestBids, + deepMerge +} from 'modules/rtdModule/index'; import { init as browsiInit, addBrowsiTag, - isIdMatchingAdUnit -} from 'modules/rtdModules/browsiProvider'; + isIdMatchingAdUnit, + _resolvePromise +} from 'modules/browsiRtdProvider'; import {config} from 'src/config'; -import {browsiSubmodule, _resolvePromise} from 'modules/rtdModules/browsiProvider'; import {makeSlot} from '../integration/faker/googletag'; let expect = require('chai').expect; @@ -18,19 +18,22 @@ let expect = require('chai').expect; describe('Real time module', function() { const conf = { 'realTimeData': { - 'name': 'browsi', 'auctionDelay': 1500, - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + } }; const predictions = - { + {p: { 'browsiAd_2': { 'w': [ '/57778053/Browsi_Demo_Low', @@ -52,6 +55,7 @@ describe('Real time module', function() { ], 'p': 0.85 } + } }; function getAdUnitMock(code = 'adUnit-code') { @@ -83,22 +87,20 @@ describe('Real time module', function() { it('check module using bidsBackCallback', function () { let adUnits1 = [getAdUnitMock('browsiAd_1')]; - _resolvePromise(predictions); - attachRealTimeDataProvider(browsiSubmodule); init(config); browsiInit(config); config.setConfig(conf); + _resolvePromise(predictions); // set slot const slots = createSlots(); window.googletag.pubads().setSlots(slots); - setTargetsAfterRequestBids(afterBidHook, {adUnits: adUnits1}); + setTargetsAfterRequestBids(afterBidHook, adUnits1); function afterBidHook() { slots.map(s => { let targeting = []; s.getTargeting().map(value => { - console.log('in slots map'); let temp = []; temp.push(Object.keys(value).toString()); temp.push(value[Object.keys(value)]); @@ -137,6 +139,44 @@ describe('Real time module', function() { } }); + it('check object dep merger', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + it('check browsi sub module', function () { const script = addBrowsiTag('scriptUrl.com'); expect(script.getAttribute('data-sitekey')).to.equal('testKey'); From 0cb7b69b5dcc901c704b768d4133dccc0eae36c7 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 16 Oct 2019 10:36:13 +0300 Subject: [PATCH 04/40] change promise to callbacks configure submodule on submodules.json --- modules/.submodules.json | 3 + modules/browsiRtdProvider.js | 100 +++++++++++++++-------- modules/rtdModule/index.js | 43 ++++++---- modules/rtdModule/provider.md | 6 +- modules/rtdModule/realTimeData.md | 4 +- test/spec/modules/realTimeModule_spec.js | 4 +- 6 files changed, 102 insertions(+), 58 deletions(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index c0e30037660..a4b4164abf8 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,5 +7,8 @@ "adpod": [ "freeWheelAdserverVideo", "dfpAdServerVideo" + ], + "rtdModule": [ + "browsiRtdProvider" ] } diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index ca87af17887..63452ea979b 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,6 +13,7 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName + * @property {number} auctionDelay */ import {config} from '../src/config.js'; @@ -23,9 +24,10 @@ import {submodule} from '../src/hook'; const MODULE_NAME = 'realTimeData'; /** @type {ModuleParams} */ let _moduleParams = {}; - -export let _resolvePromise = null; -const _waitForData = new Promise(resolve => _resolvePromise = resolve); +/** @type {null|Object} */ +let _data = null; +/** @type {null | function} */ +let _dataReadyCallback = null; /** * add browsi script to page @@ -36,6 +38,8 @@ export function addBrowsiTag(bptUrl) { script.async = true; script.setAttribute('data-sitekey', _moduleParams.siteKey); script.setAttribute('data-pubkey', _moduleParams.pubKey); + script.setAttribute('prebidbpt', 'true'); + script.setAttribute('id', 'browsi-tag'); script.setAttribute('src', bptUrl); document.head.appendChild(script); return script; @@ -48,9 +52,9 @@ export function addBrowsiTag(bptUrl) { function collectData() { const win = window.top; const doc = win.document; - let historicalData = null; + let browsiData = null; try { - historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) + browsiData = utils.getDataFromLocalStorage('__brtd'); } catch (e) { utils.logError('unable to parse __brtd'); } @@ -60,34 +64,56 @@ function collectData() { sk: _moduleParams.siteKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, - url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`) + url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`), }, - ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), - ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), + ...(browsiData ? {us: browsiData} : {us: '{}'}), ...(document.referrer ? {r: document.referrer} : {}), ...(document.title ? {at: document.title} : {}) }; - getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); + getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); +} + +export function setData(data) { + _data = data; + + if (typeof _dataReadyCallback === 'function') { + _dataReadyCallback(_data); + _dataReadyCallback = null; + } +} + +/** + * wait for data from server + * call callback when data is ready + * @param {function} callback + */ +function waitForData(callback) { + if (_data) { + _dataReadyCallback = null; + callback(_data); + } else { + _dataReadyCallback = callback; + } } /** * filter server data according to adUnits received + * call callback (onDone) when data is ready * @param {adUnit[]} adUnits - * @return {Object} filtered data - * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} + * @param {function} onDone callback function */ -function sendDataToModule(adUnits) { - return _waitForData - .then((_predictionsData) => { +function sendDataToModule(adUnits, onDone) { + try { + waitForData(_predictionsData => { const _predictions = _predictionsData.p; if (!_predictions || !Object.keys(_predictions).length) { - return ({}) + onDone({}); } const slots = getAllSlots(); if (!slots) { - return ({}) + onDone({}); } - let dataToResolve = adUnits.reduce((rp, cau) => { + let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const predictionData = _predictions[adUnitCode]; @@ -101,11 +127,11 @@ function sendDataToModule(adUnits) { } return rp; }, {}); - return (dataToResolve); - }) - .catch((e) => { - return ({}); + onDone(dataToReturn); }); + } catch (e) { + onDone({}); + } } /** @@ -152,38 +178,41 @@ function getPredictionsFromServer(url) { xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { try { - var data = JSON.parse(xmlhttp.responseText); - _resolvePromise({p: data.p, kn: data.kn}); + const data = JSON.parse(xmlhttp.responseText); + if (data && data.p && data.kn) { + setData({p: data.p, kn: data.kn}); + } else { + setData({}); + } addBrowsiTag(data.u); } catch (err) { utils.logError('unable to parse data'); } + } else if (xmlhttp.readyState === 4 && xmlhttp.status === 204) { + // unrecognized site key + setData({}); } }; xmlhttp.onloadend = function() { if (xmlhttp.status === 404) { - _resolvePromise({}); + setData({}); utils.logError('unable to get prediction data'); } }; xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { _resolvePromise({}) }; + xmlhttp.onerror = function() { setData({}) }; xmlhttp.send(); } /** * serialize object and return query params string - * @param {Object} obj + * @param {Object} data * @return {string} */ -function serialize(obj) { - let str = []; - for (let p in obj) { - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); - } - } - return str.join('&'); +function toUrlParams(data) { + return Object.keys(data) + .map(key => key + '=' + encodeURIComponent(data[key])) + .join('&'); } /** @type {RtdSubmodule} */ @@ -197,7 +226,7 @@ export const browsiSubmodule = { * get data and send back to realTimeData module * @function * @param {adUnit[]} adUnits - * @returns {Promise} + * @param {function} onDone */ getData: sendDataToModule }; @@ -207,6 +236,7 @@ export function init(config) { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + _moduleParams.auctionDelay = realTimeData.auctionDelay; } catch (e) { _moduleParams = {}; } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index e137232e1ac..4c95dc244f2 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -87,21 +87,33 @@ export function init(config) { /** * get data from sub module - * @returns {Promise} promise race - will return submodule config or false if time out + * @param {AdUnit[]} adUnits received from auction + * @param {function} callback callback function on data received */ -function getProviderData(adUnits) { - const promises = subModules.map(sm => sm.getData(adUnits)); - - // promise for timeout - const timeOutPromise = new Promise((resolve) => { - setTimeout(() => { - resolve({}); - }, _moduleConfig.auctionDelay) +function getProviderData(adUnits, callback) { + const callbackExpected = subModules.length; + let dataReceived = []; + let processDone = false; + const dataWaitTimeout = setTimeout(() => { + processDone = true; + callback(dataReceived); + }, _moduleConfig.auctionDelay); + + subModules.forEach(sm => { + sm.getData(adUnits, onDataReceived); }); - return Promise.all(promises.map(p => { - return Promise.race([p, timeOutPromise]); - })); + function onDataReceived(data) { + if (processDone) { + return + } + dataReceived.push(data); + if (dataReceived.length === callbackExpected) { + processDone = true; + clearTimeout(dataWaitTimeout); + callback(dataReceived); + } + } } /** @@ -111,7 +123,7 @@ function getProviderData(adUnits) { * @param {AdUnit[]} adUnits received from auction */ export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits).then(data => { + getProviderData(adUnits, (data) => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { @@ -119,8 +131,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { } } next(adUnits); - } - ); + }); } /** @@ -156,7 +167,7 @@ export function deepMerge(arr) { * @param {Object} reqBidsConfigObj - request bids object */ export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index c7c296b2b67..c3fb94a15cc 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -8,14 +8,14 @@ export const subModuleName = { }; ``` -2. Promise that returns the real time data according to this structure: +2. Function that returns the real time data according to the following structure: ``` { - "slotPlacementId":{ + "adUnitCode":{ "key":"value", "key2":"value" }, - "slotBPlacementId":{ + "adUnirCode2":{ "dataKey":"dataValue", } } diff --git a/modules/rtdModule/realTimeData.md b/modules/rtdModule/realTimeData.md index 6fb5f98ce31..b2859098b1f 100644 --- a/modules/rtdModule/realTimeData.md +++ b/modules/rtdModule/realTimeData.md @@ -21,11 +21,11 @@ Example showing config using `browsi` sub module Example showing real time data object received form `browsi` real time data provider ``` { - "slotPlacementId":{ + "adUnitCode":{ "key":"value", "key2":"value" }, - "slotBPlacementId":{ + "adUnitCode2":{ "dataKey":"dataValue", } } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 23c99f77a15..91e9eb2fbd8 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -8,7 +8,7 @@ import { init as browsiInit, addBrowsiTag, isIdMatchingAdUnit, - _resolvePromise + setData } from 'modules/browsiRtdProvider'; import {config} from 'src/config'; import {makeSlot} from '../integration/faker/googletag'; @@ -90,7 +90,7 @@ describe('Real time module', function() { init(config); browsiInit(config); config.setConfig(conf); - _resolvePromise(predictions); + setData(predictions); // set slot const slots = createSlots(); From 090813420b5181d010ed75ef4038a2993923dffb Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 3 Nov 2019 16:38:58 +0200 Subject: [PATCH 05/40] bug fixes --- modules/browsiRtdProvider.js | 6 +++--- modules/rtdModule/index.js | 9 ++++++--- modules/rtdModule/provider.md | 2 +- test/spec/modules/realTimeModule_spec.js | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 63452ea979b..b536f618e35 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -107,11 +107,11 @@ function sendDataToModule(adUnits, onDone) { waitForData(_predictionsData => { const _predictions = _predictionsData.p; if (!_predictions || !Object.keys(_predictions).length) { - onDone({}); + return onDone({}); } const slots = getAllSlots(); if (!slots) { - onDone({}); + return onDone({}); } let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; @@ -127,7 +127,7 @@ function sendDataToModule(adUnits, onDone) { } return rp; }, {}); - onDone(dataToReturn); + return onDone(dataToReturn); }); } catch (e) { onDone({}); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 4c95dc244f2..9f0209d6113 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -70,12 +70,15 @@ export function attachRealTimeDataProvider(submodule) { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.dataProviders || typeof (realTimeData.auctionDelay) == 'undefined') { + if (!realTimeData.dataProviders) { utils.logError('missing parameters for real time module'); return; } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; + if (typeof (_moduleConfig.auctionDelay) == 'undefined') { + _moduleConfig.auctionDelay = 0; + } // delay bidding process only if auctionDelay > 0 if (!_moduleConfig.auctionDelay > 0) { getHook('bidsBackCallback').before(setTargetsAfterRequestBids); @@ -140,7 +143,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { * @return {Object} merged object */ export function deepMerge(arr) { - if (!arr.length) { + if (!Array.isArray(arr) || !arr.length) { return {}; } return arr.reduce((merged, obj) => { @@ -199,7 +202,7 @@ function addIdDataToAdUnitBids(adUnits, data) { adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.map(bid => { const rd = data[adUnit.code] || {}; - return Object.assign(bid, rd); + return Object.assign(bid, {realTimeData: rd}); }) }); } diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index c3fb94a15cc..fb42e7188d3 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -15,7 +15,7 @@ export const subModuleName = { "key":"value", "key2":"value" }, - "adUnirCode2":{ + "adUnitCode2":{ "dataKey":"dataValue", } } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 91e9eb2fbd8..92ccae86e80 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -120,9 +120,9 @@ describe('Real time module', function() { requestBidsHook(afterBidHook, {adUnits: adUnits1}); function afterBidHook(adUnits) { - adUnits.forEach(unit => { + adUnits.adUnits.forEach(unit => { unit.bids.forEach(bid => { - expect(bid).to.have.property('bv'); + expect(bid.realTimeData).to.have.property('bv'); }); }); From cf4c5a9f502346b781e48c36bf54d2d40994021d Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 6 Nov 2019 14:09:10 +0200 Subject: [PATCH 06/40] use Prebid ajax --- modules/browsiRtdProvider.js | 48 ++++++++++++++++++------------------ modules/rtdModule/index.js | 8 +++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index b536f618e35..795c9c86f1e 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -19,6 +19,7 @@ import {config} from '../src/config.js'; import * as utils from '../src/utils'; import {submodule} from '../src/hook'; +import {ajax} from '../src/ajax'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -174,34 +175,33 @@ export function isIdMatchingAdUnit(id, allSlots, whitelist) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - const xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - try { - const data = JSON.parse(xmlhttp.responseText); - if (data && data.p && data.kn) { - setData({p: data.p, kn: data.kn}); - } else { + ajax(url, + { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.p && data.kn) { + setData({p: data.p, kn: data.kn}); + } else { + setData({}); + } + addBrowsiTag(data.u); + } catch (err) { + utils.logError('unable to parse data'); + setData({}) + } + } else if (req.status === 204) { + // unrecognized site key setData({}); } - addBrowsiTag(data.u); - } catch (err) { - utils.logError('unable to parse data'); + }, + error: function () { + setData({}); + utils.logError('unable to get prediction data'); } - } else if (xmlhttp.readyState === 4 && xmlhttp.status === 204) { - // unrecognized site key - setData({}); - } - }; - xmlhttp.onloadend = function() { - if (xmlhttp.status === 404) { - setData({}); - utils.logError('unable to get prediction data'); } - }; - xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { setData({}) }; - xmlhttp.send(); + ); } /** diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 9f0209d6113..e7ba364c0e5 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -9,10 +9,10 @@ /** * @function - * @summary return teal time data + * @summary return real time data * @name RtdSubmodule#getData - * @param {adUnit[]} adUnits - * @return {Promise} + * @param {AdUnit[]} adUnits + * @param {function} onDone */ /** @@ -76,7 +76,7 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; - if (typeof (_moduleConfig.auctionDelay) == 'undefined') { + if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } // delay bidding process only if auctionDelay > 0 From 7beeee381bde8c9abddaad89f02dafb0092d757d Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 6 Nov 2019 17:34:54 +0200 Subject: [PATCH 07/40] tests fix --- test/spec/modules/realTimeModule_spec.js | 59 +++++++++++------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 92ccae86e80..807781d5a9c 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -18,7 +18,7 @@ let expect = require('chai').expect; describe('Real time module', function() { const conf = { 'realTimeData': { - 'auctionDelay': 1500, + 'auctionDelay': 250, dataProviders: [{ 'name': 'browsi', 'params': { @@ -69,17 +69,9 @@ describe('Real time module', function() { function createSlots() { const slot1 = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); - const slot2 = makeSlot({code: '/57778053/Browsi_Demo_Low', divId: 'browsiAd_2'}); - return [ - slot1, - slot2 - ]; + return [slot1]; } - before(function() { - - }); - describe('Real time module with browsi provider', function() { afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); @@ -87,6 +79,7 @@ describe('Real time module', function() { it('check module using bidsBackCallback', function () { let adUnits1 = [getAdUnitMock('browsiAd_1')]; + let targeting = []; init(config); browsiInit(config); config.setConfig(conf); @@ -96,50 +89,52 @@ describe('Real time module', function() { const slots = createSlots(); window.googletag.pubads().setSlots(slots); - setTargetsAfterRequestBids(afterBidHook, adUnits1); function afterBidHook() { slots.map(s => { - let targeting = []; + targeting = []; s.getTargeting().map(value => { - let temp = []; - temp.push(Object.keys(value).toString()); - temp.push(value[Object.keys(value)]); - targeting.push(temp); + targeting.push(Object.keys(value).toString()); }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); }); } + setTargetsAfterRequestBids(afterBidHook, adUnits1, true); + + setTimeout(() => { + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + }, 200); }); it('check module using requestBidsHook', function () { + console.log('entrance', new Date().getMinutes() + ':' + new Date().getSeconds()); let adUnits1 = [getAdUnitMock('browsiAd_1')]; + let targeting = []; + let dataReceived = null; // set slot const slotsB = createSlots(); window.googletag.pubads().setSlots(slotsB); - requestBidsHook(afterBidHook, {adUnits: adUnits1}); - function afterBidHook(adUnits) { - adUnits.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - + function afterBidHook(data) { + dataReceived = data; slotsB.map(s => { - let targeting = []; + targeting = []; s.getTargeting().map(value => { - let temp = []; - temp.push(Object.keys(value).toString()); - temp.push(value[Object.keys(value)]); - targeting.push(temp); + targeting.push(Object.keys(value).toString()); }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); }); } + requestBidsHook(afterBidHook, {adUnits: adUnits1}); + setTimeout(() => { + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + dataReceived.adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid.realTimeData).to.have.property('bv'); + }); + }); + }, 200); }); - it('check object dep merger', function () { + it('check object deep merge', function () { const obj1 = { id1: { key: 'value', From 60aaeaa4314e3c30744b4700f2effa34ba2eb5b9 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 8 Dec 2019 16:28:46 +0200 Subject: [PATCH 08/40] browsi real time data provider improvements --- modules/browsiRtdProvider.js | 79 +++++++++++++++++++----- modules/rtdModule/index.js | 11 +++- src/auction.js | 2 +- test/spec/modules/realTimeModule_spec.js | 8 +-- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 795c9c86f1e..8cd84a1718f 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,16 +13,19 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {number} auctionDelay + * @property {?number} auctionDelay + * @property {?number} timeout */ import {config} from '../src/config.js'; import * as utils from '../src/utils'; import {submodule} from '../src/hook'; -import {ajax} from '../src/ajax'; +import {ajaxBuilder} from '../src/ajax'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ @@ -32,16 +35,20 @@ let _dataReadyCallback = null; /** * add browsi script to page - * @param {string} bptUrl + * @param {Object} data */ -export function addBrowsiTag(bptUrl) { +export function addBrowsiTag(data) { let script = document.createElement('script'); script.async = true; script.setAttribute('data-sitekey', _moduleParams.siteKey); script.setAttribute('data-pubkey', _moduleParams.pubKey); script.setAttribute('prebidbpt', 'true'); script.setAttribute('id', 'browsi-tag'); - script.setAttribute('src', bptUrl); + script.setAttribute('src', data.u); + script.prebidData = utils.deepClone(data); + if (_moduleParams.keyName) { + script.prebidData.kn = _moduleParams.keyName; + } document.head.appendChild(script); return script; } @@ -111,17 +118,20 @@ function sendDataToModule(adUnits, onDone) { return onDone({}); } const slots = getAllSlots(); - if (!slots) { + if (!slots || !slots.length) { return onDone({}); } let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } - const predictionData = _predictions[adUnitCode]; + const adSlot = getSlotById(adUnitCode); + if (!adSlot) { return rp } + const macroId = getMacroId(_predictionsData.plidm, adUnitCode, adSlot); + const predictionData = _predictions[macroId]; if (!predictionData) { return rp } if (predictionData.p) { - if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { + if (!isIdMatchingAdUnit(adUnitCode, adSlot, predictionData.w)) { return rp; } rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); @@ -157,17 +167,53 @@ function getKVObject(p, keyName) { /** * check if placement id matches one of given ad units * @param {number} id placement id - * @param {Object[]} allSlots google slots on page + * @param {Object} slot google slot * @param {string[]} whitelist ad units * @return {boolean} */ -export function isIdMatchingAdUnit(id, allSlots, whitelist) { +export function isIdMatchingAdUnit(id, slot, whitelist) { if (!whitelist || !whitelist.length) { return true; } - const slot = allSlots.filter(s => s.getSlotElementId() === id); - const slotAdUnits = slot.map(s => s.getAdUnitPath()); - return slotAdUnits.some(a => whitelist.indexOf(a) !== -1); + const slotAdUnits = slot.getAdUnitPath(); + return whitelist.indexOf(slotAdUnits) !== -1; +} + +/** + * get GPT slot by placement id + * @param {string} id placement id + * @return {?Object} + */ +function getSlotById(id) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return slots.filter(s => s.getSlotElementId() === id)[0] || null; +} + +/** + * generate id according to macro script + * @param {string} macro replacement macro + * @param {string} id placement id + * @param {Object} slot google slot + * @return {?Object} + */ +function getMacroId(macro, id, slot) { + if (macro) { + try { + const macroString = macro + .replace(//g, `${id}`) + .replace(//g, `${slot.getAdUnitPath()}`) + .replace(//g, (match, p1) => { + return (p1 && slot.getTargeting(p1).join('_')) || 'NA'; + }); + return eval(macroString);// eslint-disable-line no-eval + } catch (e) { + utils.logError(`failed to evaluate: ${macro}`); + } + } + return id; } /** @@ -175,6 +221,8 @@ export function isIdMatchingAdUnit(id, allSlots, whitelist) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { + let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + ajax(url, { success: function (response, req) { @@ -182,11 +230,11 @@ function getPredictionsFromServer(url) { try { const data = JSON.parse(response); if (data && data.p && data.kn) { - setData({p: data.p, kn: data.kn}); + setData({p: data.p, kn: data.kn, plidm: data.plidm}); } else { setData({}); } - addBrowsiTag(data.u); + addBrowsiTag(data); } catch (err) { utils.logError('unable to parse data'); setData({}) @@ -237,6 +285,7 @@ export function init(config) { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; _moduleParams.auctionDelay = realTimeData.auctionDelay; + _moduleParams.timeout = realTimeData.timeout; } catch (e) { _moduleParams = {}; } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index e7ba364c0e5..3136d20ab13 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -47,6 +47,13 @@ * @type {Object} */ +/** + * @property + * @summary timeout (if no auction dealy) + * @name ModuleConfig#timeout + * @type {number} + */ + import {getGlobal} from '../../src/prebidGlobal'; import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting'; @@ -55,6 +62,8 @@ import * as utils from '../../src/utils'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ let subModules = []; /** @type {ModuleConfig} */ @@ -100,7 +109,7 @@ function getProviderData(adUnits, callback) { const dataWaitTimeout = setTimeout(() => { processDone = true; callback(dataReceived); - }, _moduleConfig.auctionDelay); + }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); subModules.forEach(sm => { sm.getData(adUnits, onDataReceived); diff --git a/src/auction.js b/src/auction.js index 748affa0201..3a47d33ea1b 100644 --- a/src/auction.js +++ b/src/auction.js @@ -154,7 +154,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a _auctionEnd = Date.now(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); - bidsBackCallback(_adUnitCodes, function () { + bidsBackCallback(_adUnits, function () { try { if (_callback != null) { const adUnitCodes = _adUnitCodes; diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 807781d5a9c..aa80cccd61b 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -179,10 +179,10 @@ describe('Real time module', function() { expect(script.async).to.equal(true); const slots = createSlots(); - const test1 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit('browsiAd_1', slots, []); // true + const test1 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_300x250']); // true + const test2 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_Low']); // false + const test4 = isIdMatchingAdUnit('browsiAd_1', slots[0], []); // true expect(test1).to.equal(true); expect(test2).to.equal(true); From 0e06e6f86d6994d3748d5e5902ebc456c7adabcc Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 25 Aug 2019 14:59:21 +0300 Subject: [PATCH 09/40] real time data module, browsi sub module for real time data, new hook bidsBackCallback, fix for config unsubscribe --- modules/browsiProvider.js | 215 +++++++++++++++++++++++ modules/realTimeData.md | 30 ++++ modules/realTimeDataModule.js | 191 ++++++++++++++++++++ test/spec/modules/realTimeModule_spec.js | 131 +++++--------- 4 files changed, 484 insertions(+), 83 deletions(-) create mode 100644 modules/browsiProvider.js create mode 100644 modules/realTimeData.md create mode 100644 modules/realTimeDataModule.js diff --git a/modules/browsiProvider.js b/modules/browsiProvider.js new file mode 100644 index 00000000000..0ae39fe66dd --- /dev/null +++ b/modules/browsiProvider.js @@ -0,0 +1,215 @@ +/** + * This module adds browsi provider to the eal time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch predictions from browsi server + * The module will place browsi bootstrap script on page + * @module modules/browsiProvider + * @requires module:modules/realTimeData + */ + +/** + * @typedef {Object} ModuleParams + * @property {string} siteKey + * @property {string} pubKey + * @property {string} url + * @property {string} keyName + */ + +import {config} from '../src/config.js'; +import * as utils from '../src/utils'; +import {submodule} from '../src/hook'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +/** @type {ModuleParams} */ +let _moduleParams = {}; + +export let _resolvePromise = null; +const _waitForData = new Promise(resolve => _resolvePromise = resolve); + +/** + * add browsi script to page + * @param {string} bptUrl + */ +export function addBrowsiTag(bptUrl) { + let script = document.createElement('script'); + script.async = true; + script.setAttribute('data-sitekey', _moduleParams.siteKey); + script.setAttribute('data-pubkey', _moduleParams.pubKey); + script.setAttribute('src', bptUrl); + document.head.appendChild(script); + return script; +} + +/** + * collect required data from page + * send data to browsi server to get predictions + */ +function collectData() { + const win = window.top; + let historicalData = null; + try { + historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) + } catch (e) { + utils.logError('unable to parse __brtd'); + } + + let predictorData = { + ...{ + sk: _moduleParams.siteKey, + sw: (win.screen && win.screen.width) || -1, + sh: (win.screen && win.screen.height) || -1, + }, + ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), + ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), + ...(document.referrer ? {r: document.referrer} : {}), + ...(document.title ? {at: document.title} : {}) + }; + getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); +} + +/** + * filter server data according to adUnits received + * @param {adUnit[]} adUnits + * @return {Object} filtered data + * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} + */ +function sendDataToModule(adUnits) { + return _waitForData + .then((_predictions) => { + if (!_predictions) { + resolve({}) + } + const slots = getAllSlots(); + if (!slots) { + resolve({}) + } + let dataToResolve = adUnits.reduce((rp, cau) => { + const adUnitCode = cau && cau.code; + if (!adUnitCode) { return rp } + const predictionData = _predictions[adUnitCode]; + if (!predictionData) { return rp } + + if (predictionData.p) { + if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { + return rp; + } + rp[adUnitCode] = getKVObject(predictionData.p); + } + return rp; + }, {}); + return (dataToResolve); + }) + .catch(() => { + return ({}); + }); +} + +/** + * get all slots on page + * @return {Object[]} slot GoogleTag slots + */ +function getAllSlots() { + return utils.isGptPubadsDefined && window.googletag.pubads().getSlots(); +} +/** + * get prediction and return valid object for key value set + * @param {number} p + * @return {Object} key:value + */ +function getKVObject(p) { + const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); + let prObject = {}; + prObject[(_moduleParams['keyName'].toString())] = prValue.toString(); + return prObject; +} +/** + * check if placement id matches one of given ad units + * @param {number} id placement id + * @param {Object[]} allSlots google slots on page + * @param {string[]} whitelist ad units + * @return {boolean} + */ +export function isIdMatchingAdUnit(id, allSlots, whitelist) { + if (!whitelist || !whitelist.length) { + return true; + } + const slot = allSlots.filter(s => s.getSlotElementId() === id); + const slotAdUnits = slot.map(s => s.getAdUnitPath()); + return slotAdUnits.some(a => whitelist.indexOf(a) !== -1); +} + +/** + * XMLHttpRequest to get data form browsi server + * @param {string} url server url with query params + */ +function getPredictionsFromServer(url) { + const xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { + try { + var data = JSON.parse(xmlhttp.responseText); + _resolvePromise(data.p); + addBrowsiTag(data.u); + } catch (err) { + utils.logError('unable to parse data'); + } + } + }; + xmlhttp.onloadend = function() { + if (xmlhttp.status === 404) { + _resolvePromise(false); + utils.logError('unable to get prediction data'); + } + }; + xmlhttp.open('GET', url, true); + xmlhttp.onerror = function() { _resolvePromise(false) }; + xmlhttp.send(); +} + +/** + * serialize object and return query params string + * @param {Object} obj + * @return {string} + */ +function serialize(obj) { + var str = []; + for (var p in obj) { + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); + } + } + return str.join('&'); +} + +/** @type {RtdSubmodule} */ +export const browsiSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: 'browsi', + /** + * get data and send back to realTimeData module + * @function + * @param {adUnit[]} adUnits + * @returns {Promise} + */ + getData: sendDataToModule +}; + +export function init(config) { + const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { + _moduleParams = realTimeData.params || {}; + if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url && _moduleParams.keyName && + realTimeData.name && realTimeData.name.toLowerCase() === 'browsi') { + confListener(); + collectData(); + } else { + utils.logError('missing params for Browsi provider'); + } + }); +} + +submodule('realTimeData', browsiSubmodule); +init(config); diff --git a/modules/realTimeData.md b/modules/realTimeData.md new file mode 100644 index 00000000000..0dcdb123dc4 --- /dev/null +++ b/modules/realTimeData.md @@ -0,0 +1,30 @@ +## Real Time Data Configuration Example + +Example showing config using `browsi` sub module +``` + pbjs.setConfig({ + "realTimeData": { + "name": "browsi", + "primary_only": false, + "params": { + "url": "testUrl.com", + "siteKey": "testKey", + "pubKey": "testPub", + "keyName":"bv" + } + } + }); +``` + +Example showing real time data object received form `browsi` sub module +``` +{ + "slotPlacementId":{ + "key":"value", + "key2":"value" + }, + "slotBPlacementId":{ + "dataKey":"dataValue", + } +} +``` diff --git a/modules/realTimeDataModule.js b/modules/realTimeDataModule.js new file mode 100644 index 00000000000..7361d7e8517 --- /dev/null +++ b/modules/realTimeDataModule.js @@ -0,0 +1,191 @@ +/** + * This module adds Real time data support to prebid.js + * @module modules/realTimeData + */ + +/** + * @interface RtdSubmodule + */ + +/** + * @function + * @summary return teal time data + * @name RtdSubmodule#getData + * @param {adUnit[]} adUnits + * @return {Promise} + */ + +/** + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#name + * @type {string} + */ + +/** + * @interface ModuleConfig + */ + +/** + * @property + * @summary sub module name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary timeout + * @name ModuleConfig#timeout + * @type {number} + */ + +/** + * @property + * @summary params for provide (sub module) + * @name ModuleConfig#params + * @type {Object} + */ + +/** + * @property + * @summary primary ad server only + * @name ModuleConfig#primary_only + * @type {boolean} + */ + +import {getGlobal} from '../src/prebidGlobal'; +import {config} from '../src/config.js'; +import {targeting} from '../src/targeting'; +import {getHook, module} from '../src/hook'; +import * as utils from '../src/utils'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; +/** @type {RtdSubmodule[]} */ +let subModules = []; +/** @type {RtdSubmodule | null} */ +let _subModule = null; +/** @type {ModuleConfig} */ +let _moduleConfig; + +/** + * enable submodule in User ID + * @param {RtdSubmodule} submodule + */ +export function attachRealTimeDataProvider(submodule) { + subModules.push(submodule); +} +/** + * get registered sub module + * @returns {RtdSubmodule} + */ +function getSubModule() { + if (!_moduleConfig.name) { + return null; + } + const subModule = subModules.filter(m => m.name === _moduleConfig.name)[0] || null; + if (!subModule) { + throw new Error('unable to use real time data module without provider'); + } + return subModules.filter(m => m.name === _moduleConfig.name)[0] || null; +} + +export function init(config) { + const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { + if (!realTimeData.name) { + utils.logError('missing parameters for real time module'); + return; + } + confListener(); // unsubscribe config listener + _moduleConfig = realTimeData; + // get submodule + _subModule = getSubModule(); + // delay bidding process only if primary ad server only is false + if (_moduleConfig['primary_only']) { + getHook('bidsBackCallback').before(setTargetsAfterRequestBids); + } else { + getGlobal().requestBids.before(requestBidsHook); + } + }); +} + +/** + * get data from sub module + * @returns {Promise} promise race - will return submodule config or false if time out + */ +function getProviderData(adUnits) { + // promise for timeout + const timeOutPromise = new Promise((resolve) => { + setTimeout(() => { + resolve(false); + }, _moduleConfig.timeout || DEF_TIMEOUT) + }); + + return Promise.race([ + timeOutPromise, + _subModule.getData(adUnits) + ]); +} + +/** + * run hook after bids request and before callback + * get data from provider and set key values to primary ad server + * @param {function} next - next hook function + * @param {AdUnit[]} adUnits received from auction + */ +export function setTargetsAfterRequestBids(next, adUnits) { + getProviderData(adUnits).then(data => { + if (data && Object.keys(data).length) { // utils.isEmpty + setDataForPrimaryAdServer(data); + } + next(adUnits); + } + ); +} + +/** + * run hook before bids request + * get data from provider and set key values to primary ad server & bidders + * @param {function} fn - hook function + * @param {Object} reqBidsConfigObj - request bids object + */ +export function requestBidsHook(fn, reqBidsConfigObj) { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { + if (data && Object.keys(data).length) { + setDataForPrimaryAdServer(data); + addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, data); + } + return fn.call(this, reqBidsConfigObj.adUnits); + }); +} + +/** + * set data to primary ad server + * @param {Object} data - key values to set + */ +function setDataForPrimaryAdServer(data) { + if (!utils.isGptPubadsDefined()) { + utils.logError('window.googletag is not defined on the page'); + return; + } + targeting.setTargetingForGPT(data, null); +} + +/** + * @param {AdUnit[]} adUnits + * @param {Object} data - key values to set + */ +function addIdDataToAdUnitBids(adUnits, data) { + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const rd = data[adUnit.code] || {}; + bid = Object.assign(bid, rd); + }); + }); +} + +init(config); +module('realTimeData', attachRealTimeDataProvider); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 807781d5a9c..34ae0c49aa9 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,16 +1,16 @@ import { init, requestBidsHook, - setTargetsAfterRequestBids, - deepMerge -} from 'modules/rtdModule/index'; + attachRealTimeDataProvider, + setTargetsAfterRequestBids +} from 'modules/realTimeDataModule'; import { init as browsiInit, addBrowsiTag, - isIdMatchingAdUnit, - setData -} from 'modules/browsiRtdProvider'; + isIdMatchingAdUnit +} from 'modules/browsiProvider'; import {config} from 'src/config'; +import {browsiSubmodule, _resolvePromise} from 'modules/browsiProvider'; import {makeSlot} from '../integration/faker/googletag'; let expect = require('chai').expect; @@ -18,22 +18,19 @@ let expect = require('chai').expect; describe('Real time module', function() { const conf = { 'realTimeData': { - 'auctionDelay': 250, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }] - + 'name': 'browsi', + 'primary_only': false, + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } } }; const predictions = - {p: { + { 'browsiAd_2': { 'w': [ '/57778053/Browsi_Demo_Low', @@ -55,7 +52,6 @@ describe('Real time module', function() { ], 'p': 0.85 } - } }; function getAdUnitMock(code = 'adUnit-code') { @@ -69,9 +65,17 @@ describe('Real time module', function() { function createSlots() { const slot1 = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); - return [slot1]; + const slot2 = makeSlot({code: '/57778053/Browsi_Demo_Low', divId: 'browsiAd_2'}); + return [ + slot1, + slot2 + ]; } + before(function() { + + }); + describe('Real time module with browsi provider', function() { afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); @@ -79,97 +83,58 @@ describe('Real time module', function() { it('check module using bidsBackCallback', function () { let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; + _resolvePromise(predictions); + attachRealTimeDataProvider(browsiSubmodule); init(config); browsiInit(config); config.setConfig(conf); - setData(predictions); // set slot const slots = createSlots(); window.googletag.pubads().setSlots(slots); + setTargetsAfterRequestBids(afterBidHook, {adUnits: adUnits1}); function afterBidHook() { slots.map(s => { - targeting = []; + let targeting = []; s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); + console.log('in slots map'); + let temp = []; + temp.push(Object.keys(value).toString()); + temp.push(value[Object.keys(value)]); + targeting.push(temp); }); + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); }); } - setTargetsAfterRequestBids(afterBidHook, adUnits1, true); - - setTimeout(() => { - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - }, 200); }); it('check module using requestBidsHook', function () { - console.log('entrance', new Date().getMinutes() + ':' + new Date().getSeconds()); let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - let dataReceived = null; // set slot const slotsB = createSlots(); window.googletag.pubads().setSlots(slotsB); - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - } requestBidsHook(afterBidHook, {adUnits: adUnits1}); - setTimeout(() => { - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - dataReceived.adUnits.forEach(unit => { + function afterBidHook(adUnits) { + adUnits.forEach(unit => { unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); + expect(bid).to.have.property('bv'); }); }); - }, 200); - }); - it('check object deep merge', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; - - const merged = deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); + slotsB.map(s => { + let targeting = []; + s.getTargeting().map(value => { + let temp = []; + temp.push(Object.keys(value).toString()); + temp.push(value[Object.keys(value)]); + targeting.push(temp); + }); + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + }); + } }); it('check browsi sub module', function () { From e9312c71af32082a78016afd460aca8874a558f4 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Mon, 9 Sep 2019 12:18:32 +0300 Subject: [PATCH 10/40] change timeout&primary ad server only to auctionDelay update docs --- modules/{ => rtdModules}/browsiProvider.js | 6 ++--- .../index.js} | 22 +++++++-------- modules/rtdModules/provider.md | 27 +++++++++++++++++++ modules/{ => rtdModules}/realTimeData.md | 2 +- test/spec/modules/realTimeModule_spec.js | 8 +++--- 5 files changed, 45 insertions(+), 20 deletions(-) rename modules/{ => rtdModules}/browsiProvider.js (97%) rename modules/{realTimeDataModule.js => rtdModules/index.js} (90%) create mode 100644 modules/rtdModules/provider.md rename modules/{ => rtdModules}/realTimeData.md (94%) diff --git a/modules/browsiProvider.js b/modules/rtdModules/browsiProvider.js similarity index 97% rename from modules/browsiProvider.js rename to modules/rtdModules/browsiProvider.js index 0ae39fe66dd..d582390f1b7 100644 --- a/modules/browsiProvider.js +++ b/modules/rtdModules/browsiProvider.js @@ -15,9 +15,9 @@ * @property {string} keyName */ -import {config} from '../src/config.js'; -import * as utils from '../src/utils'; -import {submodule} from '../src/hook'; +import {config} from '../../src/config.js'; +import * as utils from '../../src/utils'; +import {submodule} from '../../src/hook'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/realTimeDataModule.js b/modules/rtdModules/index.js similarity index 90% rename from modules/realTimeDataModule.js rename to modules/rtdModules/index.js index 7361d7e8517..2bd89e9bf4e 100644 --- a/modules/realTimeDataModule.js +++ b/modules/rtdModules/index.js @@ -54,16 +54,14 @@ * @type {boolean} */ -import {getGlobal} from '../src/prebidGlobal'; -import {config} from '../src/config.js'; -import {targeting} from '../src/targeting'; -import {getHook, module} from '../src/hook'; -import * as utils from '../src/utils'; +import {getGlobal} from '../../src/prebidGlobal'; +import {config} from '../../src/config.js'; +import {targeting} from '../../src/targeting'; +import {getHook, module} from '../../src/hook'; +import * as utils from '../../src/utils'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ let subModules = []; /** @type {RtdSubmodule | null} */ @@ -95,7 +93,7 @@ function getSubModule() { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.name) { + if (!realTimeData.name || typeof (realTimeData.auctionDelay) == 'undefined') { utils.logError('missing parameters for real time module'); return; } @@ -121,7 +119,7 @@ function getProviderData(adUnits) { const timeOutPromise = new Promise((resolve) => { setTimeout(() => { resolve(false); - }, _moduleConfig.timeout || DEF_TIMEOUT) + }, _moduleConfig.auctionDelay) }); return Promise.race([ @@ -180,10 +178,10 @@ function setDataForPrimaryAdServer(data) { */ function addIdDataToAdUnitBids(adUnits, data) { adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { + adUnit.bids = adUnit.bids.map(bid => { const rd = data[adUnit.code] || {}; - bid = Object.assign(bid, rd); - }); + return Object.assign(bid, rd); + }) }); } diff --git a/modules/rtdModules/provider.md b/modules/rtdModules/provider.md new file mode 100644 index 00000000000..c7c296b2b67 --- /dev/null +++ b/modules/rtdModules/provider.md @@ -0,0 +1,27 @@ +New provider must include the following: + +1. sub module object: +``` +export const subModuleName = { + name: String, + getData: Function +}; +``` + +2. Promise that returns the real time data according to this structure: +``` +{ + "slotPlacementId":{ + "key":"value", + "key2":"value" + }, + "slotBPlacementId":{ + "dataKey":"dataValue", + } +} +``` + +3. Hook to Real Time Data module: +``` +submodule('realTimeData', subModuleName); +``` diff --git a/modules/realTimeData.md b/modules/rtdModules/realTimeData.md similarity index 94% rename from modules/realTimeData.md rename to modules/rtdModules/realTimeData.md index 0dcdb123dc4..ee0d5a86bda 100644 --- a/modules/realTimeData.md +++ b/modules/rtdModules/realTimeData.md @@ -5,7 +5,7 @@ Example showing config using `browsi` sub module pbjs.setConfig({ "realTimeData": { "name": "browsi", - "primary_only": false, + "auctionDelay": 1000, "params": { "url": "testUrl.com", "siteKey": "testKey", diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 34ae0c49aa9..f093af9f467 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -3,14 +3,14 @@ import { requestBidsHook, attachRealTimeDataProvider, setTargetsAfterRequestBids -} from 'modules/realTimeDataModule'; +} from 'modules/rtdModules/index'; import { init as browsiInit, addBrowsiTag, isIdMatchingAdUnit -} from 'modules/browsiProvider'; +} from 'modules/rtdModules/browsiProvider'; import {config} from 'src/config'; -import {browsiSubmodule, _resolvePromise} from 'modules/browsiProvider'; +import {browsiSubmodule, _resolvePromise} from 'modules/rtdModules/browsiProvider'; import {makeSlot} from '../integration/faker/googletag'; let expect = require('chai').expect; @@ -19,7 +19,7 @@ describe('Real time module', function() { const conf = { 'realTimeData': { 'name': 'browsi', - 'primary_only': false, + 'auctionDelay': 1500, 'params': { 'url': 'testUrl.com', 'siteKey': 'testKey', From c0901fe83bef93475263816a42be34aaefdea691 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 18 Sep 2019 16:03:42 +0300 Subject: [PATCH 11/40] support multiple providers --- modules/browsiRtdProvider.js | 130 ++++++-------- modules/rtdModule/index.js | 58 +++--- modules/rtdModule/provider.md | 6 +- modules/rtdModules/browsiProvider.js | 215 ----------------------- modules/rtdModules/index.js | 189 -------------------- modules/rtdModules/provider.md | 27 --- modules/rtdModules/realTimeData.md | 30 ---- test/spec/modules/realTimeModule_spec.js | 76 ++++++-- 8 files changed, 133 insertions(+), 598 deletions(-) delete mode 100644 modules/rtdModules/browsiProvider.js delete mode 100644 modules/rtdModules/index.js delete mode 100644 modules/rtdModules/provider.md delete mode 100644 modules/rtdModules/realTimeData.md diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 795c9c86f1e..ca87af17887 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,22 +13,19 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {number} auctionDelay */ import {config} from '../src/config.js'; import * as utils from '../src/utils'; import {submodule} from '../src/hook'; -import {ajax} from '../src/ajax'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {ModuleParams} */ let _moduleParams = {}; -/** @type {null|Object} */ -let _data = null; -/** @type {null | function} */ -let _dataReadyCallback = null; + +export let _resolvePromise = null; +const _waitForData = new Promise(resolve => _resolvePromise = resolve); /** * add browsi script to page @@ -39,8 +36,6 @@ export function addBrowsiTag(bptUrl) { script.async = true; script.setAttribute('data-sitekey', _moduleParams.siteKey); script.setAttribute('data-pubkey', _moduleParams.pubKey); - script.setAttribute('prebidbpt', 'true'); - script.setAttribute('id', 'browsi-tag'); script.setAttribute('src', bptUrl); document.head.appendChild(script); return script; @@ -53,9 +48,9 @@ export function addBrowsiTag(bptUrl) { function collectData() { const win = window.top; const doc = win.document; - let browsiData = null; + let historicalData = null; try { - browsiData = utils.getDataFromLocalStorage('__brtd'); + historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) } catch (e) { utils.logError('unable to parse __brtd'); } @@ -65,56 +60,34 @@ function collectData() { sk: _moduleParams.siteKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, - url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`), + url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`) }, - ...(browsiData ? {us: browsiData} : {us: '{}'}), + ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), + ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), ...(document.referrer ? {r: document.referrer} : {}), ...(document.title ? {at: document.title} : {}) }; - getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); -} - -export function setData(data) { - _data = data; - - if (typeof _dataReadyCallback === 'function') { - _dataReadyCallback(_data); - _dataReadyCallback = null; - } -} - -/** - * wait for data from server - * call callback when data is ready - * @param {function} callback - */ -function waitForData(callback) { - if (_data) { - _dataReadyCallback = null; - callback(_data); - } else { - _dataReadyCallback = callback; - } + getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); } /** * filter server data according to adUnits received - * call callback (onDone) when data is ready * @param {adUnit[]} adUnits - * @param {function} onDone callback function + * @return {Object} filtered data + * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} */ -function sendDataToModule(adUnits, onDone) { - try { - waitForData(_predictionsData => { +function sendDataToModule(adUnits) { + return _waitForData + .then((_predictionsData) => { const _predictions = _predictionsData.p; if (!_predictions || !Object.keys(_predictions).length) { - return onDone({}); + return ({}) } const slots = getAllSlots(); if (!slots) { - return onDone({}); + return ({}) } - let dataToReturn = adUnits.reduce((rp, cau) => { + let dataToResolve = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const predictionData = _predictions[adUnitCode]; @@ -128,11 +101,11 @@ function sendDataToModule(adUnits, onDone) { } return rp; }, {}); - return onDone(dataToReturn); + return (dataToResolve); + }) + .catch((e) => { + return ({}); }); - } catch (e) { - onDone({}); - } } /** @@ -175,44 +148,42 @@ export function isIdMatchingAdUnit(id, allSlots, whitelist) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - ajax(url, - { - success: function (response, req) { - if (req.status === 200) { - try { - const data = JSON.parse(response); - if (data && data.p && data.kn) { - setData({p: data.p, kn: data.kn}); - } else { - setData({}); - } - addBrowsiTag(data.u); - } catch (err) { - utils.logError('unable to parse data'); - setData({}) - } - } else if (req.status === 204) { - // unrecognized site key - setData({}); - } - }, - error: function () { - setData({}); - utils.logError('unable to get prediction data'); + const xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { + try { + var data = JSON.parse(xmlhttp.responseText); + _resolvePromise({p: data.p, kn: data.kn}); + addBrowsiTag(data.u); + } catch (err) { + utils.logError('unable to parse data'); } } - ); + }; + xmlhttp.onloadend = function() { + if (xmlhttp.status === 404) { + _resolvePromise({}); + utils.logError('unable to get prediction data'); + } + }; + xmlhttp.open('GET', url, true); + xmlhttp.onerror = function() { _resolvePromise({}) }; + xmlhttp.send(); } /** * serialize object and return query params string - * @param {Object} data + * @param {Object} obj * @return {string} */ -function toUrlParams(data) { - return Object.keys(data) - .map(key => key + '=' + encodeURIComponent(data[key])) - .join('&'); +function serialize(obj) { + let str = []; + for (let p in obj) { + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); + } + } + return str.join('&'); } /** @type {RtdSubmodule} */ @@ -226,7 +197,7 @@ export const browsiSubmodule = { * get data and send back to realTimeData module * @function * @param {adUnit[]} adUnits - * @param {function} onDone + * @returns {Promise} */ getData: sendDataToModule }; @@ -236,7 +207,6 @@ export function init(config) { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; - _moduleParams.auctionDelay = realTimeData.auctionDelay; } catch (e) { _moduleParams = {}; } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index e7ba364c0e5..e137232e1ac 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -9,10 +9,10 @@ /** * @function - * @summary return real time data + * @summary return teal time data * @name RtdSubmodule#getData - * @param {AdUnit[]} adUnits - * @param {function} onDone + * @param {adUnit[]} adUnits + * @return {Promise} */ /** @@ -70,15 +70,12 @@ export function attachRealTimeDataProvider(submodule) { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.dataProviders) { + if (!realTimeData.dataProviders || typeof (realTimeData.auctionDelay) == 'undefined') { utils.logError('missing parameters for real time module'); return; } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; - if (typeof (_moduleConfig.auctionDelay) === 'undefined') { - _moduleConfig.auctionDelay = 0; - } // delay bidding process only if auctionDelay > 0 if (!_moduleConfig.auctionDelay > 0) { getHook('bidsBackCallback').before(setTargetsAfterRequestBids); @@ -90,33 +87,21 @@ export function init(config) { /** * get data from sub module - * @param {AdUnit[]} adUnits received from auction - * @param {function} callback callback function on data received + * @returns {Promise} promise race - will return submodule config or false if time out */ -function getProviderData(adUnits, callback) { - const callbackExpected = subModules.length; - let dataReceived = []; - let processDone = false; - const dataWaitTimeout = setTimeout(() => { - processDone = true; - callback(dataReceived); - }, _moduleConfig.auctionDelay); - - subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived); +function getProviderData(adUnits) { + const promises = subModules.map(sm => sm.getData(adUnits)); + + // promise for timeout + const timeOutPromise = new Promise((resolve) => { + setTimeout(() => { + resolve({}); + }, _moduleConfig.auctionDelay) }); - function onDataReceived(data) { - if (processDone) { - return - } - dataReceived.push(data); - if (dataReceived.length === callbackExpected) { - processDone = true; - clearTimeout(dataWaitTimeout); - callback(dataReceived); - } - } + return Promise.all(promises.map(p => { + return Promise.race([p, timeOutPromise]); + })); } /** @@ -126,7 +111,7 @@ function getProviderData(adUnits, callback) { * @param {AdUnit[]} adUnits received from auction */ export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits, (data) => { + getProviderData(adUnits).then(data => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { @@ -134,7 +119,8 @@ export function setTargetsAfterRequestBids(next, adUnits) { } } next(adUnits); - }); + } + ); } /** @@ -143,7 +129,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { * @return {Object} merged object */ export function deepMerge(arr) { - if (!Array.isArray(arr) || !arr.length) { + if (!arr.length) { return {}; } return arr.reduce((merged, obj) => { @@ -170,7 +156,7 @@ export function deepMerge(arr) { * @param {Object} reqBidsConfigObj - request bids object */ export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { @@ -202,7 +188,7 @@ function addIdDataToAdUnitBids(adUnits, data) { adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.map(bid => { const rd = data[adUnit.code] || {}; - return Object.assign(bid, {realTimeData: rd}); + return Object.assign(bid, rd); }) }); } diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index fb42e7188d3..c7c296b2b67 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -8,14 +8,14 @@ export const subModuleName = { }; ``` -2. Function that returns the real time data according to the following structure: +2. Promise that returns the real time data according to this structure: ``` { - "adUnitCode":{ + "slotPlacementId":{ "key":"value", "key2":"value" }, - "adUnitCode2":{ + "slotBPlacementId":{ "dataKey":"dataValue", } } diff --git a/modules/rtdModules/browsiProvider.js b/modules/rtdModules/browsiProvider.js deleted file mode 100644 index d582390f1b7..00000000000 --- a/modules/rtdModules/browsiProvider.js +++ /dev/null @@ -1,215 +0,0 @@ -/** - * This module adds browsi provider to the eal time data module - * The {@link module:modules/realTimeData} module is required - * The module will fetch predictions from browsi server - * The module will place browsi bootstrap script on page - * @module modules/browsiProvider - * @requires module:modules/realTimeData - */ - -/** - * @typedef {Object} ModuleParams - * @property {string} siteKey - * @property {string} pubKey - * @property {string} url - * @property {string} keyName - */ - -import {config} from '../../src/config.js'; -import * as utils from '../../src/utils'; -import {submodule} from '../../src/hook'; - -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; -/** @type {ModuleParams} */ -let _moduleParams = {}; - -export let _resolvePromise = null; -const _waitForData = new Promise(resolve => _resolvePromise = resolve); - -/** - * add browsi script to page - * @param {string} bptUrl - */ -export function addBrowsiTag(bptUrl) { - let script = document.createElement('script'); - script.async = true; - script.setAttribute('data-sitekey', _moduleParams.siteKey); - script.setAttribute('data-pubkey', _moduleParams.pubKey); - script.setAttribute('src', bptUrl); - document.head.appendChild(script); - return script; -} - -/** - * collect required data from page - * send data to browsi server to get predictions - */ -function collectData() { - const win = window.top; - let historicalData = null; - try { - historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) - } catch (e) { - utils.logError('unable to parse __brtd'); - } - - let predictorData = { - ...{ - sk: _moduleParams.siteKey, - sw: (win.screen && win.screen.width) || -1, - sh: (win.screen && win.screen.height) || -1, - }, - ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), - ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), - ...(document.referrer ? {r: document.referrer} : {}), - ...(document.title ? {at: document.title} : {}) - }; - getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); -} - -/** - * filter server data according to adUnits received - * @param {adUnit[]} adUnits - * @return {Object} filtered data - * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} - */ -function sendDataToModule(adUnits) { - return _waitForData - .then((_predictions) => { - if (!_predictions) { - resolve({}) - } - const slots = getAllSlots(); - if (!slots) { - resolve({}) - } - let dataToResolve = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - const predictionData = _predictions[adUnitCode]; - if (!predictionData) { return rp } - - if (predictionData.p) { - if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { - return rp; - } - rp[adUnitCode] = getKVObject(predictionData.p); - } - return rp; - }, {}); - return (dataToResolve); - }) - .catch(() => { - return ({}); - }); -} - -/** - * get all slots on page - * @return {Object[]} slot GoogleTag slots - */ -function getAllSlots() { - return utils.isGptPubadsDefined && window.googletag.pubads().getSlots(); -} -/** - * get prediction and return valid object for key value set - * @param {number} p - * @return {Object} key:value - */ -function getKVObject(p) { - const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); - let prObject = {}; - prObject[(_moduleParams['keyName'].toString())] = prValue.toString(); - return prObject; -} -/** - * check if placement id matches one of given ad units - * @param {number} id placement id - * @param {Object[]} allSlots google slots on page - * @param {string[]} whitelist ad units - * @return {boolean} - */ -export function isIdMatchingAdUnit(id, allSlots, whitelist) { - if (!whitelist || !whitelist.length) { - return true; - } - const slot = allSlots.filter(s => s.getSlotElementId() === id); - const slotAdUnits = slot.map(s => s.getAdUnitPath()); - return slotAdUnits.some(a => whitelist.indexOf(a) !== -1); -} - -/** - * XMLHttpRequest to get data form browsi server - * @param {string} url server url with query params - */ -function getPredictionsFromServer(url) { - const xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - try { - var data = JSON.parse(xmlhttp.responseText); - _resolvePromise(data.p); - addBrowsiTag(data.u); - } catch (err) { - utils.logError('unable to parse data'); - } - } - }; - xmlhttp.onloadend = function() { - if (xmlhttp.status === 404) { - _resolvePromise(false); - utils.logError('unable to get prediction data'); - } - }; - xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { _resolvePromise(false) }; - xmlhttp.send(); -} - -/** - * serialize object and return query params string - * @param {Object} obj - * @return {string} - */ -function serialize(obj) { - var str = []; - for (var p in obj) { - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); - } - } - return str.join('&'); -} - -/** @type {RtdSubmodule} */ -export const browsiSubmodule = { - /** - * used to link submodule with realTimeData - * @type {string} - */ - name: 'browsi', - /** - * get data and send back to realTimeData module - * @function - * @param {adUnit[]} adUnits - * @returns {Promise} - */ - getData: sendDataToModule -}; - -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - _moduleParams = realTimeData.params || {}; - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url && _moduleParams.keyName && - realTimeData.name && realTimeData.name.toLowerCase() === 'browsi') { - confListener(); - collectData(); - } else { - utils.logError('missing params for Browsi provider'); - } - }); -} - -submodule('realTimeData', browsiSubmodule); -init(config); diff --git a/modules/rtdModules/index.js b/modules/rtdModules/index.js deleted file mode 100644 index 2bd89e9bf4e..00000000000 --- a/modules/rtdModules/index.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * This module adds Real time data support to prebid.js - * @module modules/realTimeData - */ - -/** - * @interface RtdSubmodule - */ - -/** - * @function - * @summary return teal time data - * @name RtdSubmodule#getData - * @param {adUnit[]} adUnits - * @return {Promise} - */ - -/** - * @property - * @summary used to link submodule with config - * @name RtdSubmodule#name - * @type {string} - */ - -/** - * @interface ModuleConfig - */ - -/** - * @property - * @summary sub module name - * @name ModuleConfig#name - * @type {string} - */ - -/** - * @property - * @summary timeout - * @name ModuleConfig#timeout - * @type {number} - */ - -/** - * @property - * @summary params for provide (sub module) - * @name ModuleConfig#params - * @type {Object} - */ - -/** - * @property - * @summary primary ad server only - * @name ModuleConfig#primary_only - * @type {boolean} - */ - -import {getGlobal} from '../../src/prebidGlobal'; -import {config} from '../../src/config.js'; -import {targeting} from '../../src/targeting'; -import {getHook, module} from '../../src/hook'; -import * as utils from '../../src/utils'; - -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; -/** @type {RtdSubmodule[]} */ -let subModules = []; -/** @type {RtdSubmodule | null} */ -let _subModule = null; -/** @type {ModuleConfig} */ -let _moduleConfig; - -/** - * enable submodule in User ID - * @param {RtdSubmodule} submodule - */ -export function attachRealTimeDataProvider(submodule) { - subModules.push(submodule); -} -/** - * get registered sub module - * @returns {RtdSubmodule} - */ -function getSubModule() { - if (!_moduleConfig.name) { - return null; - } - const subModule = subModules.filter(m => m.name === _moduleConfig.name)[0] || null; - if (!subModule) { - throw new Error('unable to use real time data module without provider'); - } - return subModules.filter(m => m.name === _moduleConfig.name)[0] || null; -} - -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.name || typeof (realTimeData.auctionDelay) == 'undefined') { - utils.logError('missing parameters for real time module'); - return; - } - confListener(); // unsubscribe config listener - _moduleConfig = realTimeData; - // get submodule - _subModule = getSubModule(); - // delay bidding process only if primary ad server only is false - if (_moduleConfig['primary_only']) { - getHook('bidsBackCallback').before(setTargetsAfterRequestBids); - } else { - getGlobal().requestBids.before(requestBidsHook); - } - }); -} - -/** - * get data from sub module - * @returns {Promise} promise race - will return submodule config or false if time out - */ -function getProviderData(adUnits) { - // promise for timeout - const timeOutPromise = new Promise((resolve) => { - setTimeout(() => { - resolve(false); - }, _moduleConfig.auctionDelay) - }); - - return Promise.race([ - timeOutPromise, - _subModule.getData(adUnits) - ]); -} - -/** - * run hook after bids request and before callback - * get data from provider and set key values to primary ad server - * @param {function} next - next hook function - * @param {AdUnit[]} adUnits received from auction - */ -export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits).then(data => { - if (data && Object.keys(data).length) { // utils.isEmpty - setDataForPrimaryAdServer(data); - } - next(adUnits); - } - ); -} - -/** - * run hook before bids request - * get data from provider and set key values to primary ad server & bidders - * @param {function} fn - hook function - * @param {Object} reqBidsConfigObj - request bids object - */ -export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { - if (data && Object.keys(data).length) { - setDataForPrimaryAdServer(data); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, data); - } - return fn.call(this, reqBidsConfigObj.adUnits); - }); -} - -/** - * set data to primary ad server - * @param {Object} data - key values to set - */ -function setDataForPrimaryAdServer(data) { - if (!utils.isGptPubadsDefined()) { - utils.logError('window.googletag is not defined on the page'); - return; - } - targeting.setTargetingForGPT(data, null); -} - -/** - * @param {AdUnit[]} adUnits - * @param {Object} data - key values to set - */ -function addIdDataToAdUnitBids(adUnits, data) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.map(bid => { - const rd = data[adUnit.code] || {}; - return Object.assign(bid, rd); - }) - }); -} - -init(config); -module('realTimeData', attachRealTimeDataProvider); diff --git a/modules/rtdModules/provider.md b/modules/rtdModules/provider.md deleted file mode 100644 index c7c296b2b67..00000000000 --- a/modules/rtdModules/provider.md +++ /dev/null @@ -1,27 +0,0 @@ -New provider must include the following: - -1. sub module object: -``` -export const subModuleName = { - name: String, - getData: Function -}; -``` - -2. Promise that returns the real time data according to this structure: -``` -{ - "slotPlacementId":{ - "key":"value", - "key2":"value" - }, - "slotBPlacementId":{ - "dataKey":"dataValue", - } -} -``` - -3. Hook to Real Time Data module: -``` -submodule('realTimeData', subModuleName); -``` diff --git a/modules/rtdModules/realTimeData.md b/modules/rtdModules/realTimeData.md deleted file mode 100644 index ee0d5a86bda..00000000000 --- a/modules/rtdModules/realTimeData.md +++ /dev/null @@ -1,30 +0,0 @@ -## Real Time Data Configuration Example - -Example showing config using `browsi` sub module -``` - pbjs.setConfig({ - "realTimeData": { - "name": "browsi", - "auctionDelay": 1000, - "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", - "keyName":"bv" - } - } - }); -``` - -Example showing real time data object received form `browsi` sub module -``` -{ - "slotPlacementId":{ - "key":"value", - "key2":"value" - }, - "slotBPlacementId":{ - "dataKey":"dataValue", - } -} -``` diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index f093af9f467..23c99f77a15 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,16 +1,16 @@ import { init, requestBidsHook, - attachRealTimeDataProvider, - setTargetsAfterRequestBids -} from 'modules/rtdModules/index'; + setTargetsAfterRequestBids, + deepMerge +} from 'modules/rtdModule/index'; import { init as browsiInit, addBrowsiTag, - isIdMatchingAdUnit -} from 'modules/rtdModules/browsiProvider'; + isIdMatchingAdUnit, + _resolvePromise +} from 'modules/browsiRtdProvider'; import {config} from 'src/config'; -import {browsiSubmodule, _resolvePromise} from 'modules/rtdModules/browsiProvider'; import {makeSlot} from '../integration/faker/googletag'; let expect = require('chai').expect; @@ -18,19 +18,22 @@ let expect = require('chai').expect; describe('Real time module', function() { const conf = { 'realTimeData': { - 'name': 'browsi', 'auctionDelay': 1500, - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + } }; const predictions = - { + {p: { 'browsiAd_2': { 'w': [ '/57778053/Browsi_Demo_Low', @@ -52,6 +55,7 @@ describe('Real time module', function() { ], 'p': 0.85 } + } }; function getAdUnitMock(code = 'adUnit-code') { @@ -83,22 +87,20 @@ describe('Real time module', function() { it('check module using bidsBackCallback', function () { let adUnits1 = [getAdUnitMock('browsiAd_1')]; - _resolvePromise(predictions); - attachRealTimeDataProvider(browsiSubmodule); init(config); browsiInit(config); config.setConfig(conf); + _resolvePromise(predictions); // set slot const slots = createSlots(); window.googletag.pubads().setSlots(slots); - setTargetsAfterRequestBids(afterBidHook, {adUnits: adUnits1}); + setTargetsAfterRequestBids(afterBidHook, adUnits1); function afterBidHook() { slots.map(s => { let targeting = []; s.getTargeting().map(value => { - console.log('in slots map'); let temp = []; temp.push(Object.keys(value).toString()); temp.push(value[Object.keys(value)]); @@ -137,6 +139,44 @@ describe('Real time module', function() { } }); + it('check object dep merger', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + it('check browsi sub module', function () { const script = addBrowsiTag('scriptUrl.com'); expect(script.getAttribute('data-sitekey')).to.equal('testKey'); From 398f9229fed1eee903b37cf2b9dc5fb74ef74ff7 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 16 Oct 2019 10:36:13 +0300 Subject: [PATCH 12/40] change promise to callbacks configure submodule on submodules.json --- modules/browsiRtdProvider.js | 100 +++++++++++++++-------- modules/rtdModule/index.js | 43 ++++++---- modules/rtdModule/provider.md | 6 +- test/spec/modules/realTimeModule_spec.js | 4 +- 4 files changed, 97 insertions(+), 56 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index ca87af17887..63452ea979b 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,6 +13,7 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName + * @property {number} auctionDelay */ import {config} from '../src/config.js'; @@ -23,9 +24,10 @@ import {submodule} from '../src/hook'; const MODULE_NAME = 'realTimeData'; /** @type {ModuleParams} */ let _moduleParams = {}; - -export let _resolvePromise = null; -const _waitForData = new Promise(resolve => _resolvePromise = resolve); +/** @type {null|Object} */ +let _data = null; +/** @type {null | function} */ +let _dataReadyCallback = null; /** * add browsi script to page @@ -36,6 +38,8 @@ export function addBrowsiTag(bptUrl) { script.async = true; script.setAttribute('data-sitekey', _moduleParams.siteKey); script.setAttribute('data-pubkey', _moduleParams.pubKey); + script.setAttribute('prebidbpt', 'true'); + script.setAttribute('id', 'browsi-tag'); script.setAttribute('src', bptUrl); document.head.appendChild(script); return script; @@ -48,9 +52,9 @@ export function addBrowsiTag(bptUrl) { function collectData() { const win = window.top; const doc = win.document; - let historicalData = null; + let browsiData = null; try { - historicalData = JSON.parse(utils.getDataFromLocalStorage('__brtd')) + browsiData = utils.getDataFromLocalStorage('__brtd'); } catch (e) { utils.logError('unable to parse __brtd'); } @@ -60,34 +64,56 @@ function collectData() { sk: _moduleParams.siteKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, - url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`) + url: encodeURIComponent(`${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`), }, - ...(historicalData && historicalData.pi ? {pi: historicalData.pi} : {}), - ...(historicalData && historicalData.pv ? {pv: historicalData.pv} : {}), + ...(browsiData ? {us: browsiData} : {us: '{}'}), ...(document.referrer ? {r: document.referrer} : {}), ...(document.title ? {at: document.title} : {}) }; - getPredictionsFromServer(`//${_moduleParams.url}/bpt?${serialize(predictorData)}`); + getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); +} + +export function setData(data) { + _data = data; + + if (typeof _dataReadyCallback === 'function') { + _dataReadyCallback(_data); + _dataReadyCallback = null; + } +} + +/** + * wait for data from server + * call callback when data is ready + * @param {function} callback + */ +function waitForData(callback) { + if (_data) { + _dataReadyCallback = null; + callback(_data); + } else { + _dataReadyCallback = callback; + } } /** * filter server data according to adUnits received + * call callback (onDone) when data is ready * @param {adUnit[]} adUnits - * @return {Object} filtered data - * @type {(function(adUnit[]): Promise<(adUnit | {}) | never | {}>)}} + * @param {function} onDone callback function */ -function sendDataToModule(adUnits) { - return _waitForData - .then((_predictionsData) => { +function sendDataToModule(adUnits, onDone) { + try { + waitForData(_predictionsData => { const _predictions = _predictionsData.p; if (!_predictions || !Object.keys(_predictions).length) { - return ({}) + onDone({}); } const slots = getAllSlots(); if (!slots) { - return ({}) + onDone({}); } - let dataToResolve = adUnits.reduce((rp, cau) => { + let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const predictionData = _predictions[adUnitCode]; @@ -101,11 +127,11 @@ function sendDataToModule(adUnits) { } return rp; }, {}); - return (dataToResolve); - }) - .catch((e) => { - return ({}); + onDone(dataToReturn); }); + } catch (e) { + onDone({}); + } } /** @@ -152,38 +178,41 @@ function getPredictionsFromServer(url) { xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { try { - var data = JSON.parse(xmlhttp.responseText); - _resolvePromise({p: data.p, kn: data.kn}); + const data = JSON.parse(xmlhttp.responseText); + if (data && data.p && data.kn) { + setData({p: data.p, kn: data.kn}); + } else { + setData({}); + } addBrowsiTag(data.u); } catch (err) { utils.logError('unable to parse data'); } + } else if (xmlhttp.readyState === 4 && xmlhttp.status === 204) { + // unrecognized site key + setData({}); } }; xmlhttp.onloadend = function() { if (xmlhttp.status === 404) { - _resolvePromise({}); + setData({}); utils.logError('unable to get prediction data'); } }; xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { _resolvePromise({}) }; + xmlhttp.onerror = function() { setData({}) }; xmlhttp.send(); } /** * serialize object and return query params string - * @param {Object} obj + * @param {Object} data * @return {string} */ -function serialize(obj) { - let str = []; - for (let p in obj) { - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); - } - } - return str.join('&'); +function toUrlParams(data) { + return Object.keys(data) + .map(key => key + '=' + encodeURIComponent(data[key])) + .join('&'); } /** @type {RtdSubmodule} */ @@ -197,7 +226,7 @@ export const browsiSubmodule = { * get data and send back to realTimeData module * @function * @param {adUnit[]} adUnits - * @returns {Promise} + * @param {function} onDone */ getData: sendDataToModule }; @@ -207,6 +236,7 @@ export function init(config) { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + _moduleParams.auctionDelay = realTimeData.auctionDelay; } catch (e) { _moduleParams = {}; } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index e137232e1ac..4c95dc244f2 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -87,21 +87,33 @@ export function init(config) { /** * get data from sub module - * @returns {Promise} promise race - will return submodule config or false if time out + * @param {AdUnit[]} adUnits received from auction + * @param {function} callback callback function on data received */ -function getProviderData(adUnits) { - const promises = subModules.map(sm => sm.getData(adUnits)); - - // promise for timeout - const timeOutPromise = new Promise((resolve) => { - setTimeout(() => { - resolve({}); - }, _moduleConfig.auctionDelay) +function getProviderData(adUnits, callback) { + const callbackExpected = subModules.length; + let dataReceived = []; + let processDone = false; + const dataWaitTimeout = setTimeout(() => { + processDone = true; + callback(dataReceived); + }, _moduleConfig.auctionDelay); + + subModules.forEach(sm => { + sm.getData(adUnits, onDataReceived); }); - return Promise.all(promises.map(p => { - return Promise.race([p, timeOutPromise]); - })); + function onDataReceived(data) { + if (processDone) { + return + } + dataReceived.push(data); + if (dataReceived.length === callbackExpected) { + processDone = true; + clearTimeout(dataWaitTimeout); + callback(dataReceived); + } + } } /** @@ -111,7 +123,7 @@ function getProviderData(adUnits) { * @param {AdUnit[]} adUnits received from auction */ export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits).then(data => { + getProviderData(adUnits, (data) => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { @@ -119,8 +131,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { } } next(adUnits); - } - ); + }); } /** @@ -156,7 +167,7 @@ export function deepMerge(arr) { * @param {Object} reqBidsConfigObj - request bids object */ export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits).then(data => { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { if (data && Object.keys(data).length) { const _mergedData = deepMerge(data); if (Object.keys(_mergedData).length) { diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index c7c296b2b67..c3fb94a15cc 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -8,14 +8,14 @@ export const subModuleName = { }; ``` -2. Promise that returns the real time data according to this structure: +2. Function that returns the real time data according to the following structure: ``` { - "slotPlacementId":{ + "adUnitCode":{ "key":"value", "key2":"value" }, - "slotBPlacementId":{ + "adUnirCode2":{ "dataKey":"dataValue", } } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 23c99f77a15..91e9eb2fbd8 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -8,7 +8,7 @@ import { init as browsiInit, addBrowsiTag, isIdMatchingAdUnit, - _resolvePromise + setData } from 'modules/browsiRtdProvider'; import {config} from 'src/config'; import {makeSlot} from '../integration/faker/googletag'; @@ -90,7 +90,7 @@ describe('Real time module', function() { init(config); browsiInit(config); config.setConfig(conf); - _resolvePromise(predictions); + setData(predictions); // set slot const slots = createSlots(); From b3d0bea57c3e6c5052be068d083a0ae335492b0c Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 3 Nov 2019 16:38:58 +0200 Subject: [PATCH 13/40] bug fixes --- modules/browsiRtdProvider.js | 6 +++--- modules/rtdModule/index.js | 9 ++++++--- modules/rtdModule/provider.md | 2 +- test/spec/modules/realTimeModule_spec.js | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 63452ea979b..b536f618e35 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -107,11 +107,11 @@ function sendDataToModule(adUnits, onDone) { waitForData(_predictionsData => { const _predictions = _predictionsData.p; if (!_predictions || !Object.keys(_predictions).length) { - onDone({}); + return onDone({}); } const slots = getAllSlots(); if (!slots) { - onDone({}); + return onDone({}); } let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; @@ -127,7 +127,7 @@ function sendDataToModule(adUnits, onDone) { } return rp; }, {}); - onDone(dataToReturn); + return onDone(dataToReturn); }); } catch (e) { onDone({}); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 4c95dc244f2..9f0209d6113 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -70,12 +70,15 @@ export function attachRealTimeDataProvider(submodule) { export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - if (!realTimeData.dataProviders || typeof (realTimeData.auctionDelay) == 'undefined') { + if (!realTimeData.dataProviders) { utils.logError('missing parameters for real time module'); return; } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; + if (typeof (_moduleConfig.auctionDelay) == 'undefined') { + _moduleConfig.auctionDelay = 0; + } // delay bidding process only if auctionDelay > 0 if (!_moduleConfig.auctionDelay > 0) { getHook('bidsBackCallback').before(setTargetsAfterRequestBids); @@ -140,7 +143,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { * @return {Object} merged object */ export function deepMerge(arr) { - if (!arr.length) { + if (!Array.isArray(arr) || !arr.length) { return {}; } return arr.reduce((merged, obj) => { @@ -199,7 +202,7 @@ function addIdDataToAdUnitBids(adUnits, data) { adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.map(bid => { const rd = data[adUnit.code] || {}; - return Object.assign(bid, rd); + return Object.assign(bid, {realTimeData: rd}); }) }); } diff --git a/modules/rtdModule/provider.md b/modules/rtdModule/provider.md index c3fb94a15cc..fb42e7188d3 100644 --- a/modules/rtdModule/provider.md +++ b/modules/rtdModule/provider.md @@ -15,7 +15,7 @@ export const subModuleName = { "key":"value", "key2":"value" }, - "adUnirCode2":{ + "adUnitCode2":{ "dataKey":"dataValue", } } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 91e9eb2fbd8..92ccae86e80 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -120,9 +120,9 @@ describe('Real time module', function() { requestBidsHook(afterBidHook, {adUnits: adUnits1}); function afterBidHook(adUnits) { - adUnits.forEach(unit => { + adUnits.adUnits.forEach(unit => { unit.bids.forEach(bid => { - expect(bid).to.have.property('bv'); + expect(bid.realTimeData).to.have.property('bv'); }); }); From a4f2de66b6d2ab20d0eb78ba0a2ad54b9f659989 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 6 Nov 2019 14:09:10 +0200 Subject: [PATCH 14/40] use Prebid ajax --- modules/browsiRtdProvider.js | 48 ++++++++++++++++++------------------ modules/rtdModule/index.js | 8 +++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index b536f618e35..795c9c86f1e 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -19,6 +19,7 @@ import {config} from '../src/config.js'; import * as utils from '../src/utils'; import {submodule} from '../src/hook'; +import {ajax} from '../src/ajax'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -174,34 +175,33 @@ export function isIdMatchingAdUnit(id, allSlots, whitelist) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - const xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = function() { - if (xmlhttp.readyState === 4 && xmlhttp.status === 200) { - try { - const data = JSON.parse(xmlhttp.responseText); - if (data && data.p && data.kn) { - setData({p: data.p, kn: data.kn}); - } else { + ajax(url, + { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + if (data && data.p && data.kn) { + setData({p: data.p, kn: data.kn}); + } else { + setData({}); + } + addBrowsiTag(data.u); + } catch (err) { + utils.logError('unable to parse data'); + setData({}) + } + } else if (req.status === 204) { + // unrecognized site key setData({}); } - addBrowsiTag(data.u); - } catch (err) { - utils.logError('unable to parse data'); + }, + error: function () { + setData({}); + utils.logError('unable to get prediction data'); } - } else if (xmlhttp.readyState === 4 && xmlhttp.status === 204) { - // unrecognized site key - setData({}); - } - }; - xmlhttp.onloadend = function() { - if (xmlhttp.status === 404) { - setData({}); - utils.logError('unable to get prediction data'); } - }; - xmlhttp.open('GET', url, true); - xmlhttp.onerror = function() { setData({}) }; - xmlhttp.send(); + ); } /** diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 9f0209d6113..e7ba364c0e5 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -9,10 +9,10 @@ /** * @function - * @summary return teal time data + * @summary return real time data * @name RtdSubmodule#getData - * @param {adUnit[]} adUnits - * @return {Promise} + * @param {AdUnit[]} adUnits + * @param {function} onDone */ /** @@ -76,7 +76,7 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; - if (typeof (_moduleConfig.auctionDelay) == 'undefined') { + if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } // delay bidding process only if auctionDelay > 0 From 65ed9910aa53a41c2427af1e7f5ba2d649e9e068 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 6 Nov 2019 17:34:54 +0200 Subject: [PATCH 15/40] tests fix --- test/spec/modules/realTimeModule_spec.js | 59 +++++++++++------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 92ccae86e80..807781d5a9c 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -18,7 +18,7 @@ let expect = require('chai').expect; describe('Real time module', function() { const conf = { 'realTimeData': { - 'auctionDelay': 1500, + 'auctionDelay': 250, dataProviders: [{ 'name': 'browsi', 'params': { @@ -69,17 +69,9 @@ describe('Real time module', function() { function createSlots() { const slot1 = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); - const slot2 = makeSlot({code: '/57778053/Browsi_Demo_Low', divId: 'browsiAd_2'}); - return [ - slot1, - slot2 - ]; + return [slot1]; } - before(function() { - - }); - describe('Real time module with browsi provider', function() { afterEach(function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); @@ -87,6 +79,7 @@ describe('Real time module', function() { it('check module using bidsBackCallback', function () { let adUnits1 = [getAdUnitMock('browsiAd_1')]; + let targeting = []; init(config); browsiInit(config); config.setConfig(conf); @@ -96,50 +89,52 @@ describe('Real time module', function() { const slots = createSlots(); window.googletag.pubads().setSlots(slots); - setTargetsAfterRequestBids(afterBidHook, adUnits1); function afterBidHook() { slots.map(s => { - let targeting = []; + targeting = []; s.getTargeting().map(value => { - let temp = []; - temp.push(Object.keys(value).toString()); - temp.push(value[Object.keys(value)]); - targeting.push(temp); + targeting.push(Object.keys(value).toString()); }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); }); } + setTargetsAfterRequestBids(afterBidHook, adUnits1, true); + + setTimeout(() => { + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + }, 200); }); it('check module using requestBidsHook', function () { + console.log('entrance', new Date().getMinutes() + ':' + new Date().getSeconds()); let adUnits1 = [getAdUnitMock('browsiAd_1')]; + let targeting = []; + let dataReceived = null; // set slot const slotsB = createSlots(); window.googletag.pubads().setSlots(slotsB); - requestBidsHook(afterBidHook, {adUnits: adUnits1}); - function afterBidHook(adUnits) { - adUnits.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - + function afterBidHook(data) { + dataReceived = data; slotsB.map(s => { - let targeting = []; + targeting = []; s.getTargeting().map(value => { - let temp = []; - temp.push(Object.keys(value).toString()); - temp.push(value[Object.keys(value)]); - targeting.push(temp); + targeting.push(Object.keys(value).toString()); }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); }); } + requestBidsHook(afterBidHook, {adUnits: adUnits1}); + setTimeout(() => { + expect(targeting.indexOf('bv')).to.be.greaterThan(-1); + dataReceived.adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid.realTimeData).to.have.property('bv'); + }); + }); + }, 200); }); - it('check object dep merger', function () { + it('check object deep merge', function () { const obj1 = { id1: { key: 'value', From 15337d24cbd295b35ded0a6a831207673163a1d9 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 8 Dec 2019 16:28:46 +0200 Subject: [PATCH 16/40] browsi real time data provider improvements --- modules/browsiRtdProvider.js | 79 +++++++++++++++++++----- modules/rtdModule/index.js | 11 +++- src/auction.js | 2 +- test/spec/modules/realTimeModule_spec.js | 8 +-- 4 files changed, 79 insertions(+), 21 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 795c9c86f1e..8cd84a1718f 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,16 +13,19 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {number} auctionDelay + * @property {?number} auctionDelay + * @property {?number} timeout */ import {config} from '../src/config.js'; import * as utils from '../src/utils'; import {submodule} from '../src/hook'; -import {ajax} from '../src/ajax'; +import {ajaxBuilder} from '../src/ajax'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ @@ -32,16 +35,20 @@ let _dataReadyCallback = null; /** * add browsi script to page - * @param {string} bptUrl + * @param {Object} data */ -export function addBrowsiTag(bptUrl) { +export function addBrowsiTag(data) { let script = document.createElement('script'); script.async = true; script.setAttribute('data-sitekey', _moduleParams.siteKey); script.setAttribute('data-pubkey', _moduleParams.pubKey); script.setAttribute('prebidbpt', 'true'); script.setAttribute('id', 'browsi-tag'); - script.setAttribute('src', bptUrl); + script.setAttribute('src', data.u); + script.prebidData = utils.deepClone(data); + if (_moduleParams.keyName) { + script.prebidData.kn = _moduleParams.keyName; + } document.head.appendChild(script); return script; } @@ -111,17 +118,20 @@ function sendDataToModule(adUnits, onDone) { return onDone({}); } const slots = getAllSlots(); - if (!slots) { + if (!slots || !slots.length) { return onDone({}); } let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } - const predictionData = _predictions[adUnitCode]; + const adSlot = getSlotById(adUnitCode); + if (!adSlot) { return rp } + const macroId = getMacroId(_predictionsData.plidm, adUnitCode, adSlot); + const predictionData = _predictions[macroId]; if (!predictionData) { return rp } if (predictionData.p) { - if (!isIdMatchingAdUnit(adUnitCode, slots, predictionData.w)) { + if (!isIdMatchingAdUnit(adUnitCode, adSlot, predictionData.w)) { return rp; } rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); @@ -157,17 +167,53 @@ function getKVObject(p, keyName) { /** * check if placement id matches one of given ad units * @param {number} id placement id - * @param {Object[]} allSlots google slots on page + * @param {Object} slot google slot * @param {string[]} whitelist ad units * @return {boolean} */ -export function isIdMatchingAdUnit(id, allSlots, whitelist) { +export function isIdMatchingAdUnit(id, slot, whitelist) { if (!whitelist || !whitelist.length) { return true; } - const slot = allSlots.filter(s => s.getSlotElementId() === id); - const slotAdUnits = slot.map(s => s.getAdUnitPath()); - return slotAdUnits.some(a => whitelist.indexOf(a) !== -1); + const slotAdUnits = slot.getAdUnitPath(); + return whitelist.indexOf(slotAdUnits) !== -1; +} + +/** + * get GPT slot by placement id + * @param {string} id placement id + * @return {?Object} + */ +function getSlotById(id) { + const slots = getAllSlots(); + if (!slots || !slots.length) { + return null; + } + return slots.filter(s => s.getSlotElementId() === id)[0] || null; +} + +/** + * generate id according to macro script + * @param {string} macro replacement macro + * @param {string} id placement id + * @param {Object} slot google slot + * @return {?Object} + */ +function getMacroId(macro, id, slot) { + if (macro) { + try { + const macroString = macro + .replace(//g, `${id}`) + .replace(//g, `${slot.getAdUnitPath()}`) + .replace(//g, (match, p1) => { + return (p1 && slot.getTargeting(p1).join('_')) || 'NA'; + }); + return eval(macroString);// eslint-disable-line no-eval + } catch (e) { + utils.logError(`failed to evaluate: ${macro}`); + } + } + return id; } /** @@ -175,6 +221,8 @@ export function isIdMatchingAdUnit(id, allSlots, whitelist) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { + let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + ajax(url, { success: function (response, req) { @@ -182,11 +230,11 @@ function getPredictionsFromServer(url) { try { const data = JSON.parse(response); if (data && data.p && data.kn) { - setData({p: data.p, kn: data.kn}); + setData({p: data.p, kn: data.kn, plidm: data.plidm}); } else { setData({}); } - addBrowsiTag(data.u); + addBrowsiTag(data); } catch (err) { utils.logError('unable to parse data'); setData({}) @@ -237,6 +285,7 @@ export function init(config) { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; _moduleParams.auctionDelay = realTimeData.auctionDelay; + _moduleParams.timeout = realTimeData.timeout; } catch (e) { _moduleParams = {}; } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index e7ba364c0e5..3136d20ab13 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -47,6 +47,13 @@ * @type {Object} */ +/** + * @property + * @summary timeout (if no auction dealy) + * @name ModuleConfig#timeout + * @type {number} + */ + import {getGlobal} from '../../src/prebidGlobal'; import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting'; @@ -55,6 +62,8 @@ import * as utils from '../../src/utils'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; +/** @type {number} */ +const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ let subModules = []; /** @type {ModuleConfig} */ @@ -100,7 +109,7 @@ function getProviderData(adUnits, callback) { const dataWaitTimeout = setTimeout(() => { processDone = true; callback(dataReceived); - }, _moduleConfig.auctionDelay); + }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); subModules.forEach(sm => { sm.getData(adUnits, onDataReceived); diff --git a/src/auction.js b/src/auction.js index fe1b70085e9..48710252eb3 100644 --- a/src/auction.js +++ b/src/auction.js @@ -168,7 +168,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a _auctionEnd = Date.now(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); - bidsBackCallback(_adUnitCodes, function () { + bidsBackCallback(_adUnits, function () { try { if (_callback != null) { const adUnitCodes = _adUnitCodes; diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index 807781d5a9c..aa80cccd61b 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -179,10 +179,10 @@ describe('Real time module', function() { expect(script.async).to.equal(true); const slots = createSlots(); - const test1 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit('browsiAd_1', slots, ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit('browsiAd_1', slots, []); // true + const test1 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_300x250']); // true + const test2 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = isIdMatchingAdUnit('browsiAd_1', slots[0], ['/57778053/Browsi_Demo_Low']); // false + const test4 = isIdMatchingAdUnit('browsiAd_1', slots[0], []); // true expect(test1).to.equal(true); expect(test2).to.equal(true); From 74a4102d829f33024ce0f58387c2b4adbb2187fd Mon Sep 17 00:00:00 2001 From: omerdotan Date: Mon, 20 Jul 2020 11:11:35 +0300 Subject: [PATCH 17/40] RTD module extend #4610 --- modules/browsiRtdProvider.js | 32 +- modules/rtdModule/index.js | 203 +++++++--- src/targeting.js | 5 +- test/spec/modules/realTimeModule_spec.js | 480 +++++++++++------------ 4 files changed, 411 insertions(+), 309 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 9317786fb8d..3aff3c6aac6 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -37,6 +37,8 @@ let _moduleParams = {}; let _data = null; /** @type {null | function} */ let _dataReadyCallback = null; +/** @type {string} */ +const DEF_KEYNAME = 'browsiViewability'; /** * add browsi script to page @@ -117,16 +119,14 @@ function waitForData(callback) { function sendDataToModule(adUnits, onDone) { try { waitForData(_predictionsData => { - const _predictions = _predictionsData.p; - if (!_predictions || !Object.keys(_predictions).length) { - return onDone({}); - } + const _predictions = _predictionsData.p || {}; let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const adSlot = getSlotByCode(adUnitCode); const identifier = adSlot ? getMacroId(_predictionsData.pmd, adSlot) : adUnitCode; const predictionData = _predictions[identifier]; + rp[adUnitCode] = getKVObject(-1, _predictionsData.kn); if (!predictionData) { return rp } if (predictionData.p) { @@ -160,7 +160,7 @@ function getAllSlots() { function getKVObject(p, keyName) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName).toString())] = prValue.toString(); + prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); return prObject; } /** @@ -231,7 +231,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout); ajax(url, { @@ -286,21 +286,26 @@ export const browsiSubmodule = { * @param {adUnit[]} adUnits * @param {function} onDone */ - getData: sendDataToModule + getData: sendDataToModule, + init: init }; -export function init(config) { +function init(config, gdpr, usp) { + return true; +} + +export function beforeInit(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + confListener(); _moduleParams.auctionDelay = realTimeData.auctionDelay; - _moduleParams.timeout = realTimeData.timeout; + _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; } catch (e) { _moduleParams = {}; } if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - confListener(); collectData(); } else { utils.logError('missing params for Browsi provider'); @@ -308,5 +313,8 @@ export function init(config) { }); } -submodule('realTimeData', browsiSubmodule); -init(config); +function registerSubModule() { + submodule('realTimeData', browsiSubmodule); +} +registerSubModule(); +beforeInit(config); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 3aa7753d204..7a2ef8ed7ee 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -23,14 +23,56 @@ */ /** - * @interface ModuleConfig + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#config + * @type {Object} */ /** - * @property - * @summary sub module name - * @name ModuleConfig#name - * @type {string} + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {Object} config + * @param {Object} gdpr settings + * @param {Object} usp settings + * @return {string|boolean} "failure" to remove sub module + */ + +/** + * @function? + * @summary on auction init event + * @name RtdSubmodule#auctionInit + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on auction end event + * @name RtdSubmodule#auctionEnd + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid request event + * @name RtdSubmodule#updateBidRequest + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid response event + * @name RtdSubmodule#updateBidResponse + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @interface ModuleConfig */ /** @@ -40,18 +82,43 @@ * @type {number} */ +/** + * @property + * @summary timeout (if no auction dealy) + * @name ModuleConfig#timeout + * @type {number} + */ + +/** + * @property + * @summary list of sub modules + * @name ModuleConfig#dataProviders + * @type {SubmoduleConfig[]} + */ + +/** + * @interface SubModuleConfig + */ + /** * @property * @summary params for provide (sub module) - * @name ModuleConfig#params + * @name SubModuleConfig#params * @type {Object} */ /** * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} + * @summary name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary delay auction for this sub module + * @name ModuleConfig#waitForIt + * @type {boolean} */ import {getGlobal} from '../../src/prebidGlobal.js'; @@ -59,6 +126,9 @@ import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting.js'; import {getHook, module} from '../../src/hook.js'; import * as utils from '../../src/utils.js'; +import events from '../../src/events.js'; +import CONSTANTS from '../../src/constants.json'; +import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -68,6 +138,8 @@ const DEF_TIMEOUT = 1000; let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; +/** @type {SubmoduleConfig[]} */ +let _dataProviders = []; /** * enable submodule in User ID @@ -85,6 +157,9 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; + _dataProviders = realTimeData.dataProviders; + subModules = initSubModules(subModules); + setEventsListeners(); if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } @@ -97,61 +172,74 @@ export function init(config) { }); } +/** + * call each sub module init function by config order + * if no init function / init return failure / module not configured - remove it from submodules list + */ +export function initSubModules(subModules) { + let subModulesByOrder = []; + _dataProviders.forEach(provider => { + const sm = subModules.find(s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()) !== 'failure'; + if (initResponse) { + subModulesByOrder.push(Object.assign(sm, {config: provider})); + } + }); + return subModulesByOrder; +} + +/** + * call each sub module event function by config order + */ +function setEventsListeners() { + events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { + subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { + subModules.forEach(sm => { sm.auctionEnd && sm.auctionEnd(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, (args) => { + subModules.forEach(sm => { sm.updateBidRequest && sm.updateBidRequest(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { + subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + }); +} + /** * get data from sub module * @param {AdUnit[]} adUnits received from auction * @param {function} callback callback function on data received */ -function getProviderData(adUnits, callback) { - const callbackExpected = subModules.length; - let dataReceived = []; +export function getProviderData(adUnits, callback) { + const mustWaitSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; + let callbackExpected = mustWaitSubModulesLength || subModules.length; + const mustHaveModules = mustWaitSubModulesLength > 0; + let dataReceived = {}; let processDone = false; - const dataWaitTimeout = setTimeout(() => { - processDone = true; - callback(dataReceived); - }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); - + const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived); + sm.getData(adUnits, onDataReceived.bind(sm)); }); function onDataReceived(data) { if (processDone) { return } - dataReceived.push(data); - if (dataReceived.length === callbackExpected) { - processDone = true; + dataReceived[this.name] = data; + if (!mustHaveModules || (this.config && this.config.waitForIt)) { + callbackExpected-- + } + if (callbackExpected <= 0) { clearTimeout(dataWaitTimeout); - callback(dataReceived); + done(); } } -} -/** - * delete invalid data received from provider - * this is to ensure working flow for GPT - * @param {Object} data received from provider - * @return {Object} valid data for GPT targeting - */ -export function validateProviderDataForGPT(data) { - // data must be an object, contains object with string as value - if (typeof data !== 'object') { - return {}; - } - for (let key in data) { - if (data.hasOwnProperty(key)) { - for (let innerKey in data[key]) { - if (data[key].hasOwnProperty(innerKey)) { - if (typeof data[key][innerKey] !== 'string') { - utils.logWarn(`removing ${key}: {${innerKey}:${data[key][innerKey]} } from GPT targeting because of invalid type (must be string)`); - delete data[key][innerKey]; - } - } - } - } + function done() { + processDone = true; + callback(dataReceived); } - return data; } /** @@ -163,7 +251,7 @@ export function validateProviderDataForGPT(data) { export function setTargetsAfterRequestBids(next, adUnits) { getProviderData(adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); } @@ -172,6 +260,22 @@ export function setTargetsAfterRequestBids(next, adUnits) { }); } +/** + * return an array providers data in reverse order,so the data merge will be according to correct config order + * @param {Submodule[]} modules + * @param {Object} data - data retrieved from providers + * @return {array} reversed order ready for merge + */ +function setDataOrderByProvider(modules, data) { + let rd = []; + for (let i = modules.length; i--; i >= 0) { + if (data[modules[i].name]) { + rd.push(data[modules[i].name]) + } + } + return rd; +} + /** * deep merge array of objects * @param {array} arr - objects array @@ -207,7 +311,7 @@ export function deepMerge(arr) { export function requestBidsHook(fn, reqBidsConfigObj) { getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); @@ -222,7 +326,6 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * @param {Object} data - key values to set */ function setDataForPrimaryAdServer(data) { - data = validateProviderDataForGPT(data); if (utils.isGptPubadsDefined()) { targeting.setTargetingForGPT(data, null) } else { @@ -247,5 +350,5 @@ function addIdDataToAdUnitBids(adUnits, data) { }); } -init(config); module('realTimeData', attachRealTimeDataProvider); +init(config); diff --git a/src/targeting.js b/src/targeting.js index 45c098554a5..da9b760f56d 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -325,7 +325,10 @@ export function newTargeting(auctionManager) { Object.keys(targetingConfig).filter(customSlotMatching ? customSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)) .forEach(targetId => Object.keys(targetingConfig[targetId]).forEach(key => { - let valueArr = targetingConfig[targetId][key].split(','); + let valueArr = targetingConfig[targetId][key]; + if (typeof valueArr === 'string') { + valueArr = valueArr.split(','); + } valueArr = (valueArr.length > 1) ? [valueArr] : valueArr; valueArr.map((value) => { utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${key} value: ${value}`); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index ca149fe7a44..c0b1557b4d5 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,27 +1,172 @@ -import { - init, - requestBidsHook, - setTargetsAfterRequestBids, - deepMerge, - validateProviderDataForGPT -} from 'modules/rtdModule/index.js'; -import { - init as browsiInit, - addBrowsiTag, - isIdMatchingAdUnit, - setData, - getMacroId -} from 'modules/browsiRtdProvider.js'; -import { - init as audigentInit, - setData as setAudigentData -} from 'modules/audigentRtdProvider.js'; +import * as rtdModule from 'modules/rtdModule/index.js'; import { config } from 'src/config.js'; -import { makeSlot } from '../integration/faker/googletag.js'; - -let expect = require('chai').expect; +import {makeSlot} from '../integration/faker/googletag.js'; +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; + +const validSM = { + name: 'validSM', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'key': 'validSM'}) + }, 500) + } +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'ad1': {'key': 'validSMWait'}}) + }, 50) + } +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return 'failure' } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 250, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: { banner: {}, native: {} }, + sizes: [[300, 200], [300, 600]], + bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] + }; +} describe('Real time module', function () { + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + expect(rtdModule.initSubModules([validSM, invalidSM, failureSM, nonConfSM, validSMWait])).to.eql([validSMWait, validSM]) + rtdModule.init(config); + }); + + it('should only wait for must have sub modules', function (done) { + rtdModule.getProviderData([], (data) => { + expect(data).to.eql({validSMWait: {'ad1': {'key': 'validSMWait'}}}); + done(); + }) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('check module using bidsBackCallback', function (done) { + // set slot + const slot = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slot]); + + function afterBidHook() { + expect(slot.getTargeting().length).to.equal(1); + expect(slot.getTargeting()[0].key).to.equal('validSMWait'); + done(); + } + rtdModule.setTargetsAfterRequestBids(afterBidHook, []); + }); + + it('check module using requestBidsHook', function (done) { + // set slot + const slotsB = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slotsB]); + let adUnits = [getAdUnitMock('ad1')]; + + function afterBidHook(data) { + expect(slotsB.getTargeting().length).to.equal(1); + expect(slotsB.getTargeting()[0].key).to.equal('validSMWait'); + + data.adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid.realTimeData).to.have.property('key'); + expect(bid.realTimeData.key).to.equal('validSMWait'); + }); + }); + done(); + } + rtdModule.requestBidsHook(afterBidHook, { adUnits: adUnits }); + }); +}); + +describe('browsi Real time data sub module', function () { const conf = { 'realTimeData': { 'auctionDelay': 250, @@ -33,250 +178,93 @@ describe('Real time module', function () { 'pubKey': 'testPub', 'keyName': 'bv' } - }, { - 'name': 'audigent' }] } }; - const predictions = { - p: { - 'browsiAd_2': { - 'w': [ - '/57778053/Browsi_Demo_Low', - '/57778053/Browsi_Demo_300x250' - ], - 'p': 0.07 - }, - 'browsiAd_1': { - 'w': [], - 'p': 0.06 - }, - 'browsiAd_3': { - 'w': [], - 'p': 0.53 - }, - 'browsiAd_4': { - 'w': [ - '/57778053/Browsi_Demo' - ], - 'p': 0.85 - } - } - }; - - const audigentSegments = { - audigent_segments: { 'a': 1, 'b': 2 } - } - - function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; - } - - function createSlots() { - const slot1 = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - const slot2 = makeSlot({ code: '/57778053/Browsi', divId: 'browsiAd_1' }); - return [slot1, slot2]; - } - - describe('Real time module with browsi provider', function () { - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - }); + beforeEach(function () { + config.setConfig(conf); + }); - after(function () { - config.resetConfig(); - }); + after(function () { + config.resetConfig(); + }); - it('check module using bidsBackCallback', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - init(config); - browsiInit(config); - config.setConfig(conf); - setData(predictions); - - // set slot - const slots = createSlots(); - window.googletag.pubads().setSlots(slots); - - function afterBidHook() { - slots.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should init and return true', function () { + browsiRTD.beforeInit(config); + expect(browsiRTD.browsiSubmodule.init()).to.equal(true) + }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - } - setTargetsAfterRequestBids(afterBidHook, adUnits1, true); - }); + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.realTimeData.dataProviders[0].params.keyName); + }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should match placement with ad unit', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - } - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - it('check object deep merge', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); - const merged = deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); + it('should return correct macro values', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - it('check data validation for GPT targeting', function () { - // non strings values should be removed - const obj = { - valid: {'key': 'value'}, - invalid: {'key': ['value']}, - combine: { - 'a': 'value', - 'b': [] - } - }; + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - const expected = { - valid: {'key': 'value'}, - invalid: {}, - combine: { - 'a': 'value', - } - }; - const validationResult = validateProviderDataForGPT(obj); - assert.deepEqual(expected, validationResult); - }); + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); - it('check browsi sub module', function () { - const script = addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - - const slots = createSlots(); - const test1 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit(slots[0], []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - - // macro results - slots[0].setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = getMacroId({p: '/'}, slots[0]); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = getMacroId({}, slots[0]); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = getMacroId({p: '', s: {s: 0, e: 1}}, slots[0]); - expect(macroResultC).to.equal('/'); - }) + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); }); - describe('Real time module with Audigent provider', function () { - before(function () { - init(config); - audigentInit(config); - config.setConfig(conf); - setAudigentData(audigentSegments); + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function (done) { + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData([], onDone); + function onDone(data) { + expect(data).to.eql({}); + done(); + } }); - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - config.resetConfig(); + it('should return NA if no prediction for ad unit', function (done) { + const adUnits = [getAdUnitMock('adMock')]; + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({adMock: {bv: 'NA'}}); + done(); + } }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('audigentAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('audigent_segments'); - expect(bid.realTimeData.audigent_segments).to.deep.equal(audigentSegments.audigent_segments); - }); - }); + it('should return prediction from server', function (done) { + const adUnits = [getAdUnitMock('hasPrediction')]; + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({hasPrediction: {bv: '0.20'}}); + done(); } - - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); - }); + }) + }) }); From 735e8e944e0b1c1cabed254651aa9f375d15b3ef Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 2 Aug 2020 08:45:25 +0300 Subject: [PATCH 18/40] add hook for submodule init variables naming --- modules/rtdModule/index.js | 37 +++++++++++++++--------- test/spec/modules/realTimeModule_spec.js | 10 +++++-- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 7a2ef8ed7ee..9acd484cec8 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -36,7 +36,7 @@ * @param {Object} config * @param {Object} gdpr settings * @param {Object} usp settings - * @return {string|boolean} "failure" to remove sub module + * @return {boolean} false to remove sub module */ /** @@ -129,13 +129,14 @@ import * as utils from '../../src/utils.js'; import events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import find from 'core-js-pure/features/array/find.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {number} */ const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ -let subModules = []; +export let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; /** @type {SubmoduleConfig[]} */ @@ -158,7 +159,7 @@ export function init(config) { confListener(); // unsubscribe config listener _moduleConfig = realTimeData; _dataProviders = realTimeData.dataProviders; - subModules = initSubModules(subModules); + getHook('makeBidRequests').before(initSubModules); setEventsListeners(); if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; @@ -176,16 +177,17 @@ export function init(config) { * call each sub module init function by config order * if no init function / init return failure / module not configured - remove it from submodules list */ -export function initSubModules(subModules) { +export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout, labels) { let subModulesByOrder = []; _dataProviders.forEach(provider => { - const sm = subModules.find(s => s.name === provider.name); - const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()) !== 'failure'; + const sm = find(subModules, s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()); if (initResponse) { subModulesByOrder.push(Object.assign(sm, {config: provider})); } }); - return subModulesByOrder; + subModules = subModulesByOrder; + next(adUnits, auctionStart, auctionId, cbTimeout, labels) } /** @@ -212,9 +214,16 @@ function setEventsListeners() { * @param {function} callback callback function on data received */ export function getProviderData(adUnits, callback) { - const mustWaitSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; - let callbackExpected = mustWaitSubModulesLength || subModules.length; - const mustHaveModules = mustWaitSubModulesLength > 0; + /** + * invoke callback if one of the conditions met: + * timeout reached + * all submodules answered + * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) + */ + + const waitForSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; + let callbacksExpected = waitForSubModulesLength || subModules.length; + const shouldWaitForAllSubModules = waitForSubModulesLength === 0; let dataReceived = {}; let processDone = false; const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); @@ -227,10 +236,10 @@ export function getProviderData(adUnits, callback) { return } dataReceived[this.name] = data; - if (!mustHaveModules || (this.config && this.config.waitForIt)) { - callbackExpected-- + if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { + callbacksExpected-- } - if (callbackExpected <= 0) { + if (callbacksExpected <= 0) { clearTimeout(dataWaitTimeout); done(); } @@ -268,7 +277,7 @@ export function setTargetsAfterRequestBids(next, adUnits) { */ function setDataOrderByProvider(modules, data) { let rd = []; - for (let i = modules.length; i--; i >= 0) { + for (let i = modules.length; i--; i > 0) { if (data[modules[i].name]) { rd.push(data[modules[i].name]) } diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index c0b1557b4d5..f47068724d1 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -29,7 +29,7 @@ const invalidSM = { const failureSM = { name: 'failureSM', - init: () => { return 'failure' } + init: () => { return false } }; const nonConfSM = { @@ -76,13 +76,17 @@ describe('Real time module', function () { config.setConfig(conf); }); - it('should use only valid modules', function () { + it('should use only valid modules', function (done) { rtdModule.attachRealTimeDataProvider(validSM); rtdModule.attachRealTimeDataProvider(invalidSM); rtdModule.attachRealTimeDataProvider(failureSM); rtdModule.attachRealTimeDataProvider(nonConfSM); rtdModule.attachRealTimeDataProvider(validSMWait); - expect(rtdModule.initSubModules([validSM, invalidSM, failureSM, nonConfSM, validSMWait])).to.eql([validSMWait, validSM]) + rtdModule.initSubModules(afterInitSubModules); + function afterInitSubModules() { + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + done(); + } rtdModule.init(config); }); From 959673d352e6aed71ca08b4d71f76ab896abc40e Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 23 Aug 2020 11:04:40 +0300 Subject: [PATCH 19/40] RTD bug fix --- modules/rtdModule/index.js | 36 +++++++++++++++--------- test/spec/modules/realTimeModule_spec.js | 9 ++---- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 9acd484cec8..d6fb25a9311 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -135,6 +135,8 @@ import find from 'core-js-pure/features/array/find.js'; const MODULE_NAME = 'realTimeData'; /** @type {number} */ const DEF_TIMEOUT = 1000; +/** @type {boolean} */ +let _smInit = false; /** @type {RtdSubmodule[]} */ export let subModules = []; /** @type {ModuleConfig} */ @@ -159,16 +161,15 @@ export function init(config) { confListener(); // unsubscribe config listener _moduleConfig = realTimeData; _dataProviders = realTimeData.dataProviders; - getHook('makeBidRequests').before(initSubModules); setEventsListeners(); if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } + getGlobal().requestBids.before(requestBidsHook, 40); // delay bidding process only if auctionDelay > 0 + // if auction delay is > 0 use requestBidsHook if (!_moduleConfig.auctionDelay > 0) { getHook('bidsBackCallback').before(setTargetsAfterRequestBids); - } else { - getGlobal().requestBids.before(requestBidsHook); } }); } @@ -177,7 +178,11 @@ export function init(config) { * call each sub module init function by config order * if no init function / init return failure / module not configured - remove it from submodules list */ -export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout, labels) { +export function initSubModules() { + if (_smInit) { + // only need to init once + return; + } let subModulesByOrder = []; _dataProviders.forEach(provider => { const sm = find(subModules, s => s.name === provider.name); @@ -187,7 +192,7 @@ export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout } }); subModules = subModulesByOrder; - next(adUnits, auctionStart, auctionId, cbTimeout, labels) + _smInit = true; } /** @@ -318,16 +323,21 @@ export function deepMerge(arr) { * @param {Object} reqBidsConfigObj - request bids object */ export function requestBidsHook(fn, reqBidsConfigObj) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); + initSubModules(); + if (_moduleConfig.auctionDelay > 0) { + getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { + if (data && Object.keys(data).length) { + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); + if (Object.keys(_mergedData).length) { + setDataForPrimaryAdServer(_mergedData); + addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); + } } - } + return fn.call(this, reqBidsConfigObj); + }); + } else { return fn.call(this, reqBidsConfigObj); - }); + } } /** diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index f47068724d1..cc8c7bd3954 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -76,17 +76,14 @@ describe('Real time module', function () { config.setConfig(conf); }); - it('should use only valid modules', function (done) { + it('should use only valid modules', function () { rtdModule.attachRealTimeDataProvider(validSM); rtdModule.attachRealTimeDataProvider(invalidSM); rtdModule.attachRealTimeDataProvider(failureSM); rtdModule.attachRealTimeDataProvider(nonConfSM); rtdModule.attachRealTimeDataProvider(validSMWait); - rtdModule.initSubModules(afterInitSubModules); - function afterInitSubModules() { - expect(rtdModule.subModules).to.eql([validSMWait, validSM]); - done(); - } + rtdModule.initSubModules(); + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); rtdModule.init(config); }); From 1fb1f31a6935acb0cf8a5f56c15d397c59f5a970 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 26 Aug 2020 19:08:40 +0300 Subject: [PATCH 20/40] remove auction delay and related hooks --- modules/browsiRtdProvider.js | 6 +-- modules/rtdModule/index.js | 65 ++++++------------------ test/spec/modules/realTimeModule_spec.js | 45 ++++------------ 3 files changed, 27 insertions(+), 89 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 3aff3c6aac6..519a77bdb02 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,7 +13,6 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {?number} auctionDelay * @property {?number} timeout */ @@ -231,7 +230,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout); + let ajax = ajaxBuilder(_moduleParams.timeout); ajax(url, { @@ -286,7 +285,7 @@ export const browsiSubmodule = { * @param {adUnit[]} adUnits * @param {function} onDone */ - getData: sendDataToModule, + addTargeting: sendDataToModule, init: init }; @@ -300,7 +299,6 @@ export function beforeInit(config) { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; confListener(); - _moduleParams.auctionDelay = realTimeData.auctionDelay; _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; } catch (e) { _moduleParams = {}; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index d6fb25a9311..64fb7bebec2 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -8,9 +8,9 @@ */ /** - * @function + * @function? * @summary return real time data - * @name RtdSubmodule#getData + * @name RtdSubmodule#addTargeting * @param {AdUnit[]} adUnits * @param {function} onDone */ @@ -75,13 +75,6 @@ * @interface ModuleConfig */ -/** - * @property - * @summary auction delay - * @name ModuleConfig#auctionDelay - * @type {number} - */ - /** * @property * @summary timeout (if no auction dealy) @@ -162,15 +155,8 @@ export function init(config) { _moduleConfig = realTimeData; _dataProviders = realTimeData.dataProviders; setEventsListeners(); - if (typeof (_moduleConfig.auctionDelay) === 'undefined') { - _moduleConfig.auctionDelay = 0; - } getGlobal().requestBids.before(requestBidsHook, 40); - // delay bidding process only if auctionDelay > 0 - // if auction delay is > 0 use requestBidsHook - if (!_moduleConfig.auctionDelay > 0) { - getHook('bidsBackCallback').before(setTargetsAfterRequestBids); - } + getHook('bidsBackCallback').before(setTargetsAfterRequestBids); }); } @@ -226,14 +212,19 @@ export function getProviderData(adUnits, callback) { * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) */ - const waitForSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; - let callbacksExpected = waitForSubModulesLength || subModules.length; + const dataSubModules = subModules.filter(sm => typeof sm.addTargeting === 'function'); + const waitForSubModulesLength = dataSubModules.filter(sm => !!(sm.config && sm.config.waitForIt)).length; + let callbacksExpected = waitForSubModulesLength || dataSubModules.length; const shouldWaitForAllSubModules = waitForSubModulesLength === 0; let dataReceived = {}; let processDone = false; - const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); - subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived.bind(sm)); + if (!dataSubModules.length || !adUnits) { + done(); + return; + } + const dataWaitTimeout = setTimeout(done, _moduleConfig.timeout || DEF_TIMEOUT); + dataSubModules.forEach(sm => { + sm.addTargeting(adUnits, onDataReceived.bind(sm)); }); function onDataReceived(data) { @@ -318,26 +309,13 @@ export function deepMerge(arr) { /** * run hook before bids request - * get data from provider and set key values to primary ad server & bidders + * use hook to init submodules (after consent is set) * @param {function} fn - hook function * @param {Object} reqBidsConfigObj - request bids object */ export function requestBidsHook(fn, reqBidsConfigObj) { initSubModules(); - if (_moduleConfig.auctionDelay > 0) { - getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); - } - } - return fn.call(this, reqBidsConfigObj); - }); - } else { - return fn.call(this, reqBidsConfigObj); - } + return fn.call(this, reqBidsConfigObj); } /** @@ -356,18 +334,5 @@ function setDataForPrimaryAdServer(data) { } } -/** - * @param {AdUnit[]} adUnits - * @param {Object} data - key values to set - */ -function addIdDataToAdUnitBids(adUnits, data) { - adUnits.forEach(adUnit => { - adUnit.bids = adUnit.bids.map(bid => { - const rd = data[adUnit.code] || {}; - return Object.assign(bid, {realTimeData: rd}); - }) - }); -} - module('realTimeData', attachRealTimeDataProvider); init(config); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index cc8c7bd3954..b25778633f3 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -6,20 +6,16 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; const validSM = { name: 'validSM', init: () => { return true }, - getData: (adUnits, onDone) => { - setTimeout(() => { - return onDone({'key': 'validSM'}) - }, 500) + addTargeting: (adUnits, onDone) => { + return onDone({'key': 'validSM'}) } }; const validSMWait = { name: 'validSMWait', init: () => { return true }, - getData: (adUnits, onDone) => { - setTimeout(() => { - return onDone({'ad1': {'key': 'validSMWait'}}) - }, 50) + addTargeting: (adUnits, onDone) => { + return onDone({'ad1': {'key': 'validSMWait'}}) } }; @@ -39,7 +35,7 @@ const nonConfSM = { const conf = { 'realTimeData': { - 'auctionDelay': 250, + 'timeout': 100, dataProviders: [ { 'name': 'validSMWait', @@ -82,9 +78,9 @@ describe('Real time module', function () { rtdModule.attachRealTimeDataProvider(failureSM); rtdModule.attachRealTimeDataProvider(nonConfSM); rtdModule.attachRealTimeDataProvider(validSMWait); + rtdModule.init(config); rtdModule.initSubModules(); expect(rtdModule.subModules).to.eql([validSMWait, validSM]); - rtdModule.init(config); }); it('should only wait for must have sub modules', function (done) { @@ -144,33 +140,12 @@ describe('Real time module', function () { } rtdModule.setTargetsAfterRequestBids(afterBidHook, []); }); - - it('check module using requestBidsHook', function (done) { - // set slot - const slotsB = makeSlot({ code: '/code1', divId: 'ad1' }); - window.googletag.pubads().setSlots([slotsB]); - let adUnits = [getAdUnitMock('ad1')]; - - function afterBidHook(data) { - expect(slotsB.getTargeting().length).to.equal(1); - expect(slotsB.getTargeting()[0].key).to.equal('validSMWait'); - - data.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('key'); - expect(bid.realTimeData.key).to.equal('validSMWait'); - }); - }); - done(); - } - rtdModule.requestBidsHook(afterBidHook, { adUnits: adUnits }); - }); }); describe('browsi Real time data sub module', function () { const conf = { 'realTimeData': { - 'auctionDelay': 250, + 'timeout': 250, dataProviders: [{ 'name': 'browsi', 'params': { @@ -236,7 +211,7 @@ describe('browsi Real time data sub module', function () { describe('should return data to RTD module', function () { it('should return empty if no ad units defined', function (done) { browsiRTD.setData({}); - browsiRTD.browsiSubmodule.getData([], onDone); + browsiRTD.browsiSubmodule.addTargeting([], onDone); function onDone(data) { expect(data).to.eql({}); done(); @@ -246,7 +221,7 @@ describe('browsi Real time data sub module', function () { it('should return NA if no prediction for ad unit', function (done) { const adUnits = [getAdUnitMock('adMock')]; browsiRTD.setData({}); - browsiRTD.browsiSubmodule.getData(adUnits, onDone); + browsiRTD.browsiSubmodule.addTargeting(adUnits, onDone); function onDone(data) { expect(data).to.eql({adMock: {bv: 'NA'}}); done(); @@ -261,7 +236,7 @@ describe('browsi Real time data sub module', function () { pmd: undefined }; browsiRTD.setData(data); - browsiRTD.browsiSubmodule.getData(adUnits, onDone); + browsiRTD.browsiSubmodule.addTargeting(adUnits, onDone); function onDone(data) { expect(data).to.eql({hasPrediction: {bv: '0.20'}}); done(); From 90e8249901f337eb54359fc19dd44b2687936117 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 7 Jan 2020 13:07:31 -0500 Subject: [PATCH 21/40] update audigent rtd provider --- .../gpt/audigentSegments_example.html | 82 ++++++++++++++----- modules/audigentRtdProvider.js | 11 ++- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/integrationExamples/gpt/audigentSegments_example.html b/integrationExamples/gpt/audigentSegments_example.html index 7739b558327..874e3e081b1 100644 --- a/integrationExamples/gpt/audigentSegments_example.html +++ b/integrationExamples/gpt/audigentSegments_example.html @@ -84,11 +84,10 @@ { code: 'test-div', mediaTypes: { - banner: { - sizes: [[300,250],[300,600],[728,90]] - } + banner: { + sizes: [[300,250],[300,600],[728,90]] + } }, - bids: [ { bidder: 'rubicon', @@ -120,20 +119,9 @@ consentManagement: { cmpApi: 'iab', timeout: 1000, - allowAuctionWithoutConsent: true + defaultGdprScope: true }, - // consentManagement: { - // cmpApi: 'static', - // consentData: { - // consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' - // vendorData: { - // purposeConsents: { - // '1': true - // } - // } - // } - // }, - usersync: { + userSync: { userIds: [{ name: "unifiedId", params: { @@ -145,6 +133,16 @@ name: "unifiedid", expires: 30 }, + }, { + name: "intentIqId", + params: { + partner: 0, //Set your real IntentIQ partner ID here for production. + }, + storage: { + type: "cookie", + name: "intentIqId", + expires: 30 + }, }, { name: "id5Id", params: { @@ -157,6 +155,18 @@ refreshInSeconds: 8*3600 // Refresh frequency of cookies, defaulting to 'expires' }, + }, { + name: "merkleId", + params: { + ptk: '12345678-aaaa-bbbb-cccc-123456789abc', //Set your real merkle partner key here + pubid: 'EXAMPLE' //Set your real merkle publisher id here + }, + storage: { + type: "html5", + name: "merkleId", + expires: 30 + }, + }, { name: "parrableId", params: { @@ -174,7 +184,7 @@ // foo: '9879878907987', // bar:'93939' // } - }, { + }, { name: 'identityLink', params: { pid: '14' // Set your real identityLink placement ID here @@ -184,9 +194,42 @@ name: 'idl_env', expires: 30 } + }, { + name: "sharedId", + params: { + syncTime: 60 // in seconds, default is 24 hours + }, + storage: { + type: "cookie", + name: "sharedid", + expires: 28 + } + }, { + name: 'lotamePanoramaId' + }, { + name: "liveIntentId", + params: { + publisherId: "9896876" + }, + storage: { + type: "cookie", + name: "_li_pbid", + expires: 28 + } + }, { + name: "zeotapIdPlus" + }, { + name: 'haloId', + storage: { + type: "cookie", + name: "haloId", + expires: 28 + } + }, { + name: "quantcastId" }], syncDelay: 5000, - auctionDelay: 1000 + auctionDelay: 1000 }, realTimeData: { auctionDelay: 1000, @@ -246,6 +289,7 @@

Audigent Segments Prebid

googletag.cmd.push(function() { googletag.display('test-div'); }); + TDID:
diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 0f32c84962f..2003bbdae23 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -64,15 +64,12 @@ function getSegments(adUnits, onDone) { function getSegmentsAsync(adUnits, onDone) { const userIds = (getGlobal()).getUserIds(); - let tdid = null; - if (userIds && userIds['tdid']) { - tdid = userIds['tdid']; - } else { + if (typeof userIds != 'undefined' && userIds != null) { onDone({}); } - const url = `https://seg.ad.gt/api/v1/rtb_segments?tdid=${tdid}`; + const url = `https://seg.ad.gt/api/v1/rtb_segments`; ajax(url, { success: function (response, req) { @@ -105,7 +102,9 @@ function getSegmentsAsync(adUnits, onDone) { onDone({}); utils.logError('unable to get audigent segment data'); } - } + }, + JSON.stringify(userIds), + {contentType: 'application/json'} ); } From 243cf99d623f9c864c664238b04baae809d35fa2 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Mon, 21 Sep 2020 15:33:47 -0500 Subject: [PATCH 22/40] style update --- modules/audigentRtdProvider.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 2003bbdae23..30affef223c 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -34,7 +34,6 @@ let _moduleParams = {}; * XMLHttpRequest to get data form audigent server * @param {string} url server url with query params */ - export function setData(data) { storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); } From c64556b1b77fdd306a0cd6da2c00479567bfb461 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 22 Sep 2020 20:56:03 -0500 Subject: [PATCH 23/40] change onDone() logic --- modules/audigentRtdProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 30affef223c..977423abdd4 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -64,7 +64,7 @@ function getSegments(adUnits, onDone) { function getSegmentsAsync(adUnits, onDone) { const userIds = (getGlobal()).getUserIds(); - if (typeof userIds != 'undefined' && userIds != null) { + if (typeof userIds == 'undefined' || userIds == null) { onDone({}); } From 6e7805b6c1956874777b4387601f965070781190 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Wed, 23 Sep 2020 17:21:01 +0300 Subject: [PATCH 24/40] RTD phase 3 --- modules/browsiRtdProvider.js | 110 +++----- modules/rtdModule/index.js | 257 ++++++++++--------- src/targeting.js | 24 +- test/spec/modules/browsiRtdProvider_spec.js | 88 +++++++ test/spec/modules/realTimeDataModule_spec.js | 158 ++++++++++++ test/spec/modules/realTimeModule_spec.js | 246 ------------------ 6 files changed, 444 insertions(+), 439 deletions(-) create mode 100644 test/spec/modules/browsiRtdProvider_spec.js create mode 100644 test/spec/modules/realTimeDataModule_spec.js delete mode 100644 test/spec/modules/realTimeModule_spec.js diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 519a77bdb02..c0fc27dca17 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -13,7 +13,6 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {?number} timeout */ import {config} from '../src/config.js'; @@ -28,14 +27,10 @@ const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ -let _data = null; -/** @type {null | function} */ -let _dataReadyCallback = null; +let _predictionsData = null; /** @type {string} */ const DEF_KEYNAME = 'browsiViewability'; @@ -62,7 +57,7 @@ export function addBrowsiTag(data) { * collect required data from page * send data to browsi server to get predictions */ -function collectData() { +export function collectData() { const win = window.top; const doc = win.document; let browsiData = null; @@ -87,59 +82,30 @@ function collectData() { } export function setData(data) { - _data = data; - - if (typeof _dataReadyCallback === 'function') { - _dataReadyCallback(_data); - _dataReadyCallback = null; - } -} - -/** - * wait for data from server - * call callback when data is ready - * @param {function} callback - */ -function waitForData(callback) { - if (_data) { - _dataReadyCallback = null; - callback(_data); - } else { - _dataReadyCallback = callback; - } + _predictionsData = data; } -/** - * filter server data according to adUnits received - * call callback (onDone) when data is ready - * @param {adUnit[]} adUnits - * @param {function} onDone callback function - */ -function sendDataToModule(adUnits, onDone) { +function sendDataToModule(adUnitsCodes) { try { - waitForData(_predictionsData => { - const _predictions = _predictionsData.p || {}; - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - const adSlot = getSlotByCode(adUnitCode); - const identifier = adSlot ? getMacroId(_predictionsData.pmd, adSlot) : adUnitCode; - const predictionData = _predictions[identifier]; - rp[adUnitCode] = getKVObject(-1, _predictionsData.kn); - if (!predictionData) { return rp } - - if (predictionData.p) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { - return rp; - } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + const _predictions = (_predictionsData && _predictionsData.p) || {}; + let dataToReturn = adUnitsCodes.reduce((rp, adUnitCode) => { + if (!adUnitCode) { return rp } + const adSlot = getSlotByCode(adUnitCode); + const identifier = adSlot ? getMacroId(_predictionsData['pmd'], adSlot) : adUnitCode; + const predictionData = _predictions[identifier]; + rp[adUnitCode] = getKVObject(-1, _predictionsData['kn']); + if (!predictionData) { return rp } + if (predictionData.p) { + if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { + return rp; } - return rp; - }, {}); - return onDone(dataToReturn); - }); + rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + } + return rp; + }, {}); + return dataToReturn; } catch (e) { - onDone({}); + return {}; } } @@ -230,7 +196,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.timeout); + let ajax = ajaxBuilder(); ajax(url, { @@ -282,28 +248,21 @@ export const browsiSubmodule = { /** * get data and send back to realTimeData module * @function - * @param {adUnit[]} adUnits - * @param {function} onDone + * @param {string[]} adUnitsCodes */ - addTargeting: sendDataToModule, - init: init + getTargetingData: sendDataToModule, + init: init, }; -function init(config, gdpr, usp) { +function init() { return true; } -export function beforeInit(config) { +function beforeInit(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( - pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; - confListener(); - _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; - } catch (e) { - _moduleParams = {}; - } - if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { + setModuleData(realTimeData); + confListener(); + if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { collectData(); } else { utils.logError('missing params for Browsi provider'); @@ -311,6 +270,15 @@ export function beforeInit(config) { }); } +export function setModuleData(realTimeData) { + try { + _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( + pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + } catch (e) { + _moduleParams = {}; + } +} + function registerSubModule() { submodule('realTimeData', browsiSubmodule); } diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 64fb7bebec2..19d370728b3 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -3,6 +3,28 @@ * @module modules/realTimeData */ +/** + * @interface UserConsentData + */ +/** + * @property + * @summary gdpr consent + * @name UserConsentData#gdpr + * @type {Object} + */ +/** + * @property + * @summary usp consent + * @name UserConsentData#usp + * @type {Object} + */ +/** + * @property + * @summary coppa + * @name UserConsentData#coppa + * @type {boolean} + */ + /** * @interface RtdSubmodule */ @@ -10,9 +32,20 @@ /** * @function? * @summary return real time data - * @name RtdSubmodule#addTargeting - * @param {AdUnit[]} adUnits - * @param {function} onDone + * @name RtdSubmodule#getTargetingData + * @param {string[]} adUnitsCodes + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + +/** + * @function? + * @summary modify bid request data + * @name RtdSubmodule#getBidRequestData + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @param {Object} reqBidsConfigObj + * @param {function} callback */ /** @@ -33,42 +66,36 @@ * @function * @summary init sub module * @name RtdSubmodule#init - * @param {Object} config - * @param {Object} gdpr settings - * @param {Object} usp settings + * @param {SubmoduleConfig} config + * @param {UserConsentData} user consent * @return {boolean} false to remove sub module */ /** * @function? * @summary on auction init event - * @name RtdSubmodule#auctionInit + * @name RtdSubmodule#onAuctionInitEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on auction end event - * @name RtdSubmodule#auctionEnd - * @param {Object} data - * @param {SubmoduleConfig} config - */ - -/** - * @function? - * @summary on bid request event - * @name RtdSubmodule#updateBidRequest + * @name RtdSubmodule#onAuctionEndEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** * @function? * @summary on bid response event - * @name RtdSubmodule#updateBidResponse + * @name RtdSubmodule#onBidResponseEvent * @param {Object} data * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** @@ -77,8 +104,8 @@ /** * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout + * @summary auction delay + * @name ModuleConfig#auctionDelay * @type {number} */ @@ -114,20 +141,17 @@ * @type {boolean} */ -import {getGlobal} from '../../src/prebidGlobal.js'; import {config} from '../../src/config.js'; -import {targeting} from '../../src/targeting.js'; -import {getHook, module} from '../../src/hook.js'; +import {module} from '../../src/hook.js'; import * as utils from '../../src/utils.js'; import events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; import find from 'core-js-pure/features/array/find.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {number} */ -const DEF_TIMEOUT = 1000; /** @type {boolean} */ let _smInit = false; /** @type {RtdSubmodule[]} */ @@ -136,6 +160,8 @@ export let subModules = []; let _moduleConfig; /** @type {SubmoduleConfig[]} */ let _dataProviders = []; +/** @type {UserConsentData} */ +let _userConsent; /** * enable submodule in User ID @@ -155,8 +181,7 @@ export function init(config) { _moduleConfig = realTimeData; _dataProviders = realTimeData.dataProviders; setEventsListeners(); - getGlobal().requestBids.before(requestBidsHook, 40); - getHook('bidsBackCallback').before(setTargetsAfterRequestBids); + getGlobal().requestBids.before(setBidRequestsData, 40); }); } @@ -170,9 +195,14 @@ export function initSubModules() { return; } let subModulesByOrder = []; + _userConsent = { + gdpr: gdprDataHandler.getConsentData(), + usp: uspDataHandler.getConsentData(), + coppa: !!(config.getConfig('coppa')) + }; _dataProviders.forEach(provider => { const sm = find(subModules, s => s.name === provider.name); - const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()); + const initResponse = sm && sm.init && sm.init(provider, _userConsent); if (initResponse) { subModulesByOrder.push(Object.assign(sm, {config: provider})); } @@ -186,99 +216,111 @@ export function initSubModules() { */ function setEventsListeners() { events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { - subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + subModules.forEach(sm => { sm.onAuctionInitEvent && sm.onAuctionInitEvent(args, sm.config, _userConsent) }) }); events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { - subModules.forEach(sm => { sm.auctionEnd && sm.auctionEnd(args, sm.config) }) - }); - events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, (args) => { - subModules.forEach(sm => { sm.updateBidRequest && sm.updateBidRequest(args, sm.config) }) + getAdUnitTargeting(args); + subModules.forEach(sm => { sm.onAuctionEndEvent && sm.onAuctionEndEvent(args, sm.config, _userConsent) }) }); events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { - subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + subModules.forEach(sm => { sm.onBidResponseEvent && sm.onBidResponseEvent(args, sm.config, _userConsent) }) }); } /** - * get data from sub module - * @param {AdUnit[]} adUnits received from auction - * @param {function} callback callback function on data received + * loop through configured data providers If the data provider has registered getBidRequestData, + * call it, providing reqBidsConfigObj, consent data and module params + * this allows submodules to modify bidders + * @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js */ -export function getProviderData(adUnits, callback) { - /** - * invoke callback if one of the conditions met: - * timeout reached - * all submodules answered - * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) - */ - - const dataSubModules = subModules.filter(sm => typeof sm.addTargeting === 'function'); - const waitForSubModulesLength = dataSubModules.filter(sm => !!(sm.config && sm.config.waitForIt)).length; - let callbacksExpected = waitForSubModulesLength || dataSubModules.length; - const shouldWaitForAllSubModules = waitForSubModulesLength === 0; - let dataReceived = {}; - let processDone = false; - if (!dataSubModules.length || !adUnits) { - done(); - return; +export function setBidRequestsData(fn, reqBidsConfigObj) { + initSubModules(); + + // delay bidding process only if auctionDelay > 0 + if (!_moduleConfig.auctionDelay || _moduleConfig.auctionDelay < 1) { + return exitHook(); } - const dataWaitTimeout = setTimeout(done, _moduleConfig.timeout || DEF_TIMEOUT); - dataSubModules.forEach(sm => { - sm.addTargeting(adUnits, onDataReceived.bind(sm)); + + const relevantSubModules = subModules.filter(sm => typeof sm.getBidRequestData === 'function'); + const prioritySubModules = relevantSubModules.filter(sm => !!(sm.config && sm.config.waitForIt)); + let callbacksExpected = prioritySubModules.length; + let isDone = false; + let waitTimeout; + + if (!relevantSubModules.length) { + return exitHook(); + } + + if (prioritySubModules.length) { + waitTimeout = setTimeout(exitHook, _moduleConfig.auctionDelay); + } + + relevantSubModules.forEach(sm => { + sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) }); - function onDataReceived(data) { - if (processDone) { - return + if (!prioritySubModules.length) { + return exitHook(); + } + + function onGetBidRequestDataCallback(auctionId) { + utils.logInfo('onGetBidRequestDataCallback', auctionId, this); + if (isDone) { + return; } - dataReceived[this.name] = data; - if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { - callbacksExpected-- + if (this.config && this.config.waitForIt) { + callbacksExpected--; } if (callbacksExpected <= 0) { - clearTimeout(dataWaitTimeout); - done(); + return exitHook(); } } - function done() { - processDone = true; - callback(dataReceived); + function exitHook() { + isDone = true; + clearTimeout(waitTimeout); + fn.call(this, reqBidsConfigObj); } } /** - * run hook after bids request and before callback - * get data from provider and set key values to primary ad server - * @param {function} next - next hook function - * @param {AdUnit[]} adUnits received from auction + * loop through configured data providers If the data provider has registered getTargetingData, + * call it, providing ad unit codes, consent data and module params + * the sub mlodle will return data to set on the ad unit + * this function used to place key values on primary ad server per ad unit + * @param {Object} auction object received on auction end event */ -export function setTargetsAfterRequestBids(next, adUnits) { - getProviderData(adUnits, (data) => { - if (data && Object.keys(data).length) { - const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); - if (Object.keys(_mergedData).length) { - setDataForPrimaryAdServer(_mergedData); - } - } - next(adUnits); - }); -} +export function getAdUnitTargeting(auction) { + const relevantSubModules = subModules.filter(sm => typeof sm.getTargetingData === 'function'); + if (!relevantSubModules.length) { + return; + } -/** - * return an array providers data in reverse order,so the data merge will be according to correct config order - * @param {Submodule[]} modules - * @param {Object} data - data retrieved from providers - * @return {array} reversed order ready for merge - */ -function setDataOrderByProvider(modules, data) { - let rd = []; - for (let i = modules.length; i--; i > 0) { - if (data[modules[i].name]) { - rd.push(data[modules[i].name]) + // get data + const adUnitCodes = auction.adUnitCodes; + if (!adUnitCodes) { + return; + } + let targeting = []; + for (let i = relevantSubModules.length; i--; i > 0) { + const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); + if (smTargeting && typeof smTargeting === 'object') { + targeting.push(smTargeting); + } else { + utils.logWarn('invalid getTargetingData response for sub module', relevantSubModules[i].name); } } - return rd; + // place data on auction adUnits + const mergedTargeting = deepMerge(targeting); + auction.adUnits.forEach(adUnit => { + const kv = adUnit.code && mergedTargeting[adUnit.code]; + if (!kv) { + return + } + adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = Object.assign(adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] || {}, kv); + }); + return auction.adUnits; } /** @@ -307,32 +349,5 @@ export function deepMerge(arr) { }, {}); } -/** - * run hook before bids request - * use hook to init submodules (after consent is set) - * @param {function} fn - hook function - * @param {Object} reqBidsConfigObj - request bids object - */ -export function requestBidsHook(fn, reqBidsConfigObj) { - initSubModules(); - return fn.call(this, reqBidsConfigObj); -} - -/** - * set data to primary ad server - * @param {Object} data - key values to set - */ -function setDataForPrimaryAdServer(data) { - if (utils.isGptPubadsDefined()) { - targeting.setTargetingForGPT(data, null) - } else { - window.googletag = window.googletag || {}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(() => { - targeting.setTargetingForGPT(data, null); - }); - } -} - module('realTimeData', attachRealTimeDataProvider); init(config); diff --git a/src/targeting.js b/src/targeting.js index 1b1e14fd4a6..1a18e249c0e 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -193,7 +193,8 @@ export function newTargeting(auctionManager) { // `alwaysUseBid=true`. If sending all bids is enabled, add targeting for losing bids. var targeting = getWinningBidTargeting(adUnitCodes, bidsReceived) .concat(getCustomBidTargeting(adUnitCodes, bidsReceived)) - .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)); + .concat(config.getConfig('enableSendAllBids') ? getBidLandscapeTargeting(adUnitCodes, bidsReceived) : getDealBids(adUnitCodes, bidsReceived)) + .concat(getAdUnitTargeting(adUnitCodes)); // store a reference of the targeting keys targeting.map(adUnitCode => { @@ -562,6 +563,27 @@ export function newTargeting(auctionManager) { }); } + function getAdUnitTargeting(adUnitCodes) { + function getTargetingObj(adUnit) { + return deepAccess(adUnit, CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING); + } + + function getTargetingValues(adUnit) { + const aut = getTargetingObj(adUnit); + + return Object.keys(aut) + .map(function(key) { + return {[key]: utils.isArray(aut[key]) ? aut[key] : aut[key].split(',')}; + }); + } + + return auctionManager.getAdUnits() + .filter(adUnit => includes(adUnitCodes, adUnit.code) && getTargetingObj(adUnit)) + .map(adUnit => { + return {[adUnit.code]: getTargetingValues(adUnit)} + }); + } + targeting.isApntagDefined = function() { if (window.apntag && utils.isFn(window.apntag.setKeywords)) { return true; diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js new file mode 100644 index 00000000000..d7318fa704f --- /dev/null +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -0,0 +1,88 @@ +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; +import {makeSlot} from '../integration/faker/googletag.js'; + +describe('browsi Real time data sub module', function () { + const conf = { + 'auctionDelay': 250, + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] + }; + + + beforeEach(function () { + browsiRTD.setModuleData(conf) + }); + + it('should init and return true', function () { + browsiRTD.collectData(); + expect(browsiRTD.browsiSubmodule.init()).to.equal(true) + }); + + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.dataProviders[0].params.keyName); + }); + + it('should match placement with ad unit', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); + + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true + + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); + + it('should return correct macro values', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); + + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); + + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); + + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); + }); + + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function () { + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + }); + + it('should return NA if no prediction for ad unit', function () { + makeSlot({ code: 'adMock', divId: 'browsiAd_2' }); + browsiRTD.setData({}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); + }); + + it('should return prediction from server', function () { + makeSlot({ code: 'hasPrediction', divId: 'hasPrediction' }); + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + }) + }) +}); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js new file mode 100644 index 00000000000..5c0ea3f0bae --- /dev/null +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -0,0 +1,158 @@ +import * as rtdModule from 'modules/rtdModule/index.js'; +import { config } from 'src/config.js'; +import * as sinon from 'sinon'; + +const getBidRequestDataSpy = sinon.spy(); + +const validSM = { + name: 'validSM', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad2': {'key': 'validSM'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getTargetingData: (adUnitsCodes) => { + return {'ad1': {'key': 'validSMWait'}} + }, + getBidRequestData: getBidRequestDataSpy +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 100, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +describe('Real time module', function () { + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + rtdModule.init(config); + rtdModule.initSubModules(); + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) + done(); + }) +}); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js deleted file mode 100644 index b25778633f3..00000000000 --- a/test/spec/modules/realTimeModule_spec.js +++ /dev/null @@ -1,246 +0,0 @@ -import * as rtdModule from 'modules/rtdModule/index.js'; -import { config } from 'src/config.js'; -import {makeSlot} from '../integration/faker/googletag.js'; -import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; - -const validSM = { - name: 'validSM', - init: () => { return true }, - addTargeting: (adUnits, onDone) => { - return onDone({'key': 'validSM'}) - } -}; - -const validSMWait = { - name: 'validSMWait', - init: () => { return true }, - addTargeting: (adUnits, onDone) => { - return onDone({'ad1': {'key': 'validSMWait'}}) - } -}; - -const invalidSM = { - name: 'invalidSM' -}; - -const failureSM = { - name: 'failureSM', - init: () => { return false } -}; - -const nonConfSM = { - name: 'nonConfSM', - init: () => { return true } -}; - -const conf = { - 'realTimeData': { - 'timeout': 100, - dataProviders: [ - { - 'name': 'validSMWait', - 'waitForIt': true, - }, - { - 'name': 'validSM', - 'waitForIt': false, - }, - { - 'name': 'invalidSM' - }, - { - 'name': 'failureSM' - }] - } -}; - -function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; -} - -describe('Real time module', function () { - after(function () { - config.resetConfig(); - }); - - beforeEach(function () { - config.setConfig(conf); - }); - - it('should use only valid modules', function () { - rtdModule.attachRealTimeDataProvider(validSM); - rtdModule.attachRealTimeDataProvider(invalidSM); - rtdModule.attachRealTimeDataProvider(failureSM); - rtdModule.attachRealTimeDataProvider(nonConfSM); - rtdModule.attachRealTimeDataProvider(validSMWait); - rtdModule.init(config); - rtdModule.initSubModules(); - expect(rtdModule.subModules).to.eql([validSMWait, validSM]); - }); - - it('should only wait for must have sub modules', function (done) { - rtdModule.getProviderData([], (data) => { - expect(data).to.eql({validSMWait: {'ad1': {'key': 'validSMWait'}}}); - done(); - }) - }); - - it('deep merge object', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; - - const merged = rtdModule.deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); - - it('check module using bidsBackCallback', function (done) { - // set slot - const slot = makeSlot({ code: '/code1', divId: 'ad1' }); - window.googletag.pubads().setSlots([slot]); - - function afterBidHook() { - expect(slot.getTargeting().length).to.equal(1); - expect(slot.getTargeting()[0].key).to.equal('validSMWait'); - done(); - } - rtdModule.setTargetsAfterRequestBids(afterBidHook, []); - }); -}); - -describe('browsi Real time data sub module', function () { - const conf = { - 'realTimeData': { - 'timeout': 250, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }] - } - }; - - beforeEach(function () { - config.setConfig(conf); - }); - - after(function () { - config.resetConfig(); - }); - - it('should init and return true', function () { - browsiRTD.beforeInit(config); - expect(browsiRTD.browsiSubmodule.init()).to.equal(true) - }); - - it('should create browsi script', function () { - const script = browsiRTD.addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - expect(script.prebidData.kn).to.equal(conf.realTimeData.dataProviders[0].params.keyName); - }); - - it('should match placement with ad unit', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true - const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false - const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - }); - - it('should return correct macro values', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - - slot.setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = browsiRTD.getMacroId({p: '/'}, slot); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = browsiRTD.getMacroId({}, slot); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); - expect(macroResultC).to.equal('/'); - }); - - describe('should return data to RTD module', function () { - it('should return empty if no ad units defined', function (done) { - browsiRTD.setData({}); - browsiRTD.browsiSubmodule.addTargeting([], onDone); - function onDone(data) { - expect(data).to.eql({}); - done(); - } - }); - - it('should return NA if no prediction for ad unit', function (done) { - const adUnits = [getAdUnitMock('adMock')]; - browsiRTD.setData({}); - browsiRTD.browsiSubmodule.addTargeting(adUnits, onDone); - function onDone(data) { - expect(data).to.eql({adMock: {bv: 'NA'}}); - done(); - } - }); - - it('should return prediction from server', function (done) { - const adUnits = [getAdUnitMock('hasPrediction')]; - const data = { - p: {'hasPrediction': {p: 0.234}}, - kn: 'bv', - pmd: undefined - }; - browsiRTD.setData(data); - browsiRTD.browsiSubmodule.addTargeting(adUnits, onDone); - function onDone(data) { - expect(data).to.eql({hasPrediction: {bv: '0.20'}}); - done(); - } - }) - }) -}); From 3fe672901149d585f39d6e3dc05652008d92282c Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 23 Sep 2020 18:14:17 -0500 Subject: [PATCH 25/40] return on data unavailable --- modules/audigentRtdProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 977423abdd4..3bb3d455912 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -66,6 +66,7 @@ function getSegmentsAsync(adUnits, onDone) { if (typeof userIds == 'undefined' || userIds == null) { onDone({}); + return; } const url = `https://seg.ad.gt/api/v1/rtb_segments`; From 3dc857c886c9b3020287d623e0b8359fbfa0bb7b Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 29 Sep 2020 00:06:51 -0500 Subject: [PATCH 26/40] api endpoint update --- modules/audigentRtdProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 3bb3d455912..f859c78b5b2 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -69,7 +69,7 @@ function getSegmentsAsync(adUnits, onDone) { return; } - const url = `https://seg.ad.gt/api/v1/rtb_segments`; + const url = `https://seg.halo.ad.gt/api/v1/rtb_segments`; ajax(url, { success: function (response, req) { From dd3a80ac082c0a6de46b3da21be015e95cd39fe6 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 30 Sep 2020 14:27:17 -0500 Subject: [PATCH 27/40] update audigent RTD provider for new spec --- modules/audigentRtdProvider.js | 71 +++++++++++++++++----------------- modules/audigentRtdProvider.md | 12 ++++++ 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index f859c78b5b2..d864b81ea4b 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -38,34 +38,47 @@ export function setData(data) { storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); } -function getSegments(adUnits, onDone) { +function addSegmentData(adUnits, data) { + adUnits.forEach(adUnit => { + if (adUnit.hasOwnProperty('bids')) { + adUnit.bids.forEach(bid => { + if (!bid.hasOwnProperty('realTimeData')) { + bid.realTimeData = Object(); + } + bid.realTimeData.audigent_segments = data; + }) + } + }) + + return adUnits; +} + +function getSegments(reqBidsConfigObj, callback, config, userConsent) { try { let jsonData = storage.getDataFromLocalStorage('__adgntseg'); if (jsonData) { let data = JSON.parse(jsonData); if (data.audigent_segments) { - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); + reqBidsConfigObj.adUnits = addSegmentData(reqBidsConfigObj.adUnits, data); + callback(reqBidsConfigObj.auctionId); return; } } - getSegmentsAsync(adUnits, onDone); + getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent); } catch (e) { - getSegmentsAsync(adUnits, onDone); + getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent); } } -function getSegmentsAsync(adUnits, onDone) { - const userIds = (getGlobal()).getUserIds(); +function getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent) { + let queryConfig = {} + if (typeof config == 'object' && config == null && Object.keys(config).length > 0) { + queryConfig = config + } + const userIds = (getGlobal()).getUserIds(); if (typeof userIds == 'undefined' || userIds == null) { - onDone({}); + callback(reqBidsConfigObj.auctionId); return; } @@ -77,33 +90,27 @@ function getSegmentsAsync(adUnits, onDone) { try { const data = JSON.parse(response); if (data && data.audigent_segments) { + reqBidsConfigObj.adUnits = addSegmentData(reqBidsConfigObj.adUnits, data); + callback(reqBidsConfigObj.auctionId); setData(data); - let dataToReturn = adUnits.reduce((rp, cau) => { - const adUnitCode = cau && cau.code; - if (!adUnitCode) { return rp } - rp[adUnitCode] = data; - return rp; - }, {}); - - onDone(dataToReturn); } else { - onDone({}); + callback(reqBidsConfigObj.auctionId); } } catch (err) { utils.logError('unable to parse audigent segment data'); - onDone({}) + callback(reqBidsConfigObj.auctionId); } } else if (req.status === 204) { - // unrecognized site key - onDone({}); + // unrecognized partner config + callback(reqBidsConfigObj.auctionId); } }, error: function () { - onDone({}); + callback(reqBidsConfigObj.auctionId); utils.logError('unable to get audigent segment data'); } }, - JSON.stringify(userIds), + JSON.stringify({'userIds': userIds, 'config': queryConfig}), {contentType: 'application/json'} ); } @@ -115,13 +122,7 @@ export const audigentSubmodule = { * @type {string} */ name: 'audigent', - /** - * get data and send back to realTimeData module - * @function - * @param {adUnit[]} adUnits - * @param {function} onDone - */ - getData: getSegments + getBidRequestData: getSegments }; export function init(config) { diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index 47bcbbbf951..683cdb2157e 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -13,6 +13,18 @@ Compile the audigent RTD module into your Prebid build: `gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` +Configure Prebid to add the Audigent RTD Segment Handler: +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [{name: "audigent"}] + } + ... +} +``` + Audigent segments will then be attached to each bid request objects in `bid.realTimeData.audigent_segments` From 5c9d00b2302f6c31837b7ec58b83660a124fdfdc Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 4 Oct 2020 09:05:57 +0300 Subject: [PATCH 28/40] design changes --- modules/browsiRtdProvider.js | 46 +++++++------------- modules/rtdModule/index.js | 45 +++++++++---------- test/spec/modules/browsiRtdProvider_spec.js | 35 +++++++-------- test/spec/modules/realTimeDataModule_spec.js | 14 +++--- 4 files changed, 58 insertions(+), 82 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index c0fc27dca17..4ee338e94cc 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -15,18 +15,15 @@ * @property {?string} keyName */ -import {config} from '../src/config.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import find from 'core-js-pure/features/array/find.js'; const storage = getStorageManager(); -/** @type {string} */ -const MODULE_NAME = 'realTimeData'; /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ @@ -88,13 +85,17 @@ export function setData(data) { function sendDataToModule(adUnitsCodes) { try { const _predictions = (_predictionsData && _predictionsData.p) || {}; - let dataToReturn = adUnitsCodes.reduce((rp, adUnitCode) => { - if (!adUnitCode) { return rp } + return adUnitsCodes.reduce((rp, adUnitCode) => { + if (!adUnitCode) { + return rp + } const adSlot = getSlotByCode(adUnitCode); const identifier = adSlot ? getMacroId(_predictionsData['pmd'], adSlot) : adUnitCode; const predictionData = _predictions[identifier]; rp[adUnitCode] = getKVObject(-1, _predictionsData['kn']); - if (!predictionData) { return rp } + if (!predictionData) { + return rp + } if (predictionData.p) { if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { return rp; @@ -103,7 +104,6 @@ function sendDataToModule(adUnitsCodes) { } return rp; }, {}); - return dataToReturn; } catch (e) { return {}; } @@ -254,33 +254,17 @@ export const browsiSubmodule = { init: init, }; -function init() { - return true; -} - -function beforeInit(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - setModuleData(realTimeData); - confListener(); - if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - collectData(); - } else { - utils.logError('missing params for Browsi provider'); - } - }); -} - -export function setModuleData(realTimeData) { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( - pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; - } catch (e) { - _moduleParams = {}; +function init(moduleConfig) { + _moduleParams = moduleConfig.params; + if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { + collectData(); + } else { + utils.logError('missing params for Browsi provider'); } + return true; } function registerSubModule() { submodule('realTimeData', browsiSubmodule); } registerSubModule(); -beforeInit(config); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 19d370728b3..88af58b7c17 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -152,8 +152,8 @@ import {getGlobal} from '../../src/prebidGlobal.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; -/** @type {boolean} */ -let _smInit = false; +/** @type {RtdSubmodule[]} */ +let registeredSubModules = []; /** @type {RtdSubmodule[]} */ export let subModules = []; /** @type {ModuleConfig} */ @@ -168,7 +168,7 @@ let _userConsent; * @param {RtdSubmodule} submodule */ export function attachRealTimeDataProvider(submodule) { - subModules.push(submodule); + registeredSubModules.push(submodule); } export function init(config) { @@ -182,33 +182,33 @@ export function init(config) { _dataProviders = realTimeData.dataProviders; setEventsListeners(); getGlobal().requestBids.before(setBidRequestsData, 40); + initSubModules(); }); } +function getConsentData() { + return { + gdpr: gdprDataHandler.getConsentData(), + usp: uspDataHandler.getConsentData(), + coppa: !!(config.getConfig('coppa')) + } +} + /** * call each sub module init function by config order * if no init function / init return failure / module not configured - remove it from submodules list */ -export function initSubModules() { - if (_smInit) { - // only need to init once - return; - } +function initSubModules() { + _userConsent = getConsentData(); let subModulesByOrder = []; - _userConsent = { - gdpr: gdprDataHandler.getConsentData(), - usp: uspDataHandler.getConsentData(), - coppa: !!(config.getConfig('coppa')) - }; _dataProviders.forEach(provider => { - const sm = find(subModules, s => s.name === provider.name); + const sm = find(registeredSubModules, s => s.name === provider.name); const initResponse = sm && sm.init && sm.init(provider, _userConsent); if (initResponse) { subModulesByOrder.push(Object.assign(sm, {config: provider})); } }); subModules = subModulesByOrder; - _smInit = true; } /** @@ -235,15 +235,11 @@ function setEventsListeners() { * @param {function} fn required; The next function in the chain, used by hook.js */ export function setBidRequestsData(fn, reqBidsConfigObj) { - initSubModules(); - - // delay bidding process only if auctionDelay > 0 - if (!_moduleConfig.auctionDelay || _moduleConfig.auctionDelay < 1) { - return exitHook(); - } + _userConsent = getConsentData(); const relevantSubModules = subModules.filter(sm => typeof sm.getBidRequestData === 'function'); const prioritySubModules = relevantSubModules.filter(sm => !!(sm.config && sm.config.waitForIt)); + const shouldDelayAuction = prioritySubModules.length && _moduleConfig.auctionDelay && _moduleConfig.auctionDelay > 0; let callbacksExpected = prioritySubModules.length; let isDone = false; let waitTimeout; @@ -252,7 +248,7 @@ export function setBidRequestsData(fn, reqBidsConfigObj) { return exitHook(); } - if (prioritySubModules.length) { + if (shouldDelayAuction) { waitTimeout = setTimeout(exitHook, _moduleConfig.auctionDelay); } @@ -260,12 +256,11 @@ export function setBidRequestsData(fn, reqBidsConfigObj) { sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) }); - if (!prioritySubModules.length) { + if (!shouldDelayAuction) { return exitHook(); } - function onGetBidRequestDataCallback(auctionId) { - utils.logInfo('onGetBidRequestDataCallback', auctionId, this); + function onGetBidRequestDataCallback() { if (isDone) { return; } diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index d7318fa704f..ee37d16905b 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -3,26 +3,21 @@ import {makeSlot} from '../integration/faker/googletag.js'; describe('browsi Real time data sub module', function () { const conf = { - 'auctionDelay': 250, - dataProviders: [{ - 'name': 'browsi', - 'params': { - 'url': 'testUrl.com', - 'siteKey': 'testKey', - 'pubKey': 'testPub', - 'keyName': 'bv' - } - }] + 'auctionDelay': 250, + dataProviders: [{ + 'name': 'browsi', + 'params': { + 'url': 'testUrl.com', + 'siteKey': 'testKey', + 'pubKey': 'testPub', + 'keyName': 'bv' + } + }] }; - - beforeEach(function () { - browsiRTD.setModuleData(conf) - }); - it('should init and return true', function () { browsiRTD.collectData(); - expect(browsiRTD.browsiSubmodule.init()).to.equal(true) + expect(browsiRTD.browsiSubmodule.init(conf.dataProviders[0])).to.equal(true) }); it('should create browsi script', function () { @@ -34,7 +29,7 @@ describe('browsi Real time data sub module', function () { }); it('should match placement with ad unit', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true @@ -48,7 +43,7 @@ describe('browsi Real time data sub module', function () { }); it('should return correct macro values', function () { - const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); + const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); slot.setTargeting('test', ['test', 'value']); // slot getTargeting doesn't act like GPT so we can't expect real value @@ -69,13 +64,13 @@ describe('browsi Real time data sub module', function () { }); it('should return NA if no prediction for ad unit', function () { - makeSlot({ code: 'adMock', divId: 'browsiAd_2' }); + makeSlot({code: 'adMock', divId: 'browsiAd_2'}); browsiRTD.setData({}); expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'])).to.eql({adMock: {bv: 'NA'}}); }); it('should return prediction from server', function () { - makeSlot({ code: 'hasPrediction', divId: 'hasPrediction' }); + makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { p: {'hasPrediction': {p: 0.234}}, kn: 'bv', diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index 5c0ea3f0bae..b84aef15feb 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -58,6 +58,14 @@ const conf = { }; describe('Real time module', function () { + before(function () { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + }); + after(function () { config.resetConfig(); }); @@ -67,13 +75,7 @@ describe('Real time module', function () { }); it('should use only valid modules', function () { - rtdModule.attachRealTimeDataProvider(validSM); - rtdModule.attachRealTimeDataProvider(invalidSM); - rtdModule.attachRealTimeDataProvider(failureSM); - rtdModule.attachRealTimeDataProvider(nonConfSM); - rtdModule.attachRealTimeDataProvider(validSMWait); rtdModule.init(config); - rtdModule.initSubModules(); expect(rtdModule.subModules).to.eql([validSMWait, validSM]); }); From 06b069efdfb32ff97bfe0a3b3d81cff3f10845a0 Mon Sep 17 00:00:00 2001 From: bretg Date: Fri, 9 Oct 2020 13:02:19 -0400 Subject: [PATCH 29/40] fix loop continuation --- modules/rtdModule/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 88af58b7c17..28c36fc38f0 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -298,7 +298,7 @@ export function getAdUnitTargeting(auction) { return; } let targeting = []; - for (let i = relevantSubModules.length; i--; i > 0) { + for (let i = relevantSubModules.length; i > 0; i--) { const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); if (smTargeting && typeof smTargeting === 'object') { targeting.push(smTargeting); From db41bf84f4091ded34e0ecbeb08c1f7d72563b80 Mon Sep 17 00:00:00 2001 From: bretg Date: Fri, 9 Oct 2020 13:46:24 -0400 Subject: [PATCH 30/40] proper fix this time --- modules/rtdModule/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 28c36fc38f0..d7c05ab1c1e 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -298,7 +298,7 @@ export function getAdUnitTargeting(auction) { return; } let targeting = []; - for (let i = relevantSubModules.length; i > 0; i--) { + for (let i=relevantSubModules.length-1; i >= 0; i--) { const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); if (smTargeting && typeof smTargeting === 'object') { targeting.push(smTargeting); From 6c4ec0d037338dba9854d5067188cda29cfab36e Mon Sep 17 00:00:00 2001 From: bretg Date: Fri, 9 Oct 2020 13:49:00 -0400 Subject: [PATCH 31/40] linter --- modules/rtdModule/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index d7c05ab1c1e..91b0f1937b9 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -298,7 +298,7 @@ export function getAdUnitTargeting(auction) { return; } let targeting = []; - for (let i=relevantSubModules.length-1; i >= 0; i--) { + for (let i = relevantSubModules.length - 1; i >= 0; i--) { const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); if (smTargeting && typeof smTargeting === 'object') { targeting.push(smTargeting); From a329e4ae7d08bb179eb2caba684582a1448ec053 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Mon, 12 Oct 2020 13:28:43 -0500 Subject: [PATCH 32/40] update rtd parameters, onDone semantics --- modules/audigentRtdProvider.js | 43 +++++++++++++++------------------- modules/audigentRtdProvider.md | 1 + 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index d864b81ea4b..747cf215dab 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -42,10 +42,7 @@ function addSegmentData(adUnits, data) { adUnits.forEach(adUnit => { if (adUnit.hasOwnProperty('bids')) { adUnit.bids.forEach(bid => { - if (!bid.hasOwnProperty('realTimeData')) { - bid.realTimeData = Object(); - } - bid.realTimeData.audigent_segments = data; + bid.audigent_segments = data; }) } }) @@ -53,32 +50,34 @@ function addSegmentData(adUnits, data) { return adUnits; } -function getSegments(reqBidsConfigObj, callback, config, userConsent) { +function getSegments(reqBidsConfigObj, onDone, config, userConsent) { + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + try { let jsonData = storage.getDataFromLocalStorage('__adgntseg'); if (jsonData) { let data = JSON.parse(jsonData); if (data.audigent_segments) { - reqBidsConfigObj.adUnits = addSegmentData(reqBidsConfigObj.adUnits, data); - callback(reqBidsConfigObj.auctionId); + addSegmentData(adUnits, data); + onDone(); return; } } - getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent); + getSegmentsAsync(adUnits, onDone, config, userConsent); } catch (e) { - getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent); + getSegmentsAsync(adUnits, onDone, config, userConsent); } } -function getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent) { - let queryConfig = {} +function getSegmentsAsync(adUnits, onDone, config, userConsent) { + let reqParams = {} if (typeof config == 'object' && config == null && Object.keys(config).length > 0) { - queryConfig = config + reqParams = config.params } const userIds = (getGlobal()).getUserIds(); if (typeof userIds == 'undefined' || userIds == null) { - callback(reqBidsConfigObj.auctionId); + onDone(); return; } @@ -90,37 +89,33 @@ function getSegmentsAsync(reqBidsConfigObj, callback, config, userConsent) { try { const data = JSON.parse(response); if (data && data.audigent_segments) { - reqBidsConfigObj.adUnits = addSegmentData(reqBidsConfigObj.adUnits, data); - callback(reqBidsConfigObj.auctionId); + addSegmentData(adUnits, data); + onDone(); setData(data); } else { - callback(reqBidsConfigObj.auctionId); + onDone(); } } catch (err) { utils.logError('unable to parse audigent segment data'); - callback(reqBidsConfigObj.auctionId); + onDone(); } } else if (req.status === 204) { // unrecognized partner config - callback(reqBidsConfigObj.auctionId); + onDone(); } }, error: function () { - callback(reqBidsConfigObj.auctionId); + onDone(); utils.logError('unable to get audigent segment data'); } }, - JSON.stringify({'userIds': userIds, 'config': queryConfig}), + JSON.stringify({'userIds': userIds, 'config': reqParams}), {contentType: 'application/json'} ); } /** @type {RtdSubmodule} */ export const audigentSubmodule = { - /** - * used to link submodule with realTimeData - * @type {string} - */ name: 'audigent', getBidRequestData: getSegments }; diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index 683cdb2157e..62ad3d79ac6 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -19,6 +19,7 @@ pbjs.setConfig( ... realTimeData: { auctionDelay: 1000, + waitForIt: true, dataProviders: [{name: "audigent"}] } ... From 578049ea04150cff6599b1885bcbb93935964582 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Tue, 13 Oct 2020 12:47:30 +0300 Subject: [PATCH 33/40] reduce loops --- modules/rtdModule/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 91b0f1937b9..e235868f791 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -237,8 +237,19 @@ function setEventsListeners() { export function setBidRequestsData(fn, reqBidsConfigObj) { _userConsent = getConsentData(); - const relevantSubModules = subModules.filter(sm => typeof sm.getBidRequestData === 'function'); - const prioritySubModules = relevantSubModules.filter(sm => !!(sm.config && sm.config.waitForIt)); + const relevantSubModules = []; + const prioritySubModules = []; + subModules.forEach(sm => { + if (typeof sm.getBidRequestData !== 'function') { + return; + } + relevantSubModules.push(sm); + const config = sm.config; + if (config && config.waitForIt) { + prioritySubModules.push(sm); + } + }); + const shouldDelayAuction = prioritySubModules.length && _moduleConfig.auctionDelay && _moduleConfig.auctionDelay > 0; let callbacksExpected = prioritySubModules.length; let isDone = false; From aec311a79b8525805c89cc631a67e57f025e6358 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Tue, 13 Oct 2020 19:38:53 -0500 Subject: [PATCH 34/40] documentation update --- modules/audigentRtdProvider.md | 3 +-- package-lock.json | 47 ++++++++++++++-------------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index 62ad3d79ac6..912773766ad 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -8,7 +8,6 @@ targeting. Audigent maintains a large database of first-party Tradedesk Unified ID to third party segment mappings that can now be queried at bid-time. Usage: - Compile the audigent RTD module into your Prebid build: `gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` @@ -48,7 +47,7 @@ function addAudigentSegments() { for (i = 0; i < adUnits.length; i++) { let adUnit = adUnits[i]; for (j = 0; j < adUnit.bids.length; j++) { - adUnit.bids[j].userId.lipb.segments = adUnit.bids[j].realTimeData.audigent_segments['rubicon']; + adUnit.bids[j].userId.lipb.segments = adUnit.bids[j].audigent_segments['rubicon']; } } } diff --git a/package-lock.json b/package-lock.json index 1784b885be9..81ff16fdc5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.8.0-pre", + "version": "4.11.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7796,13 +7796,22 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", "object-inspect": "^1.7.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.0" + }, + "dependencies": { + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } } }, "es-array-method-boxes-properly": { @@ -7838,6 +7847,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -12076,7 +12086,7 @@ "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "dev": true, "requires": { - "has-symbols": "^1.0.1" + "has": "^1.0.3" } }, "is-relative": { @@ -17484,7 +17494,8 @@ "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-is": { "version": "1.1.2", @@ -20696,26 +20707,6 @@ "es-abstract": "^1.17.0-next.1" } }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", From b876ed016646b6e9c3bec6514317fe28276aa423 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 14 Oct 2020 10:40:05 -0500 Subject: [PATCH 35/40] working update to rtd3 spec, update segment example, documentation --- .../gpt/audigentSegments_example.html | 4 +-- modules/audigentRtdProvider.js | 25 ++++++------------- modules/audigentRtdProvider.md | 8 ++++-- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/integrationExamples/gpt/audigentSegments_example.html b/integrationExamples/gpt/audigentSegments_example.html index 874e3e081b1..1536ece9ab7 100644 --- a/integrationExamples/gpt/audigentSegments_example.html +++ b/integrationExamples/gpt/audigentSegments_example.html @@ -233,7 +233,7 @@ }, realTimeData: { auctionDelay: 1000, - dataProviders: [{name: "audigent"}] + dataProviders: [{name: "audigent", waitForIt: true}] } }); pbjs.addAdUnits(adUnits); @@ -242,7 +242,7 @@ function sendAdserverRequest() { document.getElementById('tdid').innerHTML = adUnits[0].bids[0].userId['tdid']; - document.getElementById('audigent_segments').innerHTML = JSON.stringify(adUnits[0].bids[0].realTimeData.audigent_segments); + document.getElementById('audigent_segments').innerHTML = JSON.stringify(adUnits[0].bids[0].audigent_segments); if (pbjs.adserverRequestSent) return; pbjs.adserverRequestSent = true; diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 747cf215dab..4415cd8f4ba 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -12,10 +12,8 @@ * @property {string} pubKey * @property {string} url * @property {?string} keyName - * @property {number} auctionDelay */ -import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; @@ -89,7 +87,7 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent) { try { const data = JSON.parse(response); if (data && data.audigent_segments) { - addSegmentData(adUnits, data); + addSegmentData(adUnits, data.audigent_segments); onDone(); setData(data); } else { @@ -114,23 +112,16 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent) { ); } +export function init(config) { + _moduleParams = {}; + return true; +} + /** @type {RtdSubmodule} */ export const audigentSubmodule = { name: 'audigent', - getBidRequestData: getSegments + getBidRequestData: getSegments, + init: init }; -export function init(config) { - const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { - try { - _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter(pr => pr.name && pr.name.toLowerCase() === 'audigent')[0].params; - _moduleParams.auctionDelay = realTimeData.auctionDelay; - } catch (e) { - _moduleParams = {}; - } - confListener(); - }); -} - submodule('realTimeData', audigentSubmodule); -init(config); diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index 912773766ad..dbe48eab1c5 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -18,8 +18,12 @@ pbjs.setConfig( ... realTimeData: { auctionDelay: 1000, - waitForIt: true, - dataProviders: [{name: "audigent"}] + dataProviders: [ + { + name: "audigent", + waitForIt: true + } + ] } ... } From ede56825b911788d2a7425d22f2e1b5ac9e59a89 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Wed, 14 Oct 2020 10:44:03 -0500 Subject: [PATCH 36/40] remove unused vars, reference module name --- modules/audigentRtdProvider.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 4415cd8f4ba..b12e5dbface 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -24,9 +24,7 @@ const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; - -/** @type {ModuleParams} */ -let _moduleParams = {}; +const SUBMODULE_NAME = 'audigent'; /** * XMLHttpRequest to get data form audigent server @@ -113,15 +111,14 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent) { } export function init(config) { - _moduleParams = {}; return true; } /** @type {RtdSubmodule} */ export const audigentSubmodule = { - name: 'audigent', + name: SUBMODULE_NAME, getBidRequestData: getSegments, init: init }; -submodule('realTimeData', audigentSubmodule); +submodule(MODULE_NAME, audigentSubmodule); From 71b97635978baa38fd0569e35379092b92bc8ced Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Thu, 15 Oct 2020 23:28:31 -0500 Subject: [PATCH 37/40] resolve haloid for segments --- modules/audigentRtdProvider.js | 51 +++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index b12e5dbface..b785aafb144 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -49,26 +49,14 @@ function addSegmentData(adUnits, data) { function getSegments(reqBidsConfigObj, onDone, config, userConsent) { const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; - try { - let jsonData = storage.getDataFromLocalStorage('__adgntseg'); - if (jsonData) { - let data = JSON.parse(jsonData); - if (data.audigent_segments) { - addSegmentData(adUnits, data); - onDone(); - return; - } + let jsonData = storage.getDataFromLocalStorage('__adgntseg'); + if (jsonData) { + let data = JSON.parse(jsonData); + if (data.audigent_segments) { + addSegmentData(adUnits, data.audigent_segments); + onDone(); + return; } - getSegmentsAsync(adUnits, onDone, config, userConsent); - } catch (e) { - getSegmentsAsync(adUnits, onDone, config, userConsent); - } -} - -function getSegmentsAsync(adUnits, onDone, config, userConsent) { - let reqParams = {} - if (typeof config == 'object' && config == null && Object.keys(config).length > 0) { - reqParams = config.params } const userIds = (getGlobal()).getUserIds(); @@ -77,8 +65,31 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent) { return; } - const url = `https://seg.halo.ad.gt/api/v1/rtb_segments`; + if (storage.getDataFromLocalStorage('auHaloId')) { + let haloId = storage.getDataFromLocalStorage('auHaloId'); + userIds.haloId = haloId; + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } else { + var script = document.createElement('script') + script.type = 'text/javascript'; + + script.onload = function() { + userIds.haloId = storage.getDataFromLocalStorage('auHaloId'); + getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); + } + script.src = 'https://id.halo.ad.gt/api/v1/haloid'; + document.getElementsByTagName('head')[0].appendChild(script); + } +} + +function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { + let reqParams = {} + if (typeof config == 'object' && config != null && Object.keys(config).length > 0) { + reqParams = config.params + } + + const url = `https://seg.halo.ad.gt/api/v1/rtb_segments`; ajax(url, { success: function (response, req) { if (req.status === 200) { From 0e061c4aeb12a769e4bfeabec0272e89b8b2ff9d Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Thu, 15 Oct 2020 23:32:58 -0500 Subject: [PATCH 38/40] update documentation to markdown --- modules/audigentRtdProvider.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index dbe48eab1c5..6729b7fad90 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -1,3 +1,5 @@ +## Audigent 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. @@ -7,7 +9,8 @@ attached to bid request objects destined for different SSPs in order to optimize targeting. Audigent maintains a large database of first-party Tradedesk Unified ID to third party segment mappings that can now be queried at bid-time. -Usage: +### Usage + Compile the audigent RTD module into your Prebid build: `gulp build --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` @@ -57,7 +60,9 @@ function addAudigentSegments() { } ``` -To view an example of the segments returned by Audigent's backends: +### Testing + +To view an example of available segments returned by Audigent's backends: `gulp serve --modules=userId,unifiedIdSystem,rtdModule,audigentRtdProvider,rubiconBidAdapter` From 2461a6ee526c7464936b56284288bf20c22a1d79 Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Thu, 15 Oct 2020 23:37:59 -0500 Subject: [PATCH 39/40] update description in documentation --- modules/audigentRtdProvider.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/audigentRtdProvider.md b/modules/audigentRtdProvider.md index 6729b7fad90..03e647f651d 100644 --- a/modules/audigentRtdProvider.md +++ b/modules/audigentRtdProvider.md @@ -4,10 +4,13 @@ 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 first-party Audigent segments that can be +This real-time data module provides quality user segmentation that can be attached to bid request objects destined for different SSPs in order to optimize targeting. Audigent maintains a large database of first-party Tradedesk Unified -ID to third party segment mappings that can now be queried at bid-time. +ID, Audigent Halo ID and other id provider mappings to various third-party +segment types that are utilizable across different SSPs. With this module, +these segments can be retrieved and supplied to the SSP in real-time during +the bid request cycle. ### Usage From b9a971c08665d4871e9aef69d9bf0c17c9b555ec Mon Sep 17 00:00:00 2001 From: Anthony Lauzon Date: Mon, 19 Oct 2020 13:42:18 -0500 Subject: [PATCH 40/40] minify optimizations --- modules/audigentRtdProvider.js | 50 ++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index b785aafb144..09b76cac0df 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -5,15 +5,6 @@ * @module modules/audigentRtdProvider * @requires module:modules/realTimeData */ - -/** - * @typedef {Object} ModuleParams - * @property {string} siteKey - * @property {string} pubKey - * @property {string} url - * @property {?string} keyName - */ - import {getGlobal} from '../src/prebidGlobal.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; @@ -25,15 +16,14 @@ const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'audigent'; +const HALOID_LOCAL_NAME = 'auHaloId'; +const SEG_LOCAL_NAME = '__adgntseg'; /** - * XMLHttpRequest to get data form audigent server - * @param {string} url server url with query params + * decorate adUnits with segment data + * @param {adUnit[]} adUnits + * @param {Object} data */ -export function setData(data) { - storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); -} - function addSegmentData(adUnits, data) { adUnits.forEach(adUnit => { if (adUnit.hasOwnProperty('bids')) { @@ -46,10 +36,17 @@ function addSegmentData(adUnits, data) { return adUnits; } +/** + * segment retrieval from audigent's backends + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + */ function getSegments(reqBidsConfigObj, onDone, config, userConsent) { const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; - let jsonData = storage.getDataFromLocalStorage('__adgntseg'); + let jsonData = storage.getDataFromLocalStorage(SEG_LOCAL_NAME); if (jsonData) { let data = JSON.parse(jsonData); if (data.audigent_segments) { @@ -65,8 +62,8 @@ function getSegments(reqBidsConfigObj, onDone, config, userConsent) { return; } - if (storage.getDataFromLocalStorage('auHaloId')) { - let haloId = storage.getDataFromLocalStorage('auHaloId'); + let haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); + if (haloId) { userIds.haloId = haloId; getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); } else { @@ -74,7 +71,7 @@ function getSegments(reqBidsConfigObj, onDone, config, userConsent) { script.type = 'text/javascript'; script.onload = function() { - userIds.haloId = storage.getDataFromLocalStorage('auHaloId'); + userIds.haloId = storage.getDataFromLocalStorage(HALOID_LOCAL_NAME); getSegmentsAsync(adUnits, onDone, config, userConsent, userIds); } @@ -83,6 +80,14 @@ function getSegments(reqBidsConfigObj, onDone, config, userConsent) { } } +/** + * async segment retrieval from audigent's backends + * @param {adUnit[]} adUnits + * @param {function} onDone + * @param {Object} config + * @param {Object} userConsent + * @param {Object} userIds + */ function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { let reqParams = {} if (typeof config == 'object' && config != null && Object.keys(config).length > 0) { @@ -98,7 +103,7 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { if (data && data.audigent_segments) { addSegmentData(adUnits, data.audigent_segments); onDone(); - setData(data); + storage.setDataInLocalStorage(SEG_LOCAL_NAME, JSON.stringify(data)); } else { onDone(); } @@ -121,6 +126,11 @@ function getSegmentsAsync(adUnits, onDone, config, userConsent, userIds) { ); } +/** + * module init + * @param {Object} config + * @return {boolean} + */ export function init(config) { return true; }