From 6121b4ed320ce8da712deeaa5fac75dc4d131e81 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 25 Aug 2019 14:59:21 +0300 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 d45789ec693d57e99e9c826638480574b9b91e32 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Mon, 11 Oct 2021 13:37:14 +0300 Subject: [PATCH 17/19] browsi rtd - refresh predictions & bid request data --- modules/browsiRtdProvider.js | 104 ++++++++++++++++++-- modules/rtdModule/index.js | 4 +- test/spec/modules/browsiRtdProvider_spec.js | 59 ++++++++++- 3 files changed, 156 insertions(+), 11 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index d527223964e..205678c9971 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -15,12 +15,14 @@ * @property {?string} keyName */ -import { deepClone, logError, isGptPubadsDefined } from '../src/utils.js'; +import { deepClone, logError, isGptPubadsDefined, isNumber, isFn, deepSetValue } 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 find from 'core-js-pure/features/array/find.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import includes from 'core-js-pure/features/array/includes.js'; const storage = getStorageManager(); @@ -30,6 +32,10 @@ let _moduleParams = {}; let _predictionsData = null; /** @type {string} */ const DEF_KEYNAME = 'browsiViewability'; +/** @type {null | function} */ +let _dataReadyCallback = null; +/** @type {null|Object} */ +let _refreshCounter = {}; /** * add browsi script to page @@ -78,29 +84,49 @@ export function collectData() { getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); } +/** + * wait for data from server + * call callback when data is ready + * @param {function} callback + */ +function waitForData(callback) { + if (_predictionsData) { + _dataReadyCallback = null; + callback(_predictionsData); + } else { + _dataReadyCallback = callback; + } +} + export function setData(data) { _predictionsData = data; + if (isFn(_dataReadyCallback)) { + _dataReadyCallback(_predictionsData); + _dataReadyCallback = null; + } } -function sendDataToModule(adUnitsCodes) { +function getViewabilityByAdUnitCodes(adUnitsCodes) { try { const _predictions = (_predictionsData && _predictionsData.p) || {}; return adUnitsCodes.reduce((rp, adUnitCode) => { + _refreshCounter[adUnitCode] = _refreshCounter[adUnitCode] || 0; + const refreshCount = _refreshCounter[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']); + rp[adUnitCode] = getKVObject(-1); if (!predictionData) { return rp } - if (predictionData.p) { + if (predictionData.ps) { if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { return rp; } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + rp[adUnitCode] = getKVObject(getPredictionByRefreshCount(predictionData.ps, refreshCount)); } return rp; }, {}); @@ -109,6 +135,31 @@ function sendDataToModule(adUnitsCodes) { } } +/** + * get prediction according to refresh count + * return -1 if prediction not found + * @param {object} predictionObject + * @param {number} refreshCount + * @return {number} + */ +export function getPredictionByRefreshCount(predictionObject, refreshCount) { + if (!predictionObject || !isNumber(refreshCount)) { + return -1; + } + if (isNumber(predictionObject[refreshCount])) { + return predictionObject[refreshCount]; + } + if (Object.keys(predictionObject).length > 1) { + while (refreshCount > 0) { + refreshCount--; + if (isNumber(predictionObject[refreshCount])) { + return predictionObject[refreshCount]; + } + } + } + return -1; +} + /** * get all slots on page * @return {Object[]} slot GoogleTag slots @@ -122,12 +173,16 @@ function getAllSlots() { * @param {string?} keyName * @return {Object} key:value */ -function getKVObject(p, keyName) { +function getKVObject(p) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); + prObject[getViewabilityKey()] = prValue.toString(); return prObject; } + +function getViewabilityKey() { + return ((_moduleParams['keyName'] || (_predictionsData && _predictionsData['kn']) || DEF_KEYNAME).toString()) +} /** * check if placement id matches one of given ad units * @param {Object} slot google slot @@ -238,6 +293,28 @@ function toUrlParams(data) { .join('&'); } +function setBidRequestsData(bidObj, callback) { + let adUnitCodes = bidObj.adUnitCodes; + let adUnits = bidObj.adUnits || getGlobal().adUnits || []; + if (adUnitCodes) { + adUnits = adUnits.filter(au => includes(adUnitCodes, au.code)); + } else { + adUnitCodes = adUnits.map(au => au.code); + } + waitForData(() => { + const data = getViewabilityByAdUnitCodes(adUnitCodes); + if (data) { + adUnits.forEach(adUnit => { + const adUnitCode = adUnit.code; + if (data[adUnitCode]) { + deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[DEF_KEYNAME]: data[adUnitCode][getViewabilityKey()]}); + } + }); + } + callback(); + }) +} + /** @type {RtdSubmodule} */ export const browsiSubmodule = { /** @@ -250,10 +327,21 @@ export const browsiSubmodule = { * @function * @param {string[]} adUnitsCodes */ - getTargetingData: sendDataToModule, + getTargetingData: getTargetingData, init: init, + getBidRequestData: setBidRequestsData }; +function getTargetingData(adUnitCodes) { + const viewabilityByAdUnitCodes = getViewabilityByAdUnitCodes(adUnitCodes); + adUnitCodes.forEach(auc => { + if (isNumber(_refreshCounter[auc])) { + _refreshCounter[auc] = _refreshCounter[auc] + 1; + } + }); + return viewabilityByAdUnitCodes; +} + function init(moduleConfig) { _moduleParams = moduleConfig.params; if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 7dce09f0d1d..c5242c71946 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -42,10 +42,10 @@ * @function? * @summary modify bid request data * @name RtdSubmodule#getBidRequestData - * @param {SubmoduleConfig} config - * @param {UserConsentData} userConsent * @param {Object} reqBidsConfigObj * @param {function} callback + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index ee37d16905b..745533de00e 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -1,5 +1,6 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; import {makeSlot} from '../integration/faker/googletag.js'; +import * as utils from '../../../src/utils' describe('browsi Real time data sub module', function () { const conf = { @@ -72,7 +73,7 @@ describe('browsi Real time data sub module', function () { it('should return prediction from server', function () { makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { - p: {'hasPrediction': {p: 0.234}}, + p: {'hasPrediction': {ps: {0: 0.234}}}, kn: 'bv', pmd: undefined }; @@ -80,4 +81,60 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); }) }) + + describe('should return prediction according to refresh count', function () { + const predictions = { + 0: 0.123, + 1: 0.254, + 3: 0, + 4: 0.8 + } + const singlePrediction = { + 0: 0.123 + } + it('should return raw value for valid refresh count', function () { + expect(browsiRTD.getPredictionByRefreshCount(predictions, 0)).to.equal(0.123); + expect(browsiRTD.getPredictionByRefreshCount(predictions, 1)).to.equal(0.254); + }) + it('should return 0 for prediction = 0', function () { + expect(browsiRTD.getPredictionByRefreshCount(predictions, 3)).to.equal(0); + }) + it('should return -1 for invalid params', function () { + expect(browsiRTD.getPredictionByRefreshCount(null, 3)).to.equal(-1); + expect(browsiRTD.getPredictionByRefreshCount(predictions, null)).to.equal(-1); + }) + it('should return -1 for refresh count > 0 if prediction object length is 1 ', function () { + expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 0)).to.equal(0.123); + expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 1)).to.equal(-1); + expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 2)).to.equal(-1); + }) + it('should return last refresh prediction if there is no value for the refresh count', function () { + expect(browsiRTD.getPredictionByRefreshCount(predictions, 4)).to.equal(0.8); + expect(browsiRTD.getPredictionByRefreshCount(predictions, 5)).to.equal(0.8); + expect(browsiRTD.getPredictionByRefreshCount(predictions, 8)).to.equal(0.8); + }) + }) + describe('should set bid request data', function () { + const data = { + p: { + 'adUnit1': {ps: {0: 0.234}}, + 'adUnit2': {ps: {0: 0.134}}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + const fakeAdUnits = [ + { + code: 'adUnit1' + }, + { + code: 'adUnit2' + } + ] + browsiRTD.browsiSubmodule.getBidRequestData({adUnits: fakeAdUnits}, () => {}, {}, null); + it('should set ad unit params with prediction values', function () { + expect(utils.deepAccess(fakeAdUnits[0], 'ortb2Imp.ext.data.browsi')).to.eql({browsiViewability: '0.20'}); + expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({browsiViewability: '0.10'}); + }) + }) }); From c2abb48be181f569b9d06616650a1ff5c7daafa2 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Sun, 17 Oct 2021 10:46:10 +0300 Subject: [PATCH 18/19] Browsi RTD provider improvements --- modules/browsiRtdProvider.js | 82 ++++++++++----------- test/spec/modules/browsiRtdProvider_spec.js | 42 +++++------ 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 205678c9971..00e645ebe21 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -29,13 +29,13 @@ const storage = getStorageManager(); /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ -let _predictionsData = null; +let _browsiData = null; /** @type {string} */ const DEF_KEYNAME = 'browsiViewability'; /** @type {null | function} */ let _dataReadyCallback = null; /** @type {null|Object} */ -let _refreshCounter = {}; +let _ic = {}; /** * add browsi script to page @@ -90,43 +90,43 @@ export function collectData() { * @param {function} callback */ function waitForData(callback) { - if (_predictionsData) { + if (_browsiData) { _dataReadyCallback = null; - callback(_predictionsData); + callback(_browsiData); } else { _dataReadyCallback = callback; } } export function setData(data) { - _predictionsData = data; + _browsiData = data; if (isFn(_dataReadyCallback)) { - _dataReadyCallback(_predictionsData); + _dataReadyCallback(_browsiData); _dataReadyCallback = null; } } -function getViewabilityByAdUnitCodes(adUnitsCodes) { +function getRTD(auc) { try { - const _predictions = (_predictionsData && _predictionsData.p) || {}; - return adUnitsCodes.reduce((rp, adUnitCode) => { - _refreshCounter[adUnitCode] = _refreshCounter[adUnitCode] || 0; - const refreshCount = _refreshCounter[adUnitCode]; - if (!adUnitCode) { + const _bp = (_browsiData && _browsiData.p) || {}; + return auc.reduce((rp, uc) => { + _ic[uc] = _ic[uc] || 0; + const _c = _ic[uc]; + if (!uc) { return rp } - const adSlot = getSlotByCode(adUnitCode); - const identifier = adSlot ? getMacroId(_predictionsData['pmd'], adSlot) : adUnitCode; - const predictionData = _predictions[identifier]; - rp[adUnitCode] = getKVObject(-1); - if (!predictionData) { + const adSlot = getSlotByCode(uc); + const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; + const _pd = _bp[identifier]; + rp[uc] = getKVObject(-1); + if (!_pd) { return rp } - if (predictionData.ps) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { + if (_pd.ps) { + if (!isIdMatchingAdUnit(adSlot, _pd.w)) { return rp; } - rp[adUnitCode] = getKVObject(getPredictionByRefreshCount(predictionData.ps, refreshCount)); + rp[uc] = getKVObject(getCurrentData(_pd.ps, _c)); } return rp; }, {}); @@ -136,24 +136,24 @@ function getViewabilityByAdUnitCodes(adUnitsCodes) { } /** - * get prediction according to refresh count + * get prediction * return -1 if prediction not found * @param {object} predictionObject - * @param {number} refreshCount + * @param {number} _c * @return {number} */ -export function getPredictionByRefreshCount(predictionObject, refreshCount) { - if (!predictionObject || !isNumber(refreshCount)) { +export function getCurrentData(predictionObject, _c) { + if (!predictionObject || !isNumber(_c)) { return -1; } - if (isNumber(predictionObject[refreshCount])) { - return predictionObject[refreshCount]; + if (isNumber(predictionObject[_c])) { + return predictionObject[_c]; } if (Object.keys(predictionObject).length > 1) { - while (refreshCount > 0) { - refreshCount--; - if (isNumber(predictionObject[refreshCount])) { - return predictionObject[refreshCount]; + while (_c > 0) { + _c--; + if (isNumber(predictionObject[_c])) { + return predictionObject[_c]; } } } @@ -176,12 +176,12 @@ function getAllSlots() { function getKVObject(p) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[getViewabilityKey()] = prValue.toString(); + prObject[getKey()] = prValue.toString(); return prObject; } -function getViewabilityKey() { - return ((_moduleParams['keyName'] || (_predictionsData && _predictionsData['kn']) || DEF_KEYNAME).toString()) +function getKey() { + return ((_moduleParams['keyName'] || (_browsiData && _browsiData['kn']) || DEF_KEYNAME).toString()) } /** * check if placement id matches one of given ad units @@ -302,12 +302,12 @@ function setBidRequestsData(bidObj, callback) { adUnitCodes = adUnits.map(au => au.code); } waitForData(() => { - const data = getViewabilityByAdUnitCodes(adUnitCodes); + const data = getRTD(adUnitCodes); if (data) { adUnits.forEach(adUnit => { const adUnitCode = adUnit.code; if (data[adUnitCode]) { - deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[DEF_KEYNAME]: data[adUnitCode][getViewabilityKey()]}); + deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[DEF_KEYNAME]: data[adUnitCode][getKey()]}); } }); } @@ -332,14 +332,14 @@ export const browsiSubmodule = { getBidRequestData: setBidRequestsData }; -function getTargetingData(adUnitCodes) { - const viewabilityByAdUnitCodes = getViewabilityByAdUnitCodes(adUnitCodes); - adUnitCodes.forEach(auc => { - if (isNumber(_refreshCounter[auc])) { - _refreshCounter[auc] = _refreshCounter[auc] + 1; +function getTargetingData(uc) { + const targetingData = getRTD(uc); + uc.forEach(auc => { + if (isNumber(_ic[auc])) { + _ic[auc] = _ic[auc] + 1; } }); - return viewabilityByAdUnitCodes; + return targetingData; } function init(moduleConfig) { diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 745533de00e..9ea9a2f5135 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -30,11 +30,11 @@ 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: '/123/abc', 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 test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc', '/456/def']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/def']); // false const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true expect(test1).to.equal(true); @@ -44,12 +44,12 @@ 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: '/123/abc', 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'); + expect(macroResult).to.equal('/123/abc/NA'); const macroResultB = browsiRTD.getMacroId({}, slot); expect(macroResultB).to.equal('browsiAd_1'); @@ -82,7 +82,7 @@ describe('browsi Real time data sub module', function () { }) }) - describe('should return prediction according to refresh count', function () { + describe('should return matching prediction', function () { const predictions = { 0: 0.123, 1: 0.254, @@ -92,26 +92,24 @@ describe('browsi Real time data sub module', function () { const singlePrediction = { 0: 0.123 } - it('should return raw value for valid refresh count', function () { - expect(browsiRTD.getPredictionByRefreshCount(predictions, 0)).to.equal(0.123); - expect(browsiRTD.getPredictionByRefreshCount(predictions, 1)).to.equal(0.254); + it('should return raw value if valid', function () { + expect(browsiRTD.getCurrentData(predictions, 0)).to.equal(0.123); + expect(browsiRTD.getCurrentData(predictions, 1)).to.equal(0.254); }) it('should return 0 for prediction = 0', function () { - expect(browsiRTD.getPredictionByRefreshCount(predictions, 3)).to.equal(0); + expect(browsiRTD.getCurrentData(predictions, 3)).to.equal(0); }) it('should return -1 for invalid params', function () { - expect(browsiRTD.getPredictionByRefreshCount(null, 3)).to.equal(-1); - expect(browsiRTD.getPredictionByRefreshCount(predictions, null)).to.equal(-1); + expect(browsiRTD.getCurrentData(null, 3)).to.equal(-1); + expect(browsiRTD.getCurrentData(predictions, null)).to.equal(-1); }) - it('should return -1 for refresh count > 0 if prediction object length is 1 ', function () { - expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 0)).to.equal(0.123); - expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 1)).to.equal(-1); - expect(browsiRTD.getPredictionByRefreshCount(singlePrediction, 2)).to.equal(-1); - }) - it('should return last refresh prediction if there is no value for the refresh count', function () { - expect(browsiRTD.getPredictionByRefreshCount(predictions, 4)).to.equal(0.8); - expect(browsiRTD.getPredictionByRefreshCount(predictions, 5)).to.equal(0.8); - expect(browsiRTD.getPredictionByRefreshCount(predictions, 8)).to.equal(0.8); + it('should return prediction according to object keys length ', function () { + expect(browsiRTD.getCurrentData(singlePrediction, 0)).to.equal(0.123); + expect(browsiRTD.getCurrentData(singlePrediction, 1)).to.equal(-1); + expect(browsiRTD.getCurrentData(singlePrediction, 2)).to.equal(-1); + expect(browsiRTD.getCurrentData(predictions, 4)).to.equal(0.8); + expect(browsiRTD.getCurrentData(predictions, 5)).to.equal(0.8); + expect(browsiRTD.getCurrentData(predictions, 8)).to.equal(0.8); }) }) describe('should set bid request data', function () { From 303e2b8acacd5ca7cb8adb061d0a9ff090b4a259 Mon Sep 17 00:00:00 2001 From: omerdotan Date: Tue, 26 Oct 2021 10:26:42 +0300 Subject: [PATCH 19/19] bid request key fix --- modules/browsiRtdProvider.js | 2 +- test/spec/modules/browsiRtdProvider_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 00e645ebe21..a1943afda8d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -307,7 +307,7 @@ function setBidRequestsData(bidObj, callback) { adUnits.forEach(adUnit => { const adUnitCode = adUnit.code; if (data[adUnitCode]) { - deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[DEF_KEYNAME]: data[adUnitCode][getKey()]}); + deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[getKey()]: data[adUnitCode][getKey()]}); } }); } diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 9ea9a2f5135..32e1c7fe795 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -131,8 +131,8 @@ describe('browsi Real time data sub module', function () { ] browsiRTD.browsiSubmodule.getBidRequestData({adUnits: fakeAdUnits}, () => {}, {}, null); it('should set ad unit params with prediction values', function () { - expect(utils.deepAccess(fakeAdUnits[0], 'ortb2Imp.ext.data.browsi')).to.eql({browsiViewability: '0.20'}); - expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({browsiViewability: '0.10'}); + expect(utils.deepAccess(fakeAdUnits[0], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.20'}); + expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.10'}); }) }) });