From 6121b4ed320ce8da712deeaa5fac75dc4d131e81 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 25 Aug 2019 14:59:21 +0300 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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',