Skip to content

Commit

Permalink
RTD Module: add 'onBidRequest' event handler for RTD submodules (preb…
Browse files Browse the repository at this point in the history
…id#7729)

* RTD: Add 'onBidRequest' event handler for RTD submodules

prebid#7078

* RTD module: update documentation
  • Loading branch information
dgirardi authored and Chris Pabst committed Jan 10, 2022
1 parent 07c5491 commit 8687d93
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 67 deletions.
66 changes: 48 additions & 18 deletions modules/rtdModule/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -143,7 +152,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';
Expand All @@ -164,13 +173,50 @@ 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);
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) {
Expand Down Expand Up @@ -211,22 +257,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
Expand Down
184 changes: 135 additions & 49 deletions test/spec/modules/realTimeDataModule_spec.js
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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;
});
});
})
});

0 comments on commit 8687d93

Please sign in to comment.