UNPKG

mk9-prebid

Version:

Header Bidding Management Library

622 lines (546 loc) 22.1 kB
/** @module adaptermanger */ import { flatten, getBidderCodes, getDefinedParams, shuffle, timestamp, getBidderRequest, bind } from './utils.js'; import { getLabels, resolveStatus } from './sizeMapping.js'; import { processNativeAdUnitParams, nativeAdapters } from './native.js'; import { newBidder } from './adapters/bidderFactory.js'; import { ajaxBuilder } from './ajax.js'; import { config, RANDOM } from './config.js'; import { hook } from './hook.js'; import includes from 'core-js-pure/features/array/includes.js'; import find from 'core-js-pure/features/array/find.js'; import { adunitCounter } from './adUnits.js'; import { getRefererInfo } from './refererDetection.js'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); var events = require('./events.js'); let s2sTestingModule; // store s2sTesting module if it's loaded let adapterManager = {}; let _bidderRegistry = adapterManager.bidderRegistry = {}; let _aliasRegistry = adapterManager.aliasRegistry = {}; let _s2sConfigs = []; config.getConfig('s2sConfig', config => { if (config && config.s2sConfig) { _s2sConfigs = Array.isArray(config.s2sConfig) ? config.s2sConfig : [config.s2sConfig]; } }); var _analyticsRegistry = {}; /** * @typedef {object} LabelDescriptor * @property {boolean} labelAll describes whether or not this object expects all labels to match, or any label to match * @property {Array<string>} labels the labels listed on the bidder or adUnit * @property {Array<string>} activeLabels the labels specified as being active by requestBids */ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) { return adUnits.reduce((result, adUnit) => { let { active, mediaTypes: filteredMediaTypes, filterResults } = resolveStatus( getLabels(adUnit, labels), adUnit.mediaTypes, adUnit.sizes ); if (!active) { utils.logInfo(`Size mapping disabled adUnit "${adUnit.code}"`); } else if (filterResults) { utils.logInfo(`Size mapping filtered adUnit "${adUnit.code}" banner sizes from `, filterResults.before, 'to ', filterResults.after); } if (active) { result.push(adUnit.bids.filter(bid => bid.bidder === bidderCode) .reduce((bids, bid) => { const nativeParams = adUnit.nativeParams || utils.deepAccess(adUnit, 'mediaTypes.native'); if (nativeParams) { bid = Object.assign({}, bid, { nativeParams: processNativeAdUnitParams(nativeParams), }); } bid = Object.assign({}, bid, getDefinedParams(adUnit, [ 'ortb2Imp', 'mediaType', 'renderer', 'storedAuctionResponse' ])); let { active, mediaTypes, filterResults } = resolveStatus(getLabels(bid, labels), filteredMediaTypes); if (!active) { utils.logInfo(`Size mapping deactivated adUnit "${adUnit.code}" bidder "${bid.bidder}"`); } else if (filterResults) { utils.logInfo(`Size mapping filtered adUnit "${adUnit.code}" bidder "${bid.bidder}" banner sizes from `, filterResults.before, 'to ', filterResults.after); } if (utils.isValidMediaTypes(mediaTypes)) { bid = Object.assign({}, bid, { mediaTypes }); } else { utils.logError( `mediaTypes is not correctly configured for adunit ${adUnit.code}` ); } if (active) { bids.push(Object.assign({}, bid, { adUnitCode: adUnit.code, transactionId: adUnit.transactionId, sizes: utils.deepAccess(mediaTypes, 'banner.sizes') || utils.deepAccess(mediaTypes, 'video.playerSize') || [], bidId: bid.bid_id || utils.getUniqueIdentifierStr(), bidderRequestId, auctionId, src, bidRequestsCount: adunitCounter.getRequestsCounter(adUnit.code), bidderRequestsCount: adunitCounter.getBidderRequestsCounter(adUnit.code, bid.bidder), bidderWinsCount: adunitCounter.getBidderWinsCounter(adUnit.code, bid.bidder), })); } return bids; }, []) ); } return result; }, []).reduce(flatten, []).filter(val => val !== ''); } const hookedGetBids = hook('sync', getBids, 'getBids'); function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) { let adaptersServerSide = s2sConfig.bidders; let adUnitsCopy = utils.deepClone(adUnits); adUnitsCopy.forEach((adUnit) => { // filter out client side bids adUnit.bids = adUnit.bids.filter((bid) => { return includes(adaptersServerSide, bid.bidder) && (!doingS2STesting(s2sConfig) || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; }); }); // don't send empty requests adUnitsCopy = adUnitsCopy.filter(adUnit => { return adUnit.bids.length !== 0; }); return adUnitsCopy; } function getAdUnitCopyForClientAdapters(adUnits) { let adUnitsClientCopy = utils.deepClone(adUnits); // filter out s2s bids adUnitsClientCopy.forEach((adUnit) => { adUnit.bids = adUnit.bids.filter((bid) => { return !clientTestAdapters.length || bid.finalSource !== s2sTestingModule.SERVER; }) }); // don't send empty requests adUnitsClientCopy = adUnitsClientCopy.filter(adUnit => { return adUnit.bids.length !== 0; }); return adUnitsClientCopy; } export let gdprDataHandler = { consentData: null, setConsentData: function(consentInfo) { gdprDataHandler.consentData = consentInfo; }, getConsentData: function() { return gdprDataHandler.consentData; } }; export let uspDataHandler = { consentData: null, setConsentData: function(consentInfo) { uspDataHandler.consentData = consentInfo; }, getConsentData: function() { return uspDataHandler.consentData; } }; export let coppaDataHandler = { getCoppa: function() { return !!(config.getConfig('coppa')) } }; // export for testing export let clientTestAdapters = []; export const allS2SBidders = []; export function getAllS2SBidders() { adapterManager.s2STestingEnabled = false; _s2sConfigs.forEach(s2sConfig => { if (s2sConfig && s2sConfig.enabled) { if (s2sConfig.bidders && s2sConfig.bidders.length) { allS2SBidders.push(...s2sConfig.bidders); } } }) } adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, auctionId, cbTimeout, labels) { /** * emit and pass adunits for external modification * @see {@link https://github.com/prebid/Prebid.js/issues/4149|Issue} */ events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, adUnits); let bidderCodes = getBidderCodes(adUnits); if (config.getConfig('bidderSequence') === RANDOM) { bidderCodes = shuffle(bidderCodes); } const refererInfo = getRefererInfo(); let clientBidderCodes = bidderCodes; let bidRequests = []; if (allS2SBidders.length === 0) { getAllS2SBidders(); } _s2sConfigs.forEach(s2sConfig => { if (s2sConfig && s2sConfig.enabled) { if (doingS2STesting(s2sConfig)) { s2sTestingModule.calculateBidSources(s2sConfig); const bidderMap = s2sTestingModule.getSourceBidderMap(adUnits, allS2SBidders); // get all adapters doing client testing bidderMap[s2sTestingModule.CLIENT].forEach(bidder => { if (!includes(clientTestAdapters, bidder)) { clientTestAdapters.push(bidder); } }) } } }) // don't call these client side (unless client request is needed for testing) clientBidderCodes = bidderCodes.filter(bidderCode => { return !includes(allS2SBidders, bidderCode) || includes(clientTestAdapters, bidderCode) }); // these are called on the s2s adapter let adaptersServerSide = allS2SBidders; const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean( find(adUnits, adUnit => find(adUnit.bids, bid => ( bid.bidSource || (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder]) ) && bid.finalSource === s2sTestingModule.SERVER)) ); _s2sConfigs.forEach(s2sConfig => { if (s2sConfig && s2sConfig.enabled) { if ((isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig))) { utils.logWarn('testServerOnly: True. All client requests will be suppressed.'); clientBidderCodes.length = 0; } let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); let tid = utils.generateUUID(); adaptersServerSide.forEach(bidderCode => { const bidderRequestId = utils.getUniqueIdentifierStr(); const bidderRequest = { bidderCode, auctionId, bidderRequestId, tid, bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), auctionStart: auctionStart, timeout: s2sConfig.timeout, src: CONSTANTS.S2S.SRC, refererInfo }; if (bidderRequest.bids.length !== 0) { bidRequests.push(bidderRequest); } }); // update the s2sAdUnits object and remove all bids that didn't pass sizeConfig/label checks from getBids() // this is to keep consistency and only allow bids/adunits that passed the checks to go to pbs adUnitsS2SCopy.forEach((adUnitCopy) => { let validBids = adUnitCopy.bids.filter((adUnitBid) => find(bidRequests, request => find(request.bids, (reqBid) => reqBid.bidId === adUnitBid.bid_id))); adUnitCopy.bids = validBids; }); bidRequests.forEach(request => { if (request.adUnitsS2SCopy === undefined) { request.adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnitCopy => adUnitCopy.bids.length > 0); } }); } }) // client adapters let adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits); clientBidderCodes.forEach(bidderCode => { const bidderRequestId = utils.getUniqueIdentifierStr(); const bidderRequest = { bidderCode, auctionId, bidderRequestId, bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsClientCopy), labels, src: 'client'}), auctionStart: auctionStart, timeout: cbTimeout, refererInfo }; const adapter = _bidderRegistry[bidderCode]; if (!adapter) { utils.logError(`Trying to make a request for bidder that does not exist: ${bidderCode}`); } if (adapter && bidderRequest.bids && bidderRequest.bids.length !== 0) { bidRequests.push(bidderRequest); } }); if (gdprDataHandler.getConsentData()) { bidRequests.forEach(bidRequest => { bidRequest['gdprConsent'] = gdprDataHandler.getConsentData(); }); } if (uspDataHandler.getConsentData()) { bidRequests.forEach(bidRequest => { bidRequest['uspConsent'] = uspDataHandler.getConsentData(); }); } return bidRequests; }, 'makeBidRequests'); adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, requestCallbacks, requestBidsTimeout, onTimelyResponse) => { if (!bidRequests.length) { utils.logWarn('callBids executed with no bidRequests. Were they filtered by labels or sizing?'); return; } let [clientBidRequests, serverBidRequests] = bidRequests.reduce((partitions, bidRequest) => { partitions[Number(typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC)].push(bidRequest); return partitions; }, [[], []]); var uniqueServerBidRequests = []; serverBidRequests.forEach(serverBidRequest => { var index = -1; for (var i = 0; i < uniqueServerBidRequests.length; ++i) { if (serverBidRequest.tid === uniqueServerBidRequests[i].tid) { index = i; break; } } if (index <= -1) { uniqueServerBidRequests.push(serverBidRequest); } }); let counter = 0 _s2sConfigs.forEach((s2sConfig) => { if (s2sConfig && uniqueServerBidRequests[counter] && includes(s2sConfig.bidders, uniqueServerBidRequests[counter].bidderCode)) { // s2s should get the same client side timeout as other client side requests. const s2sAjax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { request: requestCallbacks.request.bind(null, 's2s'), done: requestCallbacks.done } : undefined); let adaptersServerSide = s2sConfig.bidders; const s2sAdapter = _bidderRegistry[s2sConfig.adapter]; let tid = uniqueServerBidRequests[counter].tid; let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.tid === tid) if (s2sAdapter) { let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy, s2sConfig}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); return doneCb.bind(bidRequest); }); // only log adapters that actually have adUnit bids let allBidders = s2sBidRequest.ad_units.reduce((adapters, adUnit) => { return adapters.concat((adUnit.bids || []).reduce((adapters, bid) => adapters.concat(bid.bidder), [])); }, []); utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => includes(allBidders, adapter)).join(',')}`); // fire BID_REQUESTED event for each s2s bidRequest uniqueServerRequests.forEach(bidRequest => { events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); }); // make bid requests s2sAdapter.callBids( s2sBidRequest, serverBidRequests, (adUnitCode, bid) => { let bidderRequest = getBidderRequest(serverBidRequests, bid.bidderCode, adUnitCode); if (bidderRequest) { addBidResponse.call(bidderRequest, adUnitCode, bid) } }, () => doneCbs.forEach(done => done()), s2sAjax ); } } else { utils.logError('missing ' + s2sConfig.adapter); } counter++ } }); // handle client adapter requests clientBidRequests.forEach(bidRequest => { bidRequest.start = timestamp(); // TODO : Do we check for bid in pool from here and skip calling adapter again ? const adapter = _bidderRegistry[bidRequest.bidderCode]; config.runWithBidder(bidRequest.bidderCode, () => { utils.logMessage(`CALLING BIDDER`); events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); }); let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { request: requestCallbacks.request.bind(null, bidRequest.bidderCode), done: requestCallbacks.done } : undefined); const adapterDone = doneCb.bind(bidRequest); try { config.runWithBidder( bidRequest.bidderCode, bind.call( adapter.callBids, adapter, bidRequest, addBidResponse.bind(bidRequest), adapterDone, ajax, onTimelyResponse, config.callbackWithBidder(bidRequest.bidderCode) ) ); } catch (e) { utils.logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); adapterDone(); } }); }; function doingS2STesting(s2sConfig) { return s2sConfig && s2sConfig.enabled && s2sConfig.testing && s2sTestingModule; } function isTestingServerOnly(s2sConfig) { return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly); }; function getSupportedMediaTypes(bidderCode) { let supportedMediaTypes = []; if (includes(adapterManager.videoAdapters, bidderCode)) supportedMediaTypes.push('video'); if (includes(nativeAdapters, bidderCode)) supportedMediaTypes.push('native'); return supportedMediaTypes; } adapterManager.videoAdapters = []; // added by adapterLoader for now adapterManager.registerBidAdapter = function (bidAdapter, bidderCode, {supportedMediaTypes = []} = {}) { if (bidAdapter && bidderCode) { if (typeof bidAdapter.callBids === 'function') { _bidderRegistry[bidderCode] = bidAdapter; if (includes(supportedMediaTypes, 'video')) { adapterManager.videoAdapters.push(bidderCode); } if (includes(supportedMediaTypes, 'native')) { nativeAdapters.push(bidderCode); } } else { utils.logError('Bidder adaptor error for bidder code: ' + bidderCode + 'bidder must implement a callBids() function'); } } else { utils.logError('bidAdapter or bidderCode not specified'); } }; adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { let existingAlias = _bidderRegistry[alias]; if (typeof existingAlias === 'undefined') { let bidAdapter = _bidderRegistry[bidderCode]; if (typeof bidAdapter === 'undefined') { // check if alias is part of s2sConfig and allow them to register if so (as base bidder may be s2s-only) const nonS2SAlias = []; _s2sConfigs.forEach(s2sConfig => { if (s2sConfig.bidders && s2sConfig.bidders.length) { const s2sBidders = s2sConfig && s2sConfig.bidders; if (!(s2sConfig && includes(s2sBidders, alias))) { nonS2SAlias.push(bidderCode); } else { _aliasRegistry[alias] = bidderCode; } } }); nonS2SAlias.forEach(bidderCode => { utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adapterManager.aliasBidAdapter'); }) } else { try { let newAdapter; let supportedMediaTypes = getSupportedMediaTypes(bidderCode); // Have kept old code to support backward compatibilitiy. // Remove this if loop when all adapters are supporting bidderFactory. i.e When Prebid.js is 1.0 if (bidAdapter.constructor.prototype != Object.prototype) { newAdapter = new bidAdapter.constructor(); newAdapter.setBidderCode(alias); } else { let spec = bidAdapter.getSpec(); let gvlid = options && options.gvlid; let skipPbsAliasing = options && options.skipPbsAliasing; newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; } adapterManager.registerBidAdapter(newAdapter, alias, { supportedMediaTypes }); } catch (e) { utils.logError(bidderCode + ' bidder does not currently support aliasing.', 'adapterManager.aliasBidAdapter'); } } } else { utils.logMessage('alias name "' + alias + '" has been already specified.'); } }; adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) { if (adapter && code) { if (typeof adapter.enableAnalytics === 'function') { adapter.code = code; _analyticsRegistry[code] = { adapter, gvlid }; } else { utils.logError(`Prebid Error: Analytics adaptor error for analytics "${code}" analytics adapter must implement an enableAnalytics() function`); } } else { utils.logError('Prebid Error: analyticsAdapter or analyticsCode not specified'); } }; adapterManager.enableAnalytics = function (config) { if (!utils.isArray(config)) { config = [config]; } utils._each(config, adapterConfig => { var adapter = _analyticsRegistry[adapterConfig.provider].adapter; if (adapter) { adapter.enableAnalytics(adapterConfig); } else { utils.logError(`Prebid Error: no analytics adapter found in registry for ${adapterConfig.provider}.`); } }); } adapterManager.getBidAdapter = function(bidder) { return _bidderRegistry[bidder]; }; adapterManager.getAnalyticsAdapter = function(code) { return _analyticsRegistry[code]; } // the s2sTesting module is injected when it's loaded rather than being imported // importing it causes the packager to include it even when it's not explicitly included in the build export function setS2STestingModule(module) { s2sTestingModule = module; } function tryCallBidderMethod(bidder, method, param) { try { const adapter = _bidderRegistry[bidder]; const spec = adapter.getSpec(); if (spec && spec[method] && typeof spec[method] === 'function') { utils.logInfo(`Invoking ${bidder}.${method}`); config.runWithBidder(bidder, bind.call(spec[method], spec, param)); } } catch (e) { utils.logWarn(`Error calling ${method} of ${bidder}`); } } adapterManager.callTimedOutBidders = function(adUnits, timedOutBidders, cbTimeout) { timedOutBidders = timedOutBidders.map((timedOutBidder) => { // Adding user configured params & timeout to timeout event data timedOutBidder.params = utils.getUserConfiguredParams(adUnits, timedOutBidder.adUnitCode, timedOutBidder.bidder); timedOutBidder.timeout = cbTimeout; return timedOutBidder; }); timedOutBidders = utils.groupBy(timedOutBidders, 'bidder'); Object.keys(timedOutBidders).forEach((bidder) => { tryCallBidderMethod(bidder, 'onTimeout', timedOutBidders[bidder]); }); } adapterManager.callBidWonBidder = function(bidder, bid, adUnits) { // Adding user configured params to bidWon event data bid.params = utils.getUserConfiguredParams(adUnits, bid.adUnitCode, bid.bidder); adunitCounter.incrementBidderWinsCounter(bid.adUnitCode, bid.bidder); tryCallBidderMethod(bidder, 'onBidWon', bid); }; adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; adapterManager.callBidViewableBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onBidViewable', bid); }; export default adapterManager;