UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,179 lines (1,050 loc) 41.9 kB
import Adapter from '../../src/adapter.js'; import { createBid } from '../../src/bidfactory.js'; import * as utils from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; import adapterManager from '../../src/adapterManager.js'; import { config } from '../../src/config.js'; import { VIDEO, NATIVE } from '../../src/mediaTypes.js'; import { processNativeAdUnitParams } from '../../src/native.js'; import { isValid } from '../../src/adapters/bidderFactory.js'; import events from '../../src/events.js'; import includes from 'core-js-pure/features/array/includes.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; import find from 'core-js-pure/features/array/find.js'; import { getPrebidInternal } from '../../src/utils.js'; const getConfig = config.getConfig; const TYPE = CONSTANTS.S2S.SRC; let _syncCount = 0; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; const DEFAULT_S2S_NETREVENUE = true; let _s2sConfigs; let eidPermissions; /** * @typedef {Object} AdapterOptions * @summary s2sConfig parameter that adds arguments to resulting OpenRTB payload that goes to Prebid Server * @property {string} adapter * @property {boolean} enabled * @property {string} endpoint * @property {string} syncEndpoint * @property {number} timeout * @example * // example of multiple bidder configuration * pbjs.setConfig({ * s2sConfig: { * adapterOptions: { * rubicon: {singleRequest: false} * appnexus: {key: "value"} * } * } * }); */ /** * @typedef {Object} S2SDefaultConfig * @summary Base config properties for server to server header bidding * @property {string} [adapter='prebidServer'] adapter code to use for S2S * @property {boolean} [enabled=false] enables S2S bidding * @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` * @property {number} [maxBids=1] * @property {AdapterOptions} [adapterOptions] adds arguments to resulting OpenRTB payload to Prebid Server * @property {Object} [syncUrlModifier] */ /** * @typedef {S2SDefaultConfig} S2SConfig * @summary Configuration for server to server header bidding * @property {string[]} bidders bidders to request S2S * @property {string} endpoint endpoint to contact * @property {string} [defaultVendor] used as key to select the bidder's default config from ßprebidServer/config.js * @property {boolean} [cacheMarkup] whether to cache the adm result * @property {string} [syncEndpoint] endpoint URL for syncing cookies * @property {Object} [extPrebid] properties will be merged into request.ext.prebid */ /** * @type {S2SDefaultConfig} */ const s2sDefaultConfig = { timeout: 1000, maxBids: 1, adapter: 'prebidServer', adapterOptions: {}, syncUrlModifier: {} }; config.setDefaults({ 's2sConfig': s2sDefaultConfig }); /** * @param {S2SConfig} option * @return {boolean} */ function updateConfigDefaultVendor(option) { if (option.defaultVendor) { let vendor = option.defaultVendor; let optionKeys = Object.keys(option); if (S2S_VENDORS[vendor]) { // vendor keys will be set if either: the key was not specified by user // or if the user did not set their own distinct value (ie using the system default) to override the vendor Object.keys(S2S_VENDORS[vendor]).forEach((vendorKey) => { if (s2sDefaultConfig[vendorKey] === option[vendorKey] || !includes(optionKeys, vendorKey)) { option[vendorKey] = S2S_VENDORS[vendor][vendorKey]; } }); } else { utils.logError('Incorrect or unavailable prebid server default vendor option: ' + vendor); return false; } } // this is how we can know if user / defaultVendor has set it, or if we should default to false return option.enabled = typeof option.enabled === 'boolean' ? option.enabled : false; } /** * @param {S2SConfig} option * @return {boolean} */ function validateConfigRequiredProps(option) { const keys = Object.keys(option); if (['accountId', 'bidders', 'endpoint'].filter(key => { if (!includes(keys, key)) { utils.logError(key + ' missing in server to server config'); return true; } return false; }).length > 0) { return false; } } // temporary change to modify the s2sConfig for new format used for endpoint URLs; // could be removed later as part of a major release, if we decide to not support the old format function formatUrlParams(option) { ['endpoint', 'syncEndpoint'].forEach((prop) => { if (utils.isStr(option[prop])) { let temp = option[prop]; option[prop] = { p1Consent: temp, noP1Consent: temp }; } if (utils.isPlainObject(option[prop]) && (!option[prop].p1Consent || !option[prop].noP1Consent)) { ['p1Consent', 'noP1Consent'].forEach((conUrl) => { if (!option[prop][conUrl]) { utils.logWarn(`s2sConfig.${prop}.${conUrl} not defined. PBS request will be skipped in some P1 scenarios.`); } }); } }); } /** * @param {(S2SConfig[]|S2SConfig)} options */ function setS2sConfig(options) { if (!options) { return; } const normalizedOptions = Array.isArray(options) ? options : [options]; const activeBidders = []; const optionsValid = normalizedOptions.every((option, i, array) => { formatUrlParams(options); const updateSuccess = updateConfigDefaultVendor(option); if (updateSuccess !== false) { const valid = validateConfigRequiredProps(option); if (valid !== false) { if (Array.isArray(option['bidders'])) { array[i]['bidders'] = option['bidders'].filter(bidder => { if (activeBidders.indexOf(bidder) === -1) { activeBidders.push(bidder); return true; } return false; }); } return true; } } utils.logWarn('prebidServer: s2s config is disabled'); return false; }); if (optionsValid) { return _s2sConfigs = normalizedOptions; } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); /** * resets the _synced variable back to false, primiarily used for testing purposes */ export function resetSyncedStatus() { _syncCount = 0; } /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { if (_s2sConfigs.length === _syncCount) { return; } _syncCount++; const payload = { uuid: utils.generateUUID(), bidders: bidderCodes, account: s2sConfig.accountId }; let userSyncLimit = s2sConfig.userSyncLimit; if (utils.isNumber(userSyncLimit) && userSyncLimit > 0) { payload['limit'] = userSyncLimit; } if (gdprConsent) { payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0; // attempt to populate gdpr_consent if we know gdprApplies or it may apply if (gdprConsent.gdprApplies !== false) { payload.gdpr_consent = gdprConsent.consentString; } } // US Privacy (CCPA) support if (uspConsent) { payload.us_privacy = uspConsent; } if (typeof s2sConfig.coopSync === 'boolean') { payload.coopSync = s2sConfig.coopSync; } const jsonPayload = JSON.stringify(payload); ajax(getMatchingConsentUrl(s2sConfig.syncEndpoint, gdprConsent), (response) => { try { response = JSON.parse(response); doAllSyncs(response.bidder_status, s2sConfig); } catch (e) { utils.logError(e); } }, jsonPayload, { contentType: 'text/plain', withCredentials: true }); } function doAllSyncs(bidders, s2sConfig) { if (bidders.length === 0) { return; } // pull the syncs off the list in the order that prebid server sends them const thisSync = bidders.shift(); // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, utils.bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); } else { // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); } } /** * Modify the cookie sync url from prebid server to add new params. * * @param {string} type the type of sync, "image", "redirect", "iframe" * @param {string} url the url to sync * @param {string} bidder name of bidder doing sync for * @param {function} done an exit callback; to signify this pixel has either: finished rendering or something went wrong * @param {S2SConfig} s2sConfig */ function doPreBidderSync(type, url, bidder, done, s2sConfig) { if (s2sConfig.syncUrlModifier && typeof s2sConfig.syncUrlModifier[bidder] === 'function') { const newSyncUrl = s2sConfig.syncUrlModifier[bidder](type, url, bidder); doBidderSync(type, newSyncUrl, bidder, done) } else { doBidderSync(type, url, bidder, done) } } /** * Run a cookie sync for the given type, url, and bidder * * @param {string} type the type of sync, "image", "redirect", "iframe" * @param {string} url the url to sync * @param {string} bidder name of bidder doing sync for * @param {function} done an exit callback; to signify this pixel has either: finished rendering or something went wrong */ function doBidderSync(type, url, bidder, done) { if (!url) { utils.logError(`No sync url for bidder "${bidder}": ${url}`); done(); } else if (type === 'image' || type === 'redirect') { utils.logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`); utils.triggerPixel(url, done); } else if (type == 'iframe') { utils.logMessage(`Invoking iframe user sync for bidder: "${bidder}"`); utils.insertUserSyncIframe(url, done); } else { utils.logError(`User sync type "${type}" not supported for bidder: "${bidder}"`); done(); } } /** * Do client-side syncs for bidders. * * @param {Array} bidders a list of bidder names */ function doClientSideSyncs(bidders, gdprConsent, uspConsent) { bidders.forEach(bidder => { let clientAdapter = adapterManager.getBidAdapter(bidder); if (clientAdapter && clientAdapter.registerSyncs) { config.runWithBidder( bidder, utils.bind.call( clientAdapter.registerSyncs, clientAdapter, [], gdprConsent, uspConsent ) ); } }); } function _appendSiteAppDevice(request, pageUrl, accountId) { if (!request) return; // ORTB specifies app OR site if (typeof config.getConfig('app') === 'object') { request.app = config.getConfig('app'); request.app.publisher = {id: accountId} } else { request.site = {}; if (utils.isPlainObject(config.getConfig('site'))) { request.site = config.getConfig('site'); } // set publisher.id if not already defined if (!utils.deepAccess(request.site, 'publisher.id')) { utils.deepSetValue(request.site, 'publisher.id', accountId); } // set site.page if not already defined if (!request.site.page) { request.site.page = pageUrl; } } if (typeof config.getConfig('device') === 'object') { request.device = config.getConfig('device'); } if (!request.device) { request.device = {}; } if (!request.device.w) { request.device.w = window.innerWidth; } if (!request.device.h) { request.device.h = window.innerHeight; } } function addBidderFirstPartyDataToRequest(request) { const bidderConfig = config.getBidderConfig(); const fpdConfigs = Object.keys(bidderConfig).reduce((acc, bidder) => { const currBidderConfig = bidderConfig[bidder]; if (currBidderConfig.ortb2) { const ortb2 = {}; if (currBidderConfig.ortb2.site) { ortb2.site = currBidderConfig.ortb2.site; } if (currBidderConfig.ortb2.user) { ortb2.user = currBidderConfig.ortb2.user; } acc.push({ bidders: [ bidder ], config: { ortb2 } }); } return acc; }, []); if (fpdConfigs.length) { utils.deepSetValue(request, 'ext.prebid.bidderconfig', fpdConfigs); } } // https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40 let nativeDataIdMap = { sponsoredBy: 1, // sponsored body: 2, // desc rating: 3, likes: 4, downloads: 5, price: 6, salePrice: 7, phone: 8, address: 9, body2: 10, // desc2 cta: 12 // ctatext }; let nativeDataNames = Object.keys(nativeDataIdMap); let nativeImgIdMap = { icon: 1, image: 3 }; let nativeEventTrackerEventMap = { impression: 1, 'viewable-mrc50': 2, 'viewable-mrc100': 3, 'viewable-video50': 4, }; let nativeEventTrackerMethodMap = { img: 1, js: 2 }; // enable reverse lookup [ nativeDataIdMap, nativeImgIdMap, nativeEventTrackerEventMap, nativeEventTrackerMethodMap ].forEach(map => { Object.keys(map).forEach(key => { map[map[key]] = key; }); }); /* * Protocol spec for OpenRTB endpoint * e.g., https://<prebid-server-url>/v1/openrtb2/auction */ let bidIdMap = {}; let nativeAssetCache = {}; // store processed native params to preserve /** * map wurl to auction id and adId for use in the BID_WON event */ let wurlMap = {}; /** * @param {string} auctionId * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() * @param {string} wurl events.winurl passed from prebidServer as wurl */ function addWurl(auctionId, adId, wurl) { if ([auctionId, adId].every(utils.isStr)) { wurlMap[`${auctionId}${adId}`] = wurl; } } function getPbsResponseData(bidderRequests, response, pbsName, pbjsName) { const bidderValues = utils.deepAccess(response, `ext.${pbsName}`); if (bidderValues) { Object.keys(bidderValues).forEach(bidder => { let biddersReq = find(bidderRequests, bidderReq => bidderReq.bidderCode === bidder); if (biddersReq) { biddersReq[pbjsName] = bidderValues[bidder]; } }); } } /** * @param {string} auctionId * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() */ function removeWurl(auctionId, adId) { if ([auctionId, adId].every(utils.isStr)) { wurlMap[`${auctionId}${adId}`] = undefined; } } /** * @param {string} auctionId * @param {string} adId generated value set to bidObject.adId by bidderFactory Bid() * @return {(string|undefined)} events.winurl which was passed as wurl */ function getWurl(auctionId, adId) { if ([auctionId, adId].every(utils.isStr)) { return wurlMap[`${auctionId}${adId}`]; } } /** * remove all cached wurls */ export function resetWurlMap() { wurlMap = {}; } const OPEN_RTB_PROTOCOL = { buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; // transform ad unit into array of OpenRTB impression objects let impIds = new Set(); adUnits.forEach(adUnit => { // in case there is a duplicate imp.id, add '-2' suffix to the second imp.id. // e.g. if there are 2 adUnits (case of twin adUnit codes) with code 'test', // first imp will have id 'test' and second imp will have id 'test-2' let impressionId = adUnit.code; let i = 1; while (impIds.has(impressionId)) { i++; impressionId = `${adUnit.code}-${i}`; } impIds.add(impressionId); const nativeParams = processNativeAdUnitParams(utils.deepAccess(adUnit, 'mediaTypes.native')); let nativeAssets; if (nativeParams) { try { nativeAssets = nativeAssetCache[impressionId] = Object.keys(nativeParams).reduce((assets, type) => { let params = nativeParams[type]; function newAsset(obj) { return Object.assign({ required: params.required ? 1 : 0 }, obj ? utils.cleanObj(obj) : {}); } switch (type) { case 'image': case 'icon': let imgTypeId = nativeImgIdMap[type]; let asset = utils.cleanObj({ type: imgTypeId, w: utils.deepAccess(params, 'sizes.0'), h: utils.deepAccess(params, 'sizes.1'), wmin: utils.deepAccess(params, 'aspect_ratios.0.min_width'), hmin: utils.deepAccess(params, 'aspect_ratios.0.min_height') }); if (!((asset.w && asset.h) || (asset.hmin && asset.wmin))) { throw 'invalid img sizes (must provide sizes or min_height & min_width if using aspect_ratios)'; } if (Array.isArray(params.aspect_ratios)) { // pass aspect_ratios as ext data I guess? asset.ext = { aspectratios: params.aspect_ratios.map( ratio => `${ratio.ratio_width}:${ratio.ratio_height}` ) } } assets.push(newAsset({ img: asset })); break; case 'title': if (!params.len) { throw 'invalid title.len'; } assets.push(newAsset({ title: { len: params.len } })); break; default: let dataAssetTypeId = nativeDataIdMap[type]; if (dataAssetTypeId) { assets.push(newAsset({ data: { type: dataAssetTypeId, len: params.len } })) } } return assets; }, []); } catch (e) { utils.logError('error creating native request: ' + String(e)) } } const videoParams = utils.deepAccess(adUnit, 'mediaTypes.video'); const bannerParams = utils.deepAccess(adUnit, 'mediaTypes.banner'); adUnit.bids.forEach(bid => { // OpenRTB response contains imp.id and bidder name. These are // combined to create a unique key for each bid since an id isn't returned bidIdMap[`${impressionId}${bid.bidder}`] = bid.bid_id; // check for and store valid aliases to add to the request if (adapterManager.aliasRegistry[bid.bidder]) { const bidder = adapterManager.bidderRegistry[bid.bidder]; // adding alias only if alias source bidder exists and alias isn't configured to be standalone // pbs adapter if (bidder && !bidder.getSpec().skipPbsAliasing) { aliases[bid.bidder] = adapterManager.aliasRegistry[bid.bidder]; } } }); let mediaTypes = {}; if (bannerParams && bannerParams.sizes) { const sizes = utils.parseSizesInput(bannerParams.sizes); // get banner sizes in form [{ w: <int>, h: <int> }, ...] const format = sizes.map(size => { const [ width, height ] = size.split('x'); const w = parseInt(width, 10); const h = parseInt(height, 10); return { w, h }; }); mediaTypes['banner'] = {format}; if (bannerParams.pos) mediaTypes['banner'].pos = bannerParams.pos; } if (!utils.isEmpty(videoParams)) { if (videoParams.context === 'outstream' && !videoParams.renderer && !adUnit.renderer) { // Don't push oustream w/o renderer to request object. utils.logError('Outstream bid without renderer cannot be sent to Prebid Server.'); } else { if (videoParams.context === 'instream' && !videoParams.hasOwnProperty('placement')) { videoParams.placement = 1; } mediaTypes['video'] = Object.keys(videoParams).filter(param => param !== 'context') .reduce((result, param) => { if (param === 'playerSize') { result.w = utils.deepAccess(videoParams, `${param}.0.0`); result.h = utils.deepAccess(videoParams, `${param}.0.1`); } else { result[param] = videoParams[param]; } return result; }, {}); } } if (nativeAssets) { try { mediaTypes['native'] = { request: JSON.stringify({ // TODO: determine best way to pass these and if we allow defaults context: 1, plcmttype: 1, eventtrackers: [ {event: 1, methods: [1]} ], // TODO: figure out how to support privacy field // privacy: int assets: nativeAssets }), ver: '1.2' } } catch (e) { utils.logError('error creating native request: ' + String(e)) } } // get bidder params in form { <bidder code>: {...params} } // initialize reduce function with the user defined `ext` properties on the ad unit const ext = adUnit.bids.reduce((acc, bid) => { const adapter = adapterManager.bidderRegistry[bid.bidder]; if (adapter && adapter.getSpec().transformBidParams) { bid.params = adapter.getSpec().transformBidParams(bid.params, true, adUnit, bidRequests); } acc[bid.bidder] = (s2sConfig.adapterOptions && s2sConfig.adapterOptions[bid.bidder]) ? Object.assign({}, bid.params, s2sConfig.adapterOptions[bid.bidder]) : bid.params; return acc; }, {...utils.deepAccess(adUnit, 'ortb2Imp.ext')}); const imp = { id: impressionId, ext, secure: s2sConfig.secure }; const ortb2 = {...utils.deepAccess(adUnit, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { /** * Prebid AdSlot * @type {(string|undefined)} */ if (prop === 'pbadslot') { if (typeof ortb2[prop] === 'string' && ortb2[prop]) { utils.deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); } else { // remove pbadslot property if it doesn't meet the spec delete imp.ext.data.pbadslot; } } else if (prop === 'adserver') { /** * Copy GAM AdUnit and Name to imp */ ['name', 'adslot'].forEach(name => { /** @type {(string|undefined)} */ const value = utils.deepAccess(ortb2, `adserver.${name}`); if (typeof value === 'string' && value) { utils.deepSetValue(imp, `ext.data.adserver.${name.toLowerCase()}`, value); } }); } else { utils.deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); Object.assign(imp, mediaTypes); // if storedAuctionResponse has been set, pass SRID const storedAuctionResponseBid = find(firstBidRequest.bids, bid => (bid.adUnitCode === adUnit.code && bid.storedAuctionResponse)); if (storedAuctionResponseBid) { utils.deepSetValue(imp, 'ext.prebid.storedauctionresponse.id', storedAuctionResponseBid.storedAuctionResponse.toString()); } const getFloorBid = find(firstBidRequest.bids, bid => bid.adUnitCode === adUnit.code && typeof bid.getFloor === 'function'); if (getFloorBid) { let floorInfo; try { floorInfo = getFloorBid.getFloor({ currency: config.getConfig('currency.adServerCurrency') || DEFAULT_S2S_CURRENCY, }); } catch (e) { utils.logError('PBS: getFloor threw an error: ', e); } if (floorInfo && floorInfo.currency && !isNaN(parseFloat(floorInfo.floor))) { imp.bidfloor = parseFloat(floorInfo.floor); imp.bidfloorcur = floorInfo.currency } } if (imp.banner || imp.video || imp.native) { imps.push(imp); } }); if (!imps.length) { utils.logError('Request to Prebid Server rejected due to invalid media type(s) in adUnit.'); return; } const request = { id: s2sBidRequest.tid, source: {tid: s2sBidRequest.tid}, tmax: s2sConfig.timeout, imp: imps, // to do: add setconfig option to pass test = 1 test: 0, ext: { prebid: { // set ext.prebid.auctiontimestamp with the auction timestamp. Data type is long integer. auctiontimestamp: firstBidRequest.auctionStart, targeting: { // includewinners is always true for openrtb includewinners: true, // includebidderkeys always false for openrtb includebidderkeys: false } } } }; // Sets pbjs version, can be overwritten below if channel exists in s2sConfig.extPrebid request.ext.prebid = Object.assign(request.ext.prebid, {channel: {name: 'pbjs', version: $$PREBID_GLOBAL$$.version}}) // set debug flag if in debug mode if (getConfig('debug')) { request.ext.prebid = Object.assign(request.ext.prebid, {debug: true}) } // s2sConfig video.ext.prebid is passed through openrtb to PBS if (s2sConfig.extPrebid && typeof s2sConfig.extPrebid === 'object') { request.ext.prebid = Object.assign(request.ext.prebid, s2sConfig.extPrebid); } /** * @type {(string[]|string|undefined)} - OpenRTB property 'cur', currencies available for bids */ const adServerCur = config.getConfig('currency.adServerCurrency'); if (adServerCur && typeof adServerCur === 'string') { // if the value is a string, wrap it with an array request.cur = [adServerCur]; } else if (Array.isArray(adServerCur) && adServerCur.length) { // if it's an array, get the first element request.cur = [adServerCur[0]]; } _appendSiteAppDevice(request, bidRequests[0].refererInfo.referer, s2sConfig.accountId); // pass schain object if it is present const schain = utils.deepAccess(bidRequests, '0.bids.0.schain'); if (schain) { request.source.ext = { schain: schain }; } if (!utils.isEmpty(aliases)) { request.ext.prebid.aliases = {...request.ext.prebid.aliases, ...aliases}; } const bidUserIdAsEids = utils.deepAccess(bidRequests, '0.bids.0.userIdAsEids'); if (utils.isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { utils.deepSetValue(request, 'user.ext.eids', bidUserIdAsEids); } if (utils.isArray(eidPermissions) && eidPermissions.length > 0) { if (requestedBidders && utils.isArray(requestedBidders)) { eidPermissions.forEach(i => { if (i.bidders) { i.bidders = i.bidders.filter(bidder => requestedBidders.includes(bidder)) } }); } utils.deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); } const multibid = config.getConfig('multibid'); if (multibid) { utils.deepSetValue(request, 'ext.prebid.multibid', multibid.reduce((result, i) => { let obj = {}; Object.keys(i).forEach(key => { obj[key.toLowerCase()] = i[key]; }); result.push(obj); return result; }, [])); } if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module let gdprApplies; if (typeof firstBidRequest.gdprConsent.gdprApplies === 'boolean') { gdprApplies = firstBidRequest.gdprConsent.gdprApplies ? 1 : 0; } utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); utils.deepSetValue(request, 'user.ext.consent', firstBidRequest.gdprConsent.consentString); if (firstBidRequest.gdprConsent.addtlConsent && typeof firstBidRequest.gdprConsent.addtlConsent === 'string') { utils.deepSetValue(request, 'user.ext.ConsentedProvidersSettings.consented_providers', firstBidRequest.gdprConsent.addtlConsent); } } // US Privacy (CCPA) support if (firstBidRequest.uspConsent) { utils.deepSetValue(request, 'regs.ext.us_privacy', firstBidRequest.uspConsent); } } if (getConfig('coppa') === true) { utils.deepSetValue(request, 'regs.coppa', 1); } const commonFpd = getConfig('ortb2') || {}; if (commonFpd.site) { utils.mergeDeep(request, {site: commonFpd.site}); } if (commonFpd.user) { utils.mergeDeep(request, {user: commonFpd.user}); } addBidderFirstPartyDataToRequest(request); return request; }, interpretResponse(response, bidderRequests, s2sConfig) { const bids = []; [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] .forEach(info => getPbsResponseData(bidderRequests, response, info[0], info[1])) if (response.seatbid) { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { let bidRequest; let key = `${bid.impid}${seatbid.seat}`; if (bidIdMap[key]) { bidRequest = utils.getBidRequest( bidIdMap[key], bidderRequests ); } const cpm = bid.price; const status = cpm !== 0 ? CONSTANTS.STATUS.GOOD : CONSTANTS.STATUS.NO_BID; let bidObject = createBid(status, bidRequest || { bidder: seatbid.seat, src: TYPE }); bidObject.cpm = cpm; // temporarily leaving attaching it to each bidResponse so no breaking change // BUT: this is a flat map, so it should be only attached to bidderRequest, a the change above does let serverResponseTimeMs = utils.deepAccess(response, ['ext', 'responsetimemillis', seatbid.seat].join('.')); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; } // Look for seatbid[].bid[].ext.prebid.bidid and place it in the bidResponse object for use in analytics adapters as 'pbsBidId' const bidId = utils.deepAccess(bid, 'ext.prebid.bidid'); if (utils.isStr(bidId)) { bidObject.pbsBidId = bidId; } // store wurl by auctionId and adId so it can be accessed from the BID_WON event handler if (utils.isStr(utils.deepAccess(bid, 'ext.prebid.events.win'))) { addWurl(bidRequest.auctionId, bidObject.adId, utils.deepAccess(bid, 'ext.prebid.events.win')); } let extPrebidTargeting = utils.deepAccess(bid, 'ext.prebid.targeting'); // If ext.prebid.targeting exists, add it as a property value named 'adserverTargeting' // The removal of hb_winurl and hb_bidid targeting values is temporary // once we get through the transition, this block will be removed. if (utils.isPlainObject(extPrebidTargeting)) { // If wurl exists, remove hb_winurl and hb_bidid targeting attributes if (utils.isStr(utils.deepAccess(bid, 'ext.prebid.events.win'))) { extPrebidTargeting = utils.getDefinedParams(extPrebidTargeting, Object.keys(extPrebidTargeting) .filter(i => (i.indexOf('hb_winurl') === -1 && i.indexOf('hb_bidid') === -1))); } bidObject.adserverTargeting = extPrebidTargeting; } bidObject.seatBidId = bid.id; if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; let sizes = bidRequest.sizes && bidRequest.sizes[0]; bidObject.playerWidth = sizes[0]; bidObject.playerHeight = sizes[1]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback if (bid.ext.prebid.cache && typeof bid.ext.prebid.cache.vastXml === 'object' && bid.ext.prebid.cache.vastXml.cacheId && bid.ext.prebid.cache.vastXml.url) { bidObject.videoCacheKey = bid.ext.prebid.cache.vastXml.cacheId; bidObject.vastUrl = bid.ext.prebid.cache.vastXml.url; } else if (extPrebidTargeting && extPrebidTargeting.hb_uuid && extPrebidTargeting.hb_cache_host && extPrebidTargeting.hb_cache_path) { bidObject.videoCacheKey = extPrebidTargeting.hb_uuid; // build url using key and cache host bidObject.vastUrl = `https://${extPrebidTargeting.hb_cache_host}${extPrebidTargeting.hb_cache_path}?uuid=${extPrebidTargeting.hb_uuid}`; } if (bid.adm) { bidObject.vastXml = bid.adm; } if (!bidObject.vastUrl && bid.nurl) { bidObject.vastUrl = bid.nurl; } } else if (utils.deepAccess(bid, 'ext.prebid.type') === NATIVE) { bidObject.mediaType = NATIVE; let adm; if (typeof bid.adm === 'string') { adm = bidObject.adm = JSON.parse(bid.adm); } else { adm = bidObject.adm = bid.adm; } let trackers = { [nativeEventTrackerMethodMap.img]: adm.imptrackers || [], [nativeEventTrackerMethodMap.js]: adm.jstracker ? [adm.jstracker] : [] }; if (adm.eventtrackers) { adm.eventtrackers.forEach(tracker => { switch (tracker.method) { case nativeEventTrackerMethodMap.img: trackers[nativeEventTrackerMethodMap.img].push(tracker.url); break; case nativeEventTrackerMethodMap.js: trackers[nativeEventTrackerMethodMap.js].push(tracker.url); break; } }); } if (utils.isPlainObject(adm) && Array.isArray(adm.assets)) { let origAssets = nativeAssetCache[bid.impid]; bidObject.native = utils.cleanObj(adm.assets.reduce((native, asset) => { let origAsset = origAssets[asset.id]; if (utils.isPlainObject(asset.img)) { native[origAsset.img.type ? nativeImgIdMap[origAsset.img.type] : 'image'] = utils.pick( asset.img, ['url', 'w as width', 'h as height'] ); } else if (utils.isPlainObject(asset.title)) { native['title'] = asset.title.text } else if (utils.isPlainObject(asset.data)) { nativeDataNames.forEach(dataType => { if (nativeDataIdMap[dataType] === origAsset.data.type) { native[dataType] = asset.data.value; } }); } return native; }, utils.cleanObj({ clickUrl: adm.link, clickTrackers: utils.deepAccess(adm, 'link.clicktrackers'), impressionTrackers: trackers[nativeEventTrackerMethodMap.img], javascriptTrackers: trackers[nativeEventTrackerMethodMap.js] }))); } else { utils.logError('prebid server native response contained no assets'); } } else { // banner if (bid.adm && bid.nurl) { bidObject.ad = bid.adm; bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bid.nurl)); } else if (bid.adm) { bidObject.ad = bid.adm; } else if (bid.nurl) { bidObject.adUrl = bid.nurl; } } bidObject.width = bid.w; bidObject.height = bid.h; if (bid.dealid) { bidObject.dealId = bid.dealid; } bidObject.requestId = bidRequest.bidId || bidRequest.bid_Id; bidObject.creative_id = bid.crid; bidObject.creativeId = bid.crid; if (bid.burl) { bidObject.burl = bid.burl; } bidObject.currency = (response.cur) ? response.cur : DEFAULT_S2S_CURRENCY; bidObject.meta = {}; let extPrebidMeta = utils.deepAccess(bid, 'ext.prebid.meta'); if (extPrebidMeta && utils.isPlainObject(extPrebidMeta)) { bidObject.meta = utils.deepClone(extPrebidMeta); } if (bid.adomain) { bidObject.meta.advertiserDomains = bid.adomain; } // the OpenRTB location for "TTL" as understood by Prebid.js is "exp" (expiration). const configTtl = s2sConfig.defaultTtl || DEFAULT_S2S_TTL; bidObject.ttl = (bid.exp) ? bid.exp : configTtl; bidObject.netRevenue = (bid.netRevenue) ? bid.netRevenue : DEFAULT_S2S_NETREVENUE; bids.push({ adUnit: bidRequest.adUnitCode, bid: bidObject }); }); }); } return bids; } }; /** * BID_WON event to request the wurl * @param {Bid} bid the winning bid object */ function bidWonHandler(bid) { const wurl = getWurl(bid.auctionId, bid.adId); if (utils.isStr(wurl)) { utils.logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`); utils.triggerPixel(wurl); // remove from wurl cache, since the wurl url was called removeWurl(bid.auctionId, bid.adId); } } function hasPurpose1Consent(gdprConsent) { let result = true; if (gdprConsent) { if (gdprConsent.gdprApplies && gdprConsent.apiVersion === 2) { result = !!(utils.deepAccess(gdprConsent, 'vendorData.purpose.consents.1') === true); } } return result; } function getMatchingConsentUrl(urlProp, gdprConsent) { return hasPurpose1Consent(gdprConsent) ? urlProp.p1Consent : urlProp.noP1Consent; } function getConsentData(bidRequests) { let gdprConsent, uspConsent; if (Array.isArray(bidRequests) && bidRequests.length > 0) { gdprConsent = bidRequests[0].gdprConsent; uspConsent = bidRequests[0].uspConsent; } return { gdprConsent, uspConsent }; } /** * Bidder adapter for Prebid Server */ export function PrebidServer() { const baseAdapter = new Adapter('prebidServer'); /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { const adUnits = utils.deepClone(s2sBidRequest.ad_units); let { gdprConsent, uspConsent } = getConsentData(bidRequests); // at this point ad units should have a size array either directly or mapped so filter for that const validAdUnits = adUnits.filter(unit => unit.mediaTypes && (unit.mediaTypes.native || (unit.mediaTypes.banner && unit.mediaTypes.banner.sizes) || (unit.mediaTypes.video && unit.mediaTypes.video.playerSize)) ); // in case config.bidders contains invalid bidders, we only process those we sent requests for const requestedBidders = validAdUnits .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(utils.uniques)) .reduce(utils.flatten) .filter(utils.uniques); if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { let syncBidders = s2sBidRequest.s2sConfig.bidders .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) .filter((bidder, index, array) => (array.indexOf(bidder) === index)); queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); } const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); const requestJson = request && JSON.stringify(request); utils.logInfo('BidRequest: ' + requestJson); const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); if (request && requestJson && endpointUrl) { ajax( endpointUrl, { success: response => handleResponse(response, requestedBidders, bidRequests, addBidResponse, done, s2sBidRequest.s2sConfig), error: done }, requestJson, { contentType: 'text/plain', withCredentials: true } ); } else { utils.logError('PBS request not made. Check endpoints.'); } } }; /* Notify Prebid of bid responses so bids can get in the auction */ function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done, s2sConfig) { let result; let bids = []; let { gdprConsent, uspConsent } = getConsentData(bidderRequests); try { result = JSON.parse(response); bids = OPEN_RTB_PROTOCOL.interpretResponse( result, bidderRequests, s2sConfig ); bids.forEach(({adUnit, bid}) => { if (isValid(adUnit, bid, bidderRequests)) { addBidResponse(adUnit, bid); } }); bidderRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); } catch (error) { utils.logError(error); } if (!result || (result.status && includes(result.status, 'Error'))) { utils.logError('error parsing response: ', result.status); } done(); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); } // Listen for bid won to call wurl events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); return Object.assign(this, { callBids: baseAdapter.callBids, setBidderCode: baseAdapter.setBidderCode, type: TYPE }); } /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included * @param {array} newEidPermissions */ function setEidPermissions(newEidPermissions) { eidPermissions = newEidPermissions; } getPrebidInternal().setEidPermissions = setEidPermissions; adapterManager.registerBidAdapter(new PrebidServer(), 'prebidServer');