UNPKG

mk9-prebid

Version:

Header Bidding Management Library

1,251 lines (1,120 loc) 43.4 kB
import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import find from 'core-js-pure/features/array/find.js'; import { Renderer } from '../src/Renderer.js'; import { getGlobal } from '../src/prebidGlobal.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.0.0.js'; // renderer code at https://github.com/rubicon-project/apex2 let rubiConf = {}; // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data config.getConfig('rubicon', config => { utils.mergeDeep(rubiConf, config.rubicon); }); const GVLID = 52; var sizeMap = { 1: '468x60', 2: '728x90', 5: '120x90', 7: '125x125', 8: '120x600', 9: '160x600', 10: '300x600', 13: '200x200', 14: '250x250', 15: '300x250', 16: '336x280', 17: '240x400', 19: '300x100', 31: '980x120', 32: '250x360', 33: '180x500', 35: '980x150', 37: '468x400', 38: '930x180', 39: '750x100', 40: '750x200', 41: '750x300', 42: '2x4', 43: '320x50', 44: '300x50', 48: '300x300', 53: '1024x768', 54: '300x1050', 55: '970x90', 57: '970x250', 58: '1000x90', 59: '320x80', 60: '320x150', 61: '1000x1000', 64: '580x500', 65: '640x480', 66: '930x600', 67: '320x480', 68: '1800x1000', 72: '320x320', 73: '320x160', 78: '980x240', 79: '980x300', 80: '980x400', 83: '480x300', 85: '300x120', 90: '548x150', 94: '970x310', 95: '970x100', 96: '970x210', 101: '480x320', 102: '768x1024', 103: '480x280', 105: '250x800', 108: '320x240', 113: '1000x300', 117: '320x100', 125: '800x250', 126: '200x600', 144: '980x600', 145: '980x150', 152: '1000x250', 156: '640x320', 159: '320x250', 179: '250x600', 195: '600x300', 198: '640x360', 199: '640x200', 213: '1030x590', 214: '980x360', 221: '1x1', 229: '320x180', 230: '2000x1400', 232: '580x400', 234: '6x6', 251: '2x2', 256: '480x820', 257: '400x600', 258: '500x200', 259: '998x200', 264: '970x1000', 265: '1920x1080', 274: '1800x200', 278: '320x500', 282: '320x400', 288: '640x380', 548: '500x1000', 550: '980x480', 552: '300x200', 558: '640x640' }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); export const spec = { code: 'rubicon', gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], /** * @param {object} bid * @return boolean */ isBidRequestValid: function (bid) { if (typeof bid.params !== 'object') { return false; } // validate account, site, zone have numeric values for (let i = 0, props = ['accountId', 'siteId', 'zoneId']; i < props.length; i++) { bid.params[props[i]] = parseInt(bid.params[props[i]]) if (isNaN(bid.params[props[i]])) { utils.logError('Rubicon: wrong format of accountId or siteId or zoneId.') return false } } let bidFormat = bidType(bid, true); // bidType is undefined? Return false if (!bidFormat) { return false; } else if (bidFormat === 'video') { // bidType is video, make sure it has required params return hasValidVideoParams(bid); } // bidType is banner? return true return true; }, /** * @param {BidRequest[]} bidRequests * @param bidderRequest * @return BidRequest[] */ buildRequests: function (bidRequests, bidderRequest) { // separate video bids because the requests are structured differently let requests = []; const videoRequests = bidRequests.filter(bidRequest => bidType(bidRequest) === 'video').map(bidRequest => { bidRequest.startTime = new Date().getTime(); const data = { id: bidRequest.transactionId, test: config.getConfig('debug') ? 1 : 0, cur: ['USD'], source: { tid: bidRequest.transactionId }, tmax: bidderRequest.timeout, imp: [{ exp: config.getConfig('s2sConfig.defaultTtl'), id: bidRequest.adUnitCode, secure: 1, ext: { [bidRequest.bidder]: bidRequest.params }, video: utils.deepAccess(bidRequest, 'mediaTypes.video') || {} }], ext: { prebid: { channel: { name: 'pbjs', version: $$PREBID_GLOBAL$$.version }, cache: { vastxml: { returnCreative: rubiConf.returnVast === true } }, targeting: { includewinners: true, // includebidderkeys always false for openrtb includebidderkeys: false, pricegranularity: getPriceGranularity(config) }, bidders: { rubicon: { integration: rubiConf.int_type || DEFAULT_PBS_INTEGRATION } } } } } // Add alias if it is there if (bidRequest.bidder !== 'rubicon') { data.ext.prebid.aliases = { [bidRequest.bidder]: 'rubicon' } } let modules = (getGlobal()).installedModules; if (modules && (!modules.length || modules.indexOf('rubiconAnalyticsAdapter') !== -1)) { utils.deepSetValue(data, 'ext.prebid.analytics', {'rubicon': {'client-analytics': true}}); } let bidFloor; if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { let floorInfo; try { floorInfo = bidRequest.getFloor({ currency: 'USD', mediaType: 'video', size: parseSizes(bidRequest, 'video') }); } catch (e) { utils.logError('Rubicon: getFloor threw an error: ', e); } bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; } else { bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); } if (!isNaN(bidFloor)) { data.imp[0].bidfloor = bidFloor; } // if value is set, will overwrite with same value data.imp[0].ext[bidRequest.bidder].video.size_id = determineRubiconVideoSizeId(bidRequest) appendSiteAppDevice(data, bidRequest, bidderRequest); addVideoParameters(data, bidRequest); if (bidderRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module let gdprApplies; if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; } utils.deepSetValue(data, 'regs.ext.gdpr', gdprApplies); utils.deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); } if (bidderRequest.uspConsent) { utils.deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); } const eids = utils.deepAccess(bidderRequest, 'bids.0.userIdAsEids'); if (eids && eids.length) { utils.deepSetValue(data, 'user.ext.eids', eids); } // set user.id value from config value const configUserId = config.getConfig('user.id'); if (configUserId) { utils.deepSetValue(data, 'user.id', configUserId); } if (config.getConfig('coppa') === true) { utils.deepSetValue(data, 'regs.coppa', 1); } if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } const multibid = config.getConfig('multibid'); if (multibid) { utils.deepSetValue(data, 'ext.prebid.multibid', multibid.reduce((result, i) => { let obj = {}; Object.keys(i).forEach(key => { obj[key.toLowerCase()] = i[key]; }); result.push(obj); return result; }, [])); } applyFPD(bidRequest, VIDEO, data); // if storedAuctionResponse has been set, pass SRID if (bidRequest.storedAuctionResponse) { utils.deepSetValue(data.imp[0], 'ext.prebid.storedauctionresponse.id', bidRequest.storedAuctionResponse.toString()); } // set ext.prebid.auctiontimestamp using auction time utils.deepSetValue(data.imp[0], 'ext.prebid.auctiontimestamp', bidderRequest.auctionStart); return { method: 'POST', url: `https://${rubiConf.videoHost || 'prebid-server'}.rubiconproject.com/openrtb2/auction`, data, bidRequest } }); if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = videoRequests.concat(bidRequests.filter(bidRequest => bidType(bidRequest) === 'banner').map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); return { method: 'GET', url: `https://${rubiConf.bannerHost || 'fastlane'}.rubiconproject.com/a/api/fastlane.json`, data: spec.getOrderedParams(bidParams).reduce((paramString, key) => { const propValue = bidParams[key]; return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; }, '') + `slots=1&rand=${Math.random()}`, bidRequest }; })); } else { // single request requires bids to be grouped by site id into a single request // note: utils.groupBy wasn't used because deep property access was needed const nonVideoRequests = bidRequests.filter(bidRequest => bidType(bidRequest) === 'banner'); const groupedBidRequests = nonVideoRequests.reduce((groupedBids, bid) => { (groupedBids[bid.params['siteId']] = groupedBids[bid.params['siteId']] || []).push(bid); return groupedBids; }, {}); // fastlane SRA has a limit of 10 slots const SRA_BID_LIMIT = 10; // multiple requests are used if bids groups have more than 10 bids requests = videoRequests.concat(Object.keys(groupedBidRequests).reduce((aggregate, bidGroupKey) => { // for each partioned bidGroup, append a bidRequest to requests list partitionArray(groupedBidRequests[bidGroupKey], SRA_BID_LIMIT).forEach(bidsInGroup => { const combinedSlotParams = spec.combineSlotUrlParams(bidsInGroup.map(bidRequest => { return spec.createSlotParams(bidRequest, bidderRequest); })); // SRA request returns grouped bidRequest arrays not a plain bidRequest aggregate.push({ method: 'GET', url: `https://${rubiConf.bannerHost || 'fastlane'}.rubiconproject.com/a/api/fastlane.json`, data: spec.getOrderedParams(combinedSlotParams).reduce((paramString, key) => { const propValue = combinedSlotParams[key]; return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; }, '') + `slots=${bidsInGroup.length}&rand=${Math.random()}`, bidRequest: bidsInGroup }); }); return aggregate; }, [])); } return requests; }, getOrderedParams: function(params) { const containsTgV = /^tg_v/ const containsTgI = /^tg_i/ const containsUId = /^eid_|^tpid_/ const orderedParams = [ 'account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'gdpr', 'gdpr_consent', 'us_privacy', 'rp_schain', ].concat(Object.keys(params).filter(item => containsUId.test(item))) .concat([ 'x_liverampidl', 'ppuid', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw' ]).concat(Object.keys(params).filter(item => containsTgV.test(item))) .concat(Object.keys(params).filter(item => containsTgI.test(item))) .concat([ 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_floor', 'rp_secure', 'tk_user_key' ]); return orderedParams.concat(Object.keys(params).filter(item => (orderedParams.indexOf(item) === -1))); }, /** * @summary combines param values from an array of slots into a single semicolon delineated value * or just one value if they are all the same. * @param {Object[]} aSlotUrlParams - example [{p1: 'foo', p2: 'test'}, {p2: 'test'}, {p1: 'bar', p2: 'test'}] * @return {Object} - example {p1: 'foo;;bar', p2: 'test'} */ combineSlotUrlParams: function(aSlotUrlParams) { // if only have params for one slot, return those params if (aSlotUrlParams.length === 1) { return aSlotUrlParams[0]; } // reduce param values from all slot objects into an array of values in a single object const oCombinedSlotUrlParams = aSlotUrlParams.reduce(function(oCombinedParams, oSlotUrlParams, iIndex) { Object.keys(oSlotUrlParams).forEach(function(param) { if (!oCombinedParams.hasOwnProperty(param)) { oCombinedParams[param] = new Array(aSlotUrlParams.length); // initialize array; } // insert into the proper element of the array oCombinedParams[param].splice(iIndex, 1, oSlotUrlParams[param]); }); return oCombinedParams; }, {}); // convert arrays into semicolon delimited strings const re = new RegExp('^([^;]*)(;\\1)+$'); // regex to test for duplication Object.keys(oCombinedSlotUrlParams).forEach(function(param) { const sValues = oCombinedSlotUrlParams[param].join(';'); // consolidate param values into one value if they are all the same const match = sValues.match(re); oCombinedSlotUrlParams[param] = match ? match[1] : sValues; }); return oCombinedSlotUrlParams; }, /** * @param {BidRequest} bidRequest * @param {Object} bidderRequest * @returns {Object} - object key values named and formatted as slot params */ createSlotParams: function(bidRequest, bidderRequest) { bidRequest.startTime = new Date().getTime(); const params = bidRequest.params; // use rubicon sizes if provided, otherwise adUnit.sizes const parsedSizes = parseSizes(bidRequest, 'banner'); const [latitude, longitude] = params.latLong || []; const data = { 'account_id': params.accountId, 'site_id': params.siteId, 'zone_id': params.zoneId, 'size_id': parsedSizes[0], 'alt_size_ids': parsedSizes.slice(1).join(',') || undefined, 'rp_floor': (params.floor = parseFloat(params.floor)) >= 0.01 ? params.floor : undefined, 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), 'p_geo.longitude': isNaN(parseFloat(longitude)) ? undefined : parseFloat(longitude).toFixed(4), 'tg_fl.eid': bidRequest.code, 'rf': _getPageUrl(bidRequest, bidderRequest) }; // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { let floorInfo; try { floorInfo = bidRequest.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }); } catch (e) { utils.logError('Rubicon: getFloor threw an error: ', e); } data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value let posMapping = {1: 'atf', 3: 'btf'}; let pos = posMapping[utils.deepAccess(bidRequest, 'mediaTypes.banner.pos')] || ''; data['p_pos'] = (params.position === 'atf' || params.position === 'btf') ? params.position : pos; // pass publisher provided userId if configured const configUserId = config.getConfig('user.id'); if (configUserId) { data['ppuid'] = configUserId; } // loop through userIds and add to request if (bidRequest.userIdAsEids) { bidRequest.userIdAsEids.forEach(eid => { try { // special cases if (eid.source === 'adserver.org') { data['tpid_tdid'] = eid.uids[0].id; data['eid_adserver.org'] = eid.uids[0].id; } else if (eid.source === 'liveintent.com') { data['tpid_liveintent.com'] = eid.uids[0].id; data['eid_liveintent.com'] = eid.uids[0].id; if (eid.ext && Array.isArray(eid.ext.segments) && eid.ext.segments.length) { data['tg_v.LIseg'] = eid.ext.segments.join(','); } } else if (eid.source === 'liveramp.com') { data['x_liverampidl'] = eid.uids[0].id; } else if (eid.source === 'id5-sync.com') { data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; } // send AE "ppuid" signal if exists, and hasn't already been sent if (!data['ppuid']) { // get the first eid.uids[*].ext.stype === 'ppuid', if one exists const ppId = find(eid.uids, uid => uid.ext && uid.ext.stype === 'ppuid'); if (ppId && ppId.id) { data['ppuid'] = ppId.id; } } } catch (e) { utils.logWarn('Rubicon: error reading eid:', eid, e); } }); } if (bidderRequest.gdprConsent) { // add 'gdpr' only if 'gdprApplies' is defined if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { data['gdpr'] = Number(bidderRequest.gdprConsent.gdprApplies); } data['gdpr_consent'] = bidderRequest.gdprConsent.consentString; } if (bidderRequest.uspConsent) { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } data['rp_maxbids'] = bidderRequest.bidLimit || 1; applyFPD(bidRequest, BANNER, data); if (config.getConfig('coppa') === true) { data['coppa'] = 1; } // if SupplyChain is supplied and contains all required fields if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { data.rp_schain = spec.serializeSupplyChain(bidRequest.schain); } return data; }, /** * Serializes schain params according to OpenRTB requirements * @param {Object} supplyChain * @returns {String} */ serializeSupplyChain: function (supplyChain) { const supplyChainIsValid = hasValidSupplyChainParams(supplyChain); if (!supplyChainIsValid) return ''; const { ver, complete, nodes } = supplyChain; return `${ver},${complete}!${spec.serializeSupplyChainNodes(nodes)}`; }, /** * Properly sorts schain object params * @param {Array} nodes * @returns {String} */ serializeSupplyChainNodes: function (nodes) { const nodePropOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; return nodes.map(node => { return nodePropOrder.map(prop => encodeURIComponent(node[prop] || '')).join(','); }).join('!'); }, /** * @param {*} responseObj * @param {BidRequest|Object.<string, BidRequest[]>} bidRequest - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object * @return {Bid[]} An array of bids which */ interpretResponse: function (responseObj, {bidRequest}) { responseObj = responseObj.body; // check overall response if (!responseObj || typeof responseObj !== 'object') { return []; } // video response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = utils.deepAccess(responseObj, 'ext.errors.rubicon'); if (Array.isArray(responseErrors) && responseErrors.length > 0) { utils.logWarn('Rubicon: Error in video response'); } const bids = []; responseObj.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { let bidObject = { requestId: bidRequest.bidId, currency: responseObj.cur || 'USD', creativeId: bid.crid, cpm: bid.price || 0, bidderCode: seatbid.seat, ttl: 300, netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true width: bid.w || utils.deepAccess(bidRequest, 'mediaTypes.video.w') || utils.deepAccess(bidRequest, 'params.video.playerWidth'), height: bid.h || utils.deepAccess(bidRequest, 'mediaTypes.video.h') || utils.deepAccess(bidRequest, 'params.video.playerHeight'), }; if (bid.id) { bidObject.seatBidId = bid.id; } if (bid.dealid) { bidObject.dealId = bid.dealid; } if (bid.adomain) { utils.deepSetValue(bidObject, 'meta.advertiserDomains', Array.isArray(bid.adomain) ? bid.adomain : [bid.adomain]); } if (utils.deepAccess(bid, 'ext.bidder.rp.advid')) { utils.deepSetValue(bidObject, 'meta.advertiserId', bid.ext.bidder.rp.advid); } let serverResponseTimeMs = utils.deepAccess(responseObj, 'ext.responsetimemillis.rubicon'); if (bidRequest && serverResponseTimeMs) { bidRequest.serverResponseTimeMs = serverResponseTimeMs; } if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; utils.deepSetValue(bidObject, 'meta.mediaType', VIDEO); const extPrebidTargeting = utils.deepAccess(bid, 'ext.prebid.targeting'); // If ext.prebid.targeting exists, add it as a property value named 'adserverTargeting' if (extPrebidTargeting && typeof extPrebidTargeting === 'object') { bidObject.adserverTargeting = extPrebidTargeting; } // 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 (bid.nurl) { bidObject.vastUrl = bid.nurl; } if (!bidObject.vastUrl && bid.nurl) { bidObject.vastUrl = bid.nurl; } const videoContext = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); if (videoContext.toLowerCase() === 'outstream') { bidObject.renderer = outstreamRenderer(bidObject); } } else { utils.logWarn('Rubicon: video response received non-video media type'); } bids.push(bidObject); }); }); return bids; } let ads = responseObj.ads; let lastImpId; let multibid = 0; // video ads array is wrapped in an object if (typeof bidRequest === 'object' && !Array.isArray(bidRequest) && bidType(bidRequest) === 'video' && typeof ads === 'object') { ads = ads[bidRequest.adUnitCode]; } // check the ad response if (!Array.isArray(ads) || ads.length < 1) { return []; } return ads.reduce((bids, ad, i) => { (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; if (ad.status !== 'ok') { return bids; } // associate bidRequests; assuming ads matches bidRequest const associatedBidRequest = Array.isArray(bidRequest) ? bidRequest[i - multibid] : bidRequest; if (associatedBidRequest && typeof associatedBidRequest === 'object') { let bid = { requestId: associatedBidRequest.bidId, currency: 'USD', creativeId: ad.creative_id || `${ad.network || ''}-${ad.advertiser || ''}`, cpm: ad.cpm || 0, dealId: ad.deal, ttl: 300, // 5 minutes netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true rubicon: { advertiserId: ad.advertiser, networkId: ad.network }, meta: { advertiserId: ad.advertiser, networkId: ad.network, mediaType: BANNER } }; if (ad.creative_type) { bid.mediaType = ad.creative_type; } if (ad.adomain) { bid.meta.advertiserDomains = Array.isArray(ad.adomain) ? ad.adomain : [ad.adomain]; } if (ad.creative_type === VIDEO) { bid.width = associatedBidRequest.params.video.playerWidth; bid.height = associatedBidRequest.params.video.playerHeight; bid.vastUrl = ad.creative_depot_url; bid.impression_id = ad.impression_id; bid.videoCacheKey = ad.impression_id; } else { bid.ad = _renderCreative(ad.script, ad.impression_id); [bid.width, bid.height] = sizeMap[ad.size_id].split('x').map(num => Number(num)); } // add server-side targeting bid.rubiconTargeting = (Array.isArray(ad.targeting) ? ad.targeting : []) .reduce((memo, item) => { memo[item.key] = item.values[0]; return memo; }, {'rpfl_elemid': associatedBidRequest.adUnitCode}); bids.push(bid); } else { utils.logError(`Rubicon: bidRequest undefined at index position:${i}`, bidRequest, responseObj); } return bids; }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { if (!hasSynced && syncOptions.iframeEnabled) { // data is only assigned if params are available to pass to syncEndpoint let params = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string') { // add 'gdpr' only if 'gdprApplies' is defined if (typeof gdprConsent.gdprApplies === 'boolean') { params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; } else { params += `?gdpr_consent=${gdprConsent.consentString}`; } } if (uspConsent) { params += `${params ? '&' : '?'}us_privacy=${encodeURIComponent(uspConsent)}`; } hasSynced = true; return { type: 'iframe', url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } }, /** * Covert bid param types for S2S * @param {Object} params bid params * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol * @return {Object} params bid params */ transformBidParams: function(params, isOpenRtb) { return utils.convertTypes({ 'accountId': 'number', 'siteId': 'number', 'zoneId': 'number' }, params); } }; function _getScreenResolution() { return [window.screen.width, window.screen.height].join('x'); } /** * @param {BidRequest} bidRequest * @param bidderRequest * @returns {string} */ function _getPageUrl(bidRequest, bidderRequest) { let pageUrl = config.getConfig('pageUrl'); if (bidRequest.params.referrer) { pageUrl = bidRequest.params.referrer; } else if (!pageUrl) { pageUrl = bidderRequest.refererInfo.referer; } return bidRequest.params.secure ? pageUrl.replace(/^http:/i, 'https:') : pageUrl; } function _renderCreative(script, impId) { return `<html> <head><script type='text/javascript'>inDapIF=true;</script></head> <body style='margin : 0; padding: 0;'> <!-- Rubicon Project Ad Tag --> <div data-rp-impression-id='${impId}'> <script type='text/javascript'>${script}</script> </div> </body> </html>`; } function hideGoogleAdsDiv(adUnit) { const el = adUnit.querySelector("div[id^='google_ads']"); if (el) { el.style.setProperty('display', 'none'); } } function hideSmartAdServerIframe(adUnit) { const el = adUnit.querySelector("script[id^='sas_script']"); const nextSibling = el && el.nextSibling; if (nextSibling && nextSibling.localName === 'iframe') { nextSibling.style.setProperty('display', 'none'); } } function renderBid(bid) { // hide existing ad units const adUnitElement = document.getElementById(bid.adUnitCode); hideGoogleAdsDiv(adUnitElement); hideSmartAdServerIframe(adUnitElement); // configure renderer const config = bid.renderer.getConfig(); bid.renderer.push(() => { window.MagniteApex.renderAd({ width: bid.width, height: bid.height, vastUrl: bid.vastUrl, placement: { attachTo: `#${bid.adUnitCode}`, align: config.align || 'center', position: config.position || 'append' }, closeButton: config.closeButton || false, label: config.label || undefined, collapse: config.collapse || true }); }); } function outstreamRenderer(rtbBid) { const renderer = Renderer.install({ id: rtbBid.adId, url: rubiConf.rendererUrl || DEFAULT_RENDERER_URL, config: rubiConf.rendererConfig || {}, loaded: false, adUnitCode: rtbBid.adUnitCode }); try { renderer.setRender(renderBid); } catch (err) { utils.logWarn('Prebid Error calling setRender on renderer', err); } return renderer; } function parseSizes(bid, mediaType) { let params = bid.params; if (mediaType === 'video') { let size = []; if (params.video && params.video.playerWidth && params.video.playerHeight) { size = [ params.video.playerWidth, params.video.playerHeight ]; } else if (Array.isArray(utils.deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { size = bid.mediaTypes.video.playerSize[0]; } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { size = bid.sizes[0]; } return size; } // deprecated: temp legacy support let sizes = []; if (Array.isArray(params.sizes)) { sizes = params.sizes; } else if (typeof utils.deepAccess(bid, 'mediaTypes.banner.sizes') !== 'undefined') { sizes = mapSizes(bid.mediaTypes.banner.sizes); } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { sizes = mapSizes(bid.sizes) } else { utils.logWarn('Rubicon: no sizes are setup or found'); } return masSizeOrdering(sizes); } /** * @param {Object} data * @param bidRequest * @param bidderRequest */ function appendSiteAppDevice(data, bidRequest, bidderRequest) { if (!data) return; // ORTB specifies app OR site if (typeof config.getConfig('app') === 'object') { data.app = config.getConfig('app'); } else { data.site = { page: _getPageUrl(bidRequest, bidderRequest) } } if (typeof config.getConfig('device') === 'object') { data.device = config.getConfig('device'); } // Add language to site and device objects if there if (bidRequest.params.video.language) { ['site', 'device'].forEach(function(param) { if (data[param]) { data[param].content = Object.assign({language: bidRequest.params.video.language}, data[param].content) } }); } } /** * @param {Object} data * @param {BidRequest} bidRequest */ function addVideoParameters(data, bidRequest) { if (typeof data.imp[0].video === 'object' && data.imp[0].video.skip === undefined) { data.imp[0].video.skip = bidRequest.params.video.skip; } if (typeof data.imp[0].video === 'object' && data.imp[0].video.skipafter === undefined) { data.imp[0].video.skipafter = bidRequest.params.video.skipdelay; } // video.pos can already be specified by adunit.mediatypes.video.pos. // but if not, it might be specified in the params if (typeof data.imp[0].video === 'object' && data.imp[0].video.pos === undefined) { if (bidRequest.params.position === 'atf') { data.imp[0].video.pos = 1; } else if (bidRequest.params.position === 'btf') { data.imp[0].video.pos = 3; } } const size = parseSizes(bidRequest, 'video') data.imp[0].video.w = size[0] data.imp[0].video.h = size[1] } function applyFPD(bidRequest, mediaType, data) { const BID_FPD = { user: {ext: {data: {...bidRequest.params.visitor}}}, site: {ext: {data: {...bidRequest.params.inventory}}} }; if (bidRequest.params.keywords) BID_FPD.site.keywords = (utils.isArray(bidRequest.params.keywords)) ? bidRequest.params.keywords.join(',') : bidRequest.params.keywords; let fpd = utils.mergeDeep({}, config.getConfig('ortb2') || {}, BID_FPD); let impData = utils.deepAccess(bidRequest.ortb2Imp, 'ext.data') || {}; const SEGTAX = {user: [4], site: [1, 2, 5, 6]}; const MAP = {user: 'tg_v.', site: 'tg_i.', adserver: 'tg_i.dfp_ad_unit_code', pbadslot: 'tg_i.pbadslot', keywords: 'kw'}; const validate = function(prop, key, parentName) { if (key === 'data' && Array.isArray(prop)) { return prop.filter(name => name.segment && utils.deepAccess(name, 'ext.segtax') && SEGTAX[parentName] && SEGTAX[parentName].indexOf(utils.deepAccess(name, 'ext.segtax')) !== -1).map(value => { let segments = value.segment.filter(obj => obj.id).reduce((result, obj) => { result.push(obj.id); return result; }, []); if (segments.length > 0) return segments.toString(); }).toString(); } else if (typeof prop === 'object' && !Array.isArray(prop)) { utils.logWarn('Rubicon: Filtered FPD key: ', key, ': Expected value to be string, integer, or an array of strings/ints'); } else if (typeof prop !== 'undefined') { return (Array.isArray(prop)) ? prop.filter(value => { if (typeof value !== 'object' && typeof value !== 'undefined') return value.toString(); utils.logWarn('Rubicon: Filtered value: ', value, 'for key', key, ': Expected value to be string, integer, or an array of strings/ints'); }).toString() : prop.toString(); } }; const addBannerData = function(obj, name, key, isParent = true) { let val = validate(obj, key, name); let loc = (MAP[key] && isParent) ? `${MAP[key]}` : (key === 'data') ? `${MAP[name]}iab` : `${MAP[name]}${key}`; data[loc] = (data[loc]) ? data[loc].concat(',', val) : val; } Object.keys(impData).forEach((key) => { if (key === 'adserver') { ['name', 'adslot'].forEach(prop => { if (impData[key][prop]) impData[key][prop] = impData[key][prop].toString().replace(/^\/+/, ''); }); } else if (key === 'pbadslot') { impData[key] = impData[key].toString().replace(/^\/+/, ''); } }); if (mediaType === BANNER) { ['site', 'user'].forEach(name => { Object.keys(fpd[name]).forEach((key) => { if (name === 'site' && key === 'content' && fpd[name][key].data) { addBannerData(fpd[name][key].data, name, 'data'); } else if (key !== 'ext') { addBannerData(fpd[name][key], name, key); } else if (fpd[name][key].data) { Object.keys(fpd[name].ext.data).forEach((key) => { addBannerData(fpd[name].ext.data[key], name, key, false); }); } }); }); Object.keys(impData).forEach((key) => { (key === 'adserver') ? addBannerData(impData[key].adslot, name, key) : addBannerData(impData[key], 'site', key); }); } else { if (Object.keys(impData).length) { utils.mergeDeep(data.imp[0].ext, {data: impData}); } utils.mergeDeep(data, fpd); } } /** * @param sizes * @returns {*} */ function mapSizes(sizes) { return utils.parseSizesInput(sizes) // map sizes while excluding non-matches .reduce((result, size) => { let mappedSize = parseInt(sizeMap[size], 10); if (mappedSize) { result.push(mappedSize); } return result; }, []); } /** * Test if bid has mediaType or mediaTypes set for video. * Also makes sure the video object is present in the rubicon bidder params * @param {BidRequest} bidRequest * @returns {boolean} */ export function hasVideoMediaType(bidRequest) { if (typeof utils.deepAccess(bidRequest, 'params.video') !== 'object') { return false; } return (typeof utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`) !== 'undefined'); } /** * Determine bidRequest mediaType * @param bid the bid to test * @param log whether we should log errors/warnings for invalid bids * @returns {string|undefined} Returns 'video' or 'banner' if resolves to a type, or undefined otherwise (invalid). */ function bidType(bid, log = false) { // Is it considered video ad unit by rubicon if (hasVideoMediaType(bid)) { // Removed legacy mediaType support. new way using mediaTypes.video object is now required // We require either context as instream or outstream if (['outstream', 'instream'].indexOf(utils.deepAccess(bid, `mediaTypes.${VIDEO}.context`)) === -1) { if (log) { utils.logError('Rubicon: mediaTypes.video.context must be outstream or instream'); } return; } // we require playerWidth and playerHeight to come from one of params.playerWidth/playerHeight or mediaTypes.video.playerSize or adUnit.sizes if (parseSizes(bid, 'video').length < 2) { if (log) { utils.logError('Rubicon: could not determine the playerSize of the video'); } return; } if (log) { utils.logMessage('Rubicon: making video request for adUnit', bid.adUnitCode); } return 'video'; } else { // we require banner sizes to come from one of params.sizes or mediaTypes.banner.sizes or adUnit.sizes, in that order // if we cannot determine them, we reject it! if (parseSizes(bid, 'banner').length === 0) { if (log) { utils.logError('Rubicon: could not determine the sizes for banner request'); } return; } // everything looks good for banner so lets do it if (log) { utils.logMessage('Rubicon: making banner request for adUnit', bid.adUnitCode); } return 'banner'; } } export const resetRubiConf = () => rubiConf = {}; export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; return sizes.sort((first, second) => { // sort by MAS_SIZE_PRIORITY priority order const firstPriority = MAS_SIZE_PRIORITY.indexOf(first); const secondPriority = MAS_SIZE_PRIORITY.indexOf(second); if (firstPriority > -1 || secondPriority > -1) { if (firstPriority === -1) { return 1; } if (secondPriority === -1) { return -1; } return firstPriority - secondPriority; } // and finally ascending order return first - second; }); } export function determineRubiconVideoSizeId(bid) { // If we have size_id in the bid then use it let rubiconSizeId = parseInt(utils.deepAccess(bid, 'params.video.size_id')); if (!isNaN(rubiconSizeId)) { return rubiconSizeId; } // otherwise 203 for outstream and 201 for instream // When this function is used we know it has to be one of outstream or instream return utils.deepAccess(bid, `mediaTypes.${VIDEO}.context`) === 'outstream' ? 203 : 201; } /** * @param {PrebidConfig} config * @returns {{ranges: {ranges: Object[]}}} */ export function getPriceGranularity(config) { return { ranges: { low: [{max: 5.00, increment: 0.50}], medium: [{max: 20.00, increment: 0.10}], high: [{max: 20.00, increment: 0.01}], auto: [ {max: 5.00, increment: 0.05}, {min: 5.00, max: 10.00, increment: 0.10}, {min: 10.00, max: 20.00, increment: 0.50} ], dense: [ {max: 3.00, increment: 0.01}, {min: 3.00, max: 8.00, increment: 0.05}, {min: 8.00, max: 20.00, increment: 0.50} ], custom: config.getConfig('customPriceBucket') && config.getConfig('customPriceBucket').buckets }[config.getConfig('priceGranularity')] }; } // Function to validate the required video params export function hasValidVideoParams(bid) { let isValid = true; // incase future javascript changes the string represenation of the array or number classes! let arrayType = Object.prototype.toString.call([]); let numberType = Object.prototype.toString.call(0); // required params and their associated object type var requiredParams = { mimes: arrayType, protocols: arrayType, linearity: numberType, api: arrayType } // loop through each param and verify it has the correct Object.keys(requiredParams).forEach(function(param) { if (Object.prototype.toString.call(utils.deepAccess(bid, 'mediaTypes.video.' + param)) !== requiredParams[param]) { isValid = false; utils.logError('Rubicon: mediaTypes.video.' + param + ' is required and must be of type: ' + requiredParams[param]); } }) return isValid; } /** * Make sure the required params are present * @param {Object} schain * @param {Bool} */ export function hasValidSupplyChainParams(schain) { let isValid = false; const requiredFields = ['asi', 'sid', 'hp']; if (!schain.nodes) return isValid; isValid = schain.nodes.reduce((status, node) => { if (!status) return status; return requiredFields.every(field => node.hasOwnProperty(field)); }, true); if (!isValid) utils.logError('Rubicon: required schain params missing'); return isValid; } /** * Creates a URL key value param, encoding the * param unless the key is schain * @param {String} key * @param {String} param * @returns {String} */ export function encodeParam(key, param) { if (key === 'rp_schain') return `rp_schain=${param}`; return `${key}=${encodeURIComponent(param)}`; } /** * split array into multiple arrays of defined size * @param {Array} array * @param {number} size * @returns {Array} */ function partitionArray(array, size) { return array.map((e, i) => (i % size === 0) ? array.slice(i, i + size) : null).filter((e) => e) } var hasSynced = false; export function resetUserSync() { hasSynced = false; } registerBidder(spec);