From e5143d2360a16ea20b8d526874f2834ff6426233 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 17 Nov 2021 08:54:25 -0800 Subject: [PATCH 1/2] RTD: Add 'onBidRequest' event handler for RTD submodules https://github.com/prebid/Prebid.js/issues/7078 --- modules/rtdModule/index.js | 53 ++++-- test/spec/modules/realTimeDataModule_spec.js | 184 ++++++++++++++----- 2 files changed, 171 insertions(+), 66 deletions(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index c5242c71946..5ea5251a25b 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -143,7 +143,7 @@ import {config} from '../../src/config.js'; import {module} from '../../src/hook.js'; -import { logError, logWarn } from '../../src/utils.js'; +import {logError, logWarn} from '../../src/utils.js'; import events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; @@ -169,8 +169,43 @@ let _userConsent; */ export function attachRealTimeDataProvider(submodule) { registeredSubModules.push(submodule); + return function detach() { + const idx = registeredSubModules.indexOf(submodule) + if (idx >= 0) { + registeredSubModules.splice(idx, 1); + initSubModules(); + } + } } +/** + * call each sub module event function by config order + */ +const setEventsListeners = (function () { + let registered = false; + return function setEventsListeners() { + if (!registered) { + Object.entries({ + [CONSTANTS.EVENTS.AUCTION_INIT]: 'onAuctionInitEvent', + [CONSTANTS.EVENTS.AUCTION_END]: 'onAuctionEndEvent', + [CONSTANTS.EVENTS.BID_RESPONSE]: 'onBidResponseEvent', + [CONSTANTS.EVENTS.BID_REQUESTED]: 'onBidRequestEvent' + }).forEach(([ev, handler]) => { + events.on(ev, (args) => { + subModules.forEach(sm => { + try { + sm[handler] && sm[handler](args, sm.config, _userConsent) + } catch (e) { + logError(`RTD provider '${sm.name}': error in '${handler}':`, e); + } + }); + }) + }); + registered = true; + } + } +})(); + export function init(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { if (!realTimeData.dataProviders) { @@ -211,22 +246,6 @@ function initSubModules() { subModules = subModulesByOrder; } -/** - * call each sub module event function by config order - */ -function setEventsListeners() { - events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { - subModules.forEach(sm => { sm.onAuctionInitEvent && sm.onAuctionInitEvent(args, sm.config, _userConsent) }) - }); - events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { - getAdUnitTargeting(args); - subModules.forEach(sm => { sm.onAuctionEndEvent && sm.onAuctionEndEvent(args, sm.config, _userConsent) }) - }); - events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { - subModules.forEach(sm => { sm.onBidResponseEvent && sm.onBidResponseEvent(args, sm.config, _userConsent) }) - }); -} - /** * loop through configured data providers If the data provider has registered getBidRequestData, * call it, providing reqBidsConfigObj, consent data and module params diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index b84aef15feb..45559c5bc48 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -1,6 +1,8 @@ import * as rtdModule from 'modules/rtdModule/index.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import * as sinon from 'sinon'; +import {default as CONSTANTS} from '../../../src/constants.json'; +import {default as events} from '../../../src/events.js'; const getBidRequestDataSpy = sinon.spy(); @@ -58,33 +60,87 @@ const conf = { }; describe('Real time module', function () { - before(function () { - rtdModule.attachRealTimeDataProvider(validSM); - rtdModule.attachRealTimeDataProvider(invalidSM); - rtdModule.attachRealTimeDataProvider(failureSM); - rtdModule.attachRealTimeDataProvider(nonConfSM); - rtdModule.attachRealTimeDataProvider(validSMWait); - }); + let eventHandlers; + let sandbox; - after(function () { - config.resetConfig(); - }); + function mockEmitEvent(event, ...args) { + (eventHandlers[event] || []).forEach((h) => h(...args)); + } - beforeEach(function () { - config.setConfig(conf); + before(() => { + eventHandlers = {}; + sandbox = sinon.sandbox.create(); + sandbox.stub(events, 'on').callsFake((event, handler) => { + if (!eventHandlers.hasOwnProperty(event)) { + eventHandlers[event] = []; + } + eventHandlers[event].push(handler); + }); }); - it('should use only valid modules', function () { - rtdModule.init(config); - expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + after(() => { + sandbox.restore(); }); - it('should be able to modify bid request', function (done) { - rtdModule.setBidRequestsData(() => { - assert(getBidRequestDataSpy.calledTwice); - assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + describe('', () => { + const PROVIDERS = [validSM, invalidSM, failureSM, nonConfSM, validSMWait]; + let _detachers; + + beforeEach(function () { + _detachers = PROVIDERS.map(rtdModule.attachRealTimeDataProvider); + rtdModule.init(config); + config.setConfig(conf); + }); + + afterEach(function () { + _detachers.forEach((f) => f()); + config.resetConfig(); + }); + + it('should use only valid modules', function () { + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + }); + + it('should be able to modify bid request', function (done) { + rtdModule.setBidRequestsData(() => { + assert(getBidRequestDataSpy.calledTwice); + assert(getBidRequestDataSpy.calledWith({bidRequest: {}})); + done(); + }, {bidRequest: {}}) + }); + + it('sould place targeting on adUnits', function (done) { + const auction = { + adUnitCodes: ['ad1', 'ad2'], + adUnits: [ + { + code: 'ad1' + }, + { + code: 'ad2', + adserverTargeting: {preKey: 'preValue'} + } + ] + }; + + const expectedAdUnits = [ + { + code: 'ad1', + adserverTargeting: {key: 'validSMWait'} + }, + { + code: 'ad2', + adserverTargeting: { + preKey: 'preValue', + key: 'validSM' + } + } + ]; + + const adUnits = rtdModule.getAdUnitTargeting(auction); + assert.deepEqual(expectedAdUnits, adUnits) done(); - }, {bidRequest: {}}) + }) }); it('deep merge object', function () { @@ -125,36 +181,66 @@ describe('Real time module', function () { assert.deepEqual(expected, merged); }); - it('sould place targeting on adUnits', function (done) { - const auction = { - adUnitCodes: ['ad1', 'ad2'], - adUnits: [ - { - code: 'ad1' - }, - { - code: 'ad2', - adserverTargeting: {preKey: 'preValue'} - } - ] + describe('event', () => { + const EVENTS = { + [CONSTANTS.EVENTS.AUCTION_INIT]: 'onAuctionInitEvent', + [CONSTANTS.EVENTS.AUCTION_END]: 'onAuctionEndEvent', + [CONSTANTS.EVENTS.BID_RESPONSE]: 'onBidResponseEvent', + [CONSTANTS.EVENTS.BID_REQUESTED]: 'onBidRequestEvent' + } + const conf = { + 'realTimeData': { + dataProviders: [ + { + 'name': 'tp1', + }, + { + 'name': 'tp2' + } + ] + } }; + let providers; + let _detachers; - const expectedAdUnits = [ - { - code: 'ad1', - adserverTargeting: {key: 'validSMWait'} - }, - { - code: 'ad2', - adserverTargeting: { - preKey: 'preValue', - key: 'validSM' - } + function eventHandlingProvider(name) { + const provider = { + name: name, + init: () => true } - ]; - - const adUnits = rtdModule.getAdUnitTargeting(auction); - assert.deepEqual(expectedAdUnits, adUnits) - done(); + Object.values(EVENTS).forEach((ev) => provider[ev] = sinon.spy()); + return provider; + } + + beforeEach(() => { + providers = [eventHandlingProvider('tp1'), eventHandlingProvider('tp2')]; + _detachers = providers.map(rtdModule.attachRealTimeDataProvider); + rtdModule.init(config); + config.setConfig(conf); + }); + + afterEach(() => { + _detachers.forEach((d) => d()) + config.resetConfig(); + }) + + Object.entries(EVENTS).forEach(([event, hook]) => { + it(`'${event}' should be propagated to providers through '${hook}'`, () => { + const eventArg = {}; + mockEmitEvent(event, eventArg); + providers.forEach((provider) => { + const providerConf = conf.realTimeData.dataProviders.find((cfg) => cfg.name === provider.name); + expect(provider[hook].called).to.be.true; + expect(provider[hook].args).to.have.length(1); + expect(provider[hook].args[0]).to.include.members([eventArg, providerConf]) + }) + }); + + it(`${event} should not fail to propagate elsewhere if a provider throws in its event handler`, () => { + providers[0][hook] = function () { throw new Error() }; + mockEmitEvent(event); + expect(providers[1][hook].called).to.be.true; + }); + }); }) }); From 7854cb03183a35b36b46000816b211559756aaed Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 1 Dec 2021 07:21:31 -0800 Subject: [PATCH 2/2] RTD module: update documentation --- modules/rtdModule/index.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 5ea5251a25b..1efa89e329f 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -98,6 +98,15 @@ * @param {UserConsentData} userConsent */ +/** + * @function? + * @summary on bid requested event + * @name RtdSubmodule#onBidRequestEvent + * @param {Object} data + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + /** * @interface ModuleConfig */ @@ -164,8 +173,10 @@ let _dataProviders = []; let _userConsent; /** - * enable submodule in User ID + * Register a RTD submodule. + * * @param {RtdSubmodule} submodule + * @returns {function()} a de-registration function that will unregister the module when called. */ export function attachRealTimeDataProvider(submodule) { registeredSubModules.push(submodule);