UNPKG

mk9-prebid

Version:

Header Bidding Management Library

430 lines (379 loc) 13.2 kB
import { registerBidder } from '../src/adapters/bidderFactory.js'; import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, deepClone, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); const OUTSTREAM_REDNERER_URL = 'https://mtrx.go.sonobi.com/sbi_outstream_renderer.js'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * * @param {BidRequest} bid - The bid params to validate. * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: (bid) => { if (!bid.params) { return false; } if (!bid.params.ad_unit && !bid.params.placement_id) { return false; } if (!deepAccess(bid, 'mediaTypes.banner') && !deepAccess(bid, 'mediaTypes.video')) { return false; } if (deepAccess(bid, 'mediaTypes.banner')) { // Sonobi does not support multi type bids, favor banner over video if (!deepAccess(bid, 'mediaTypes.banner.sizes') && !bid.params.sizes) { // sizes at the banner or params level is required. return false; } } else if (deepAccess(bid, 'mediaTypes.video')) { if (deepAccess(bid, 'mediaTypes.video.context') === 'outstream' && !bid.params.sizes) { // bids.params.sizes is required for outstream video adUnits return false; } if (deepAccess(bid, 'mediaTypes.video.context') === 'instream' && !deepAccess(bid, 'mediaTypes.video.playerSize')) { // playerSize is required for instream adUnits. return false; } } return true; }, /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} validBidRequests - an array of bids * @return {object} ServerRequest - Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { const bids = validBidRequests.map(bid => { let slotIdentifier = _validateSlot(bid); if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier; return { [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}${_validateFloor(bid)}${_validateGPID(bid)}` } } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { return { [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}${_validateFloor(bid)}${_validateGPID(bid)}` } } else { logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); } }); let data = {}; bids.forEach((bid) => { Object.assign(data, bid); }); const payload = { 'key_maker': JSON.stringify(data), 'ref': bidderRequest.refererInfo.referer, 's': generateUUID(), 'pv': PAGEVIEW_ID, 'vp': _getPlatform(), 'lib_name': 'prebid', 'lib_v': '$prebid.version$', 'us': 0, }; if (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) { payload.us = config.getConfig('userSync').syncsPerBidder; } // use userSync's internal function to determine if we can drop an iframe sync pixel if (_iframeAllowed()) { payload.ius = 1; } else { payload.ius = 0; } if (deepAccess(validBidRequests[0], 'params.hfa')) { payload.hfa = deepAccess(validBidRequests[0], 'params.hfa'); } if (validBidRequests[0].params.referrer) { payload.ref = validBidRequests[0].params.referrer; } // Apply GDPR parameters to request. if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = bidderRequest.gdprConsent.gdprApplies ? 'true' : 'false'; if (bidderRequest.gdprConsent.consentString) { payload.consent_string = bidderRequest.gdprConsent.consentString; } } if (validBidRequests[0].schain) { payload.schain = JSON.stringify(validBidRequests[0].schain) } if (deepAccess(validBidRequests[0], 'userId') && Object.keys(validBidRequests[0].userId).length > 0) { const userIds = deepClone(validBidRequests[0].userId); if (userIds.id5id) { userIds.id5id = deepAccess(userIds, 'id5id.uid'); } payload.userid = JSON.stringify(userIds); } const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); if (Array.isArray(eids) && eids.length > 0) { payload.eids = JSON.stringify(eids); } let keywords = validBidRequests[0].params.keywords; // a CSV of keywords if (keywords) { payload.kw = keywords; } if (bidderRequest && bidderRequest.uspConsent) { payload.us_privacy = bidderRequest.uspConsent; } if (config.getConfig('coppa') === true) { payload.coppa = 1; } else { payload.coppa = 0; } // If there is no key_maker data, then don't make the request. if (isEmpty(data)) { return null; } let url = STR_ENDPOINT; if (deepAccess(validBidRequests[0], 'params.bid_request_url')) { url = deepAccess(validBidRequests[0], 'params.bid_request_url'); } return { method: 'GET', url: url, withCredentials: true, data: payload, bidderRequests: validBidRequests }; }, /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. * @param {*} bidderRequest - Info describing the request to the server. * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: (serverResponse, bidderRequest) => { const bidResponse = serverResponse.body; const bidsReturned = []; const referrer = bidderRequest.data.ref; if (Object.keys(bidResponse.slots).length === 0) { return bidsReturned; } Object.keys(bidResponse.slots).forEach(slot => { const bid = bidResponse.slots[slot]; const bidId = _getBidIdFromTrinityKey(slot); const bidRequest = _findBidderRequest(bidderRequest.bidderRequests, bidId); let mediaType = null; if (bid.sbi_ct === 'video') { mediaType = 'video'; const context = deepAccess(bidRequest, 'mediaTypes.video.context'); if (context === 'outstream') { mediaType = 'outstream'; } } const createCreative = _creative(mediaType, referrer); if (bid.sbi_aid && bid.sbi_mouse && bid.sbi_size) { const [ width = 1, height = 1 ] = bid.sbi_size.split('x'); let aDomains = []; if (bid.sbi_adomain) { aDomains = [bid.sbi_adomain] } const bids = { requestId: bidId, cpm: Number(bid.sbi_mouse), width: Number(width), height: Number(height), ad: createCreative(bidResponse.sbi_dc, bid.sbi_aid), ttl: 500, creativeId: bid.sbi_crid || bid.sbi_aid, aid: bid.sbi_aid, netRevenue: true, currency: 'USD', meta: { advertiserDomains: aDomains } }; if (bid.sbi_dozer) { bids.dealId = bid.sbi_dozer; } if (mediaType === 'video') { bids.mediaType = 'video'; bids.vastUrl = createCreative(bidResponse.sbi_dc, bid.sbi_aid); delete bids.ad; delete bids.width; delete bids.height; } else if (mediaType === 'outstream' && bidRequest) { bids.mediaType = 'video'; bids.vastUrl = createCreative(bidResponse.sbi_dc, bid.sbi_aid); bids.renderer = newRenderer(bidRequest.adUnitCode, bids, deepAccess( bidRequest, 'renderer.options' )); let videoSize = deepAccess(bidRequest, 'params.sizes'); if (Array.isArray(videoSize) && Array.isArray(videoSize[0])) { // handle case of multiple sizes videoSize = videoSize[0] // Only take the first size for outstream } if (videoSize) { bids.width = videoSize[0]; bids.height = videoSize[1]; } } bidsReturned.push(bids); } }); return bidsReturned; }, /** * Register User Sync. */ getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { const syncs = []; try { if (syncOptions.pixelEnabled) { serverResponses[0].body.sbi_px.forEach(pixel => { syncs.push({ type: pixel.type, url: pixel.url }); }); } } catch (e) {} return syncs; } }; function _findBidderRequest(bidderRequests, bidId) { for (let i = 0; i < bidderRequests.length; i++) { if (bidderRequests[i].bidId === bidId) { return bidderRequests[i]; } } } function _validateSize (bid) { if (deepAccess(bid, 'mediaTypes.video')) { return ''; // Video bids arent allowed to override sizes via the trinity request } if (bid.params.sizes) { return parseSizesInput(bid.params.sizes).join(','); } if (deepAccess(bid, 'mediaTypes.banner.sizes')) { return parseSizesInput(deepAccess(bid, 'mediaTypes.banner.sizes')).join(','); } // Handle deprecated sizes definition if (bid.sizes) { return parseSizesInput(bid.sizes).join(','); } } function _validateSlot (bid) { if (bid.params.ad_unit) { return bid.params.ad_unit; } return bid.params.placement_id; } function _validateFloor (bid) { const floor = getBidFloor(bid); if (floor) { return `|f=${floor}`; } return ''; } function _validateGPID(bid) { const gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') || deepAccess(getGptSlotInfoForAdUnitCode(bid.adUnitCode), 'gptSlot') || bid.params.ad_unit; if (gpid) { return `|gpid=${gpid}` } return '' } const _creative = (mediaType, referer) => (sbiDc, sbiAid) => { if (mediaType === 'video' || mediaType === 'outstream') { return _videoCreative(sbiDc, sbiAid, referer) } const src = `https://${sbiDc}apex.go.sonobi.com/sbi.js?aid=${sbiAid}&as=null&ref=${encodeURIComponent(referer)}`; return '<script type="text/javascript" src="' + src + '"></script>'; }; function _videoCreative(sbiDc, sbiAid, referer) { return `https://${sbiDc}apex.go.sonobi.com/vast.xml?vid=${sbiAid}&ref=${encodeURIComponent(referer)}` } function _getBidIdFromTrinityKey (key) { return key.split('|').slice(-1)[0] } /** * @param context - the window to determine the innerWidth from. This is purely for test purposes as it should always be the current window */ export const _isInbounds = (context = window) => (lowerBound = 0, upperBound = Number.MAX_SAFE_INTEGER) => context.innerWidth >= lowerBound && context.innerWidth < upperBound; /** * @param context - the window to determine the innerWidth from. This is purely for test purposes as it should always be the current window */ export function _getPlatform(context = window) { const isInBounds = _isInbounds(context); const MOBILE_VIEWPORT = { lt: 768 }; const TABLET_VIEWPORT = { lt: 992, ge: 768 }; if (isInBounds(0, MOBILE_VIEWPORT.lt)) { return 'mobile' } if (isInBounds(TABLET_VIEWPORT.ge, TABLET_VIEWPORT.lt)) { return 'tablet' } return 'desktop'; } function newRenderer(adUnitCode, bid, rendererOptions = {}) { const renderer = Renderer.install({ id: bid.aid, url: OUTSTREAM_REDNERER_URL, config: rendererOptions, loaded: false, adUnitCode }); try { renderer.setRender(outstreamRender); } catch (err) { logWarn('Prebid Error calling setRender on renderer', err); } renderer.setEventHandlers({ impression: () => logMessage('Sonobi outstream video impression event'), loaded: () => logMessage('Sonobi outstream video loaded event'), ended: () => { logMessage('Sonobi outstream renderer video event'); // document.querySelector(`#${adUnitCode}`).style.display = 'none'; } }); return renderer; } function outstreamRender(bid) { // push to render queue because SbiOutstreamRenderer may not be loaded yet bid.renderer.push(() => { const [ width, height ] = bid.getSize().split('x'); const renderer = new window.SbiOutstreamRenderer(); renderer.init({ vastUrl: bid.vastUrl, height, width, }); renderer.setRootElement(bid.adUnitCode); }); } function _iframeAllowed() { return userSync.canBidderRegisterSync('iframe', BIDDER_CODE); } function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return (bid.params.floor) ? bid.params.floor : null; } let floor = bid.getFloor({ currency: 'USD', mediaType: '*', size: '*' }); if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { return floor.floor; } return ''; } registerBidder(spec);