UNPKG

mk9-prebid

Version:

Header Bidding Management Library

598 lines (532 loc) 19.7 kB
import * as utils from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import includes from 'core-js-pure/features/array/includes'; import find from 'core-js-pure/features/array/find.js'; const BIDDER_CODE = 'yieldmo'; const CURRENCY = 'USD'; const TIME_TO_LIVE = 300; const NET_REVENUE = true; const BANNER_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; const VIDEO_SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebidvideo'; const OUTSTREAM_VIDEO_PLAYER_URL = 'https://prebid-outstream.yieldmo.com/bundle.js'; const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'startdelay', 'skipafter', 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos', 'skip', 'skippable']; const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const LOCAL_WINDOW = utils.getWindowTop(); const DEFAULT_PLAYBACK_METHOD = 2; const DEFAULT_START_DELAY = 0; const VAST_TIMEOUT = 15000; const MAX_BANNER_REQUEST_URL_LENGTH = 8000; const BANNER_REQUEST_PROPERTIES_TO_REDUCE = ['description', 'title', 'pr', 'page_url']; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ isBidRequestValid: function (bid) { return !!(bid && bid.adUnitCode && bid.bidId && (hasBannerMediaType(bid) || hasVideoMediaType(bid)) && validateVideoParams(bid)); }, /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { const bannerBidRequests = bidRequests.filter(request => hasBannerMediaType(request)); const videoBidRequests = bidRequests.filter(request => hasVideoMediaType(request)); let serverRequests = []; if (bannerBidRequests.length > 0) { let serverRequest = { pbav: '$prebid.version$', p: [], page_url: bidderRequest.refererInfo.referer, bust: new Date().getTime().toString(), pr: (LOCAL_WINDOW.document && LOCAL_WINDOW.document.referrer) || '', scrd: LOCAL_WINDOW.devicePixelRatio || 0, dnt: getDNT(), description: getPageDescription(), title: LOCAL_WINDOW.document.title || '', w: LOCAL_WINDOW.innerWidth, h: LOCAL_WINDOW.innerHeight, userConsent: JSON.stringify({ // case of undefined, stringify will remove param gdprApplies: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', cmp: utils.deepAccess(bidderRequest, 'gdprConsent.consentString') || '' }), us_privacy: utils.deepAccess(bidderRequest, 'uspConsent') || '' }; const mtp = window.navigator.maxTouchPoints; if (mtp) { serverRequest.mtp = mtp; } bannerBidRequests.forEach(request => { serverRequest.p.push(addPlacement(request)); const pubcid = getId(request, 'pubcid'); if (pubcid) { serverRequest.pubcid = pubcid; } else if (request.crumbs && request.crumbs.pubcid) { serverRequest.pubcid = request.crumbs.pubcid; } const tdid = getId(request, 'tdid'); if (tdid) { serverRequest.tdid = tdid; } const criteoId = getId(request, 'criteoId'); if (criteoId) { serverRequest.cri_prebid = criteoId; } if (request.schain) { serverRequest.schain = JSON.stringify(request.schain); } if (utils.deepAccess(request, 'params.lr_env')) { serverRequest.ats_envelope = request.params.lr_env; } }); serverRequest.p = '[' + serverRequest.p.toString() + ']'; // check if url exceeded max length const url = `${BANNER_SERVER_ENDPOINT}?${utils.parseQueryStringParameters(serverRequest)}`; let extraCharacters = url.length - MAX_BANNER_REQUEST_URL_LENGTH; if (extraCharacters > 0) { for (let i = 0; i < BANNER_REQUEST_PROPERTIES_TO_REDUCE.length; i++) { extraCharacters = shortcutProperty(extraCharacters, serverRequest, BANNER_REQUEST_PROPERTIES_TO_REDUCE[i]); if (extraCharacters <= 0) { break; } } } serverRequests.push({ method: 'GET', url: BANNER_SERVER_ENDPOINT, data: serverRequest }); } if (videoBidRequests.length > 0) { const serverRequest = openRtbRequest(videoBidRequests, bidderRequest); serverRequests.push({ method: 'POST', url: VIDEO_SERVER_ENDPOINT, data: serverRequest }); } return serverRequests; }, /** * Makes Yieldmo Ad Server response compatible to Prebid specs * @param {ServerResponse} serverResponse successful response from Ad Server * @param {ServerRequest} bidRequest * @return {Bid[]} an array of bids */ interpretResponse: function (serverResponse, bidRequest) { let bids = []; const data = serverResponse.body; if (data.length > 0) { data.forEach(response => { if (response.cpm > 0) { bids.push(createNewBannerBid(response)); } }); } if (data.seatbid) { const seatbids = data.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); seatbids.forEach(bid => bids.push(createNewVideoBid(bid, bidRequest))); } return bids; }, getUserSyncs: function () { return []; } }; registerBidder(spec); /*************************************** * Helper Functions ***************************************/ /** * @param {BidRequest} bidRequest bid request */ function hasBannerMediaType(bidRequest) { return !!utils.deepAccess(bidRequest, 'mediaTypes.banner'); } /** * @param {BidRequest} bidRequest bid request */ function hasVideoMediaType(bidRequest) { return !!utils.deepAccess(bidRequest, 'mediaTypes.video'); } /** * Adds placement information to array * @param request bid request */ function addPlacement(request) { const gpid = utils.deepAccess(request, 'ortb2Imp.ext.data.pbadslot'); const placementInfo = { placement_id: request.adUnitCode, callback_id: request.bidId, sizes: request.mediaTypes.banner.sizes }; if (request.params) { if (request.params.placementId) { placementInfo.ym_placement_id = request.params.placementId; } const bidfloor = getBidFloor(request, BANNER); if (bidfloor) { placementInfo.bidFloor = bidfloor; } } if (gpid) { placementInfo.gpid = gpid; } return JSON.stringify(placementInfo); } /** * creates a new banner bid with response information * @param response server response */ function createNewBannerBid(response) { return { requestId: response['callback_id'], cpm: response.cpm, width: response.width, height: response.height, creativeId: response.creative_id, currency: CURRENCY, netRevenue: NET_REVENUE, ttl: TIME_TO_LIVE, ad: response.ad, meta: { advertiserDomains: response.adomain || [], mediaType: BANNER, }, }; } /** * creates a new video bid with response information * @param response openRTB server response * @param bidRequest server request */ function createNewVideoBid(response, bidRequest) { const imp = find((utils.deepAccess(bidRequest, 'data.imp') || []), imp => imp.id === response.impid); let result = { requestId: imp.id, cpm: response.price, width: imp.video.w, height: imp.video.h, creativeId: response.crid || response.adid, currency: CURRENCY, netRevenue: NET_REVENUE, mediaType: VIDEO, ttl: TIME_TO_LIVE, vastXml: response.adm, meta: { advertiserDomains: response.adomain || [], mediaType: VIDEO, }, }; if (imp.video.placement && imp.video.placement !== 1) { const renderer = Renderer.install({ url: OUTSTREAM_VIDEO_PLAYER_URL, config: { width: result.width, height: result.height, vastTimeout: VAST_TIMEOUT, maxAllowedVastTagRedirects: 5, allowVpaid: true, autoPlay: true, preload: true, mute: true }, id: imp.tagid, loaded: false, }); renderer.setRender(function (bid) { bid.renderer.push(() => { const { id, config } = bid.renderer; window.YMoutstreamPlayer(bid, id, config); }); }); result.renderer = renderer; } return result; } /** * Detects whether dnt is true * @returns true if user enabled dnt */ function getDNT() { return ( window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false ); } /** * get page description */ function getPageDescription() { if (document.querySelector('meta[name="description"]')) { return document .querySelector('meta[name="description"]') .getAttribute('content') || ''; // Value of the description metadata from the publisher's page. } else { return ''; } } /** * Gets an id from the userId object if it exists * @param {*} request * @param {*} idType * @returns an id if there is one, or undefined */ function getId(request, idType) { return (typeof utils.deepAccess(request, 'userId') === 'object') ? request.userId[idType] : undefined; } /** * @param {BidRequest[]} bidRequests bid request object * @param {BidderRequest} bidderRequest bidder request object * @return Object OpenRTB request object */ function openRtbRequest(bidRequests, bidderRequest) { const schain = bidRequests[0].schain; let openRtbRequest = { id: bidRequests[0].bidderRequestId, at: 1, imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), site: openRtbSite(bidRequests[0], bidderRequest), device: openRtbDevice(bidRequests[0]), badv: bidRequests[0].params.badv || [], bcat: bidRequests[0].params.bcat || [], ext: { prebid: '$prebid.version$', }, ats_envelope: bidRequests[0].params.lr_env, }; if (schain) { openRtbRequest.schain = schain; } populateOpenRtbGdpr(openRtbRequest, bidderRequest); return openRtbRequest; } /** * @param {BidRequest} bidRequest bidder request object. * @return Object OpenRTB's 'imp' (impression) object */ function openRtbImpression(bidRequest) { const gpid = utils.deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); const size = extractPlayerSize(bidRequest); const imp = { id: bidRequest.bidId, tagid: bidRequest.adUnitCode, bidfloor: getBidFloor(bidRequest, VIDEO), ext: { placement_id: bidRequest.params.placementId }, video: { w: size[0], h: size[1], linearity: 1 } }; const mediaTypesParams = utils.deepAccess(bidRequest, 'mediaTypes.video'); Object.keys(mediaTypesParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = mediaTypesParams[param]); const videoParams = utils.deepAccess(bidRequest, 'params.video'); Object.keys(videoParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = videoParams[param]); if (imp.video.skippable) { imp.video.skip = 1; delete imp.video.skippable; } if (imp.video.placement !== 1) { imp.video.startdelay = DEFAULT_START_DELAY; imp.video.playbackmethod = [ DEFAULT_PLAYBACK_METHOD ]; } if (gpid) { imp.ext.gpid = gpid; } return imp; } function getBidFloor(bidRequest, mediaType) { let floorInfo = {}; if (typeof bidRequest.getFloor === 'function') { floorInfo = bidRequest.getFloor({ currency: CURRENCY, mediaType, size: '*' }); } return floorInfo.floor || bidRequest.params.bidfloor || bidRequest.params.bidFloor || 0; } /** * @param {BidRequest} bidRequest bidder request object. * @return [number, number] || null Player's width and height, or undefined otherwise. */ function extractPlayerSize(bidRequest) { const sizeArr = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); if (utils.isArrayOfNums(sizeArr, 2)) { return sizeArr; } else if (utils.isArray(sizeArr) && utils.isArrayOfNums(sizeArr[0], 2)) { return sizeArr[0]; } return null; } /** * @param {BidRequest} bidRequest bid request object * @param {BidderRequest} bidderRequest bidder request object * @return Object OpenRTB's 'site' object */ function openRtbSite(bidRequest, bidderRequest) { let result = {}; const loc = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); if (!utils.isEmpty(loc)) { result.page = `${loc.protocol}://${loc.hostname}${loc.pathname}`; } if (self === top && document.referrer) { result.ref = document.referrer; } const keywords = document.getElementsByTagName('meta')['keywords']; if (keywords && keywords.content) { result.keywords = keywords.content; } const siteParams = utils.deepAccess(bidRequest, 'params.site'); if (siteParams) { Object.keys(siteParams) .filter(param => includes(OPENRTB_VIDEO_SITEPARAMS, param)) .forEach(param => result[param] = siteParams[param]); } return result; } /** * @return Object OpenRTB's 'device' object */ function openRtbDevice(bidRequest) { const ip = utils.deepAccess(bidRequest, 'params.device.ip'); const deviceObj = { ua: navigator.userAgent, language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), }; if (ip) { deviceObj.ip = ip; } return deviceObj; } /** * Updates openRtbRequest with GDPR info from bidderRequest, if present. * @param {Object} openRtbRequest OpenRTB's request to update. * @param {BidderRequest} bidderRequest bidder request object. */ function populateOpenRtbGdpr(openRtbRequest, bidderRequest) { const gdpr = bidderRequest.gdprConsent; if (gdpr && 'gdprApplies' in gdpr) { utils.deepSetValue(openRtbRequest, 'regs.ext.gdpr', gdpr.gdprApplies ? 1 : 0); utils.deepSetValue(openRtbRequest, 'user.ext.consent', gdpr.consentString); } const uspConsent = utils.deepAccess(bidderRequest, 'uspConsent'); if (uspConsent) { utils.deepSetValue(openRtbRequest, 'regs.ext.us_privacy', uspConsent); } } /** * Determines whether or not the given video bid request is valid. If it's not a video bid, returns true. * @param {object} bid, bid to validate * @return boolean, true if valid, otherwise false */ function validateVideoParams(bid) { if (!hasVideoMediaType(bid)) { return true; } const paramRequired = (paramStr, value, conditionStr) => { let error = `"${paramStr}" is required`; if (conditionStr) { error += ' when ' + conditionStr; } throw new Error(error); } const paramInvalid = (paramStr, value, expectedStr) => { expectedStr = expectedStr ? ', expected: ' + expectedStr : ''; value = JSON.stringify(value); throw new Error(`"${paramStr}"=${value} is invalid${expectedStr}`); } const isDefined = val => typeof val !== 'undefined'; const validate = (fieldPath, validateCb, errorCb, errorCbParam) => { if (fieldPath.indexOf('video') === 0) { const valueFieldPath = 'params.' + fieldPath; const mediaFieldPath = 'mediaTypes.' + fieldPath; const valueParams = utils.deepAccess(bid, valueFieldPath); const mediaTypesParams = utils.deepAccess(bid, mediaFieldPath); const hasValidValueParams = validateCb(valueParams); const hasValidMediaTypesParams = validateCb(mediaTypesParams); if (hasValidValueParams) return valueParams; else if (hasValidMediaTypesParams) return hasValidMediaTypesParams; else { if (!hasValidValueParams) errorCb(valueFieldPath, valueParams, errorCbParam); else if (!hasValidMediaTypesParams) errorCb(mediaFieldPath, mediaTypesParams, errorCbParam); } return valueParams || mediaTypesParams; } else { const value = utils.deepAccess(bid, fieldPath); if (!validateCb(value)) { errorCb(fieldPath, value, errorCbParam); } return value; } } try { validate('video.context', val => !utils.isEmpty(val), paramRequired); validate('params.placementId', val => !utils.isEmpty(val), paramRequired); validate('video.playerSize', val => utils.isArrayOfNums(val, 2) || (utils.isArray(val) && val.every(v => utils.isArrayOfNums(v, 2))), paramInvalid, 'array of 2 integers, ex: [640,480] or [[640,480]]'); validate('video.mimes', val => isDefined(val), paramRequired); validate('video.mimes', val => utils.isArray(val) && val.every(v => utils.isStr(v)), paramInvalid, 'array of strings, ex: ["video/mp4"]'); const placement = validate('video.placement', val => isDefined(val), paramRequired); validate('video.placement', val => val >= 1 && val <= 5, paramInvalid); if (placement === 1) { validate('video.startdelay', val => isDefined(val), (field, v) => paramRequired(field, v, 'placement == 1')); validate('video.startdelay', val => utils.isNumber(val), paramInvalid, 'number, ex: 5'); } validate('video.protocols', val => isDefined(val), paramRequired); validate('video.protocols', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), paramInvalid, 'array of numbers, ex: [2,3]'); validate('video.api', val => isDefined(val), paramRequired); validate('video.api', val => utils.isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), paramInvalid, 'array of numbers, ex: [2,3]'); validate('video.playbackmethod', val => !isDefined(val) || utils.isArrayOfNums(val), paramInvalid, 'array of integers, ex: [2,6]'); validate('video.maxduration', val => isDefined(val), paramRequired); validate('video.maxduration', val => utils.isInteger(val), paramInvalid); validate('video.minduration', val => !isDefined(val) || utils.isNumber(val), paramInvalid); validate('video.skippable', val => !isDefined(val) || utils.isBoolean(val), paramInvalid); validate('video.skipafter', val => !isDefined(val) || utils.isNumber(val), paramInvalid); validate('video.pos', val => !isDefined(val) || utils.isNumber(val), paramInvalid); validate('params.badv', val => !isDefined(val) || utils.isArray(val), paramInvalid, 'array of strings, ex: ["ford.com","pepsi.com"]'); validate('params.bcat', val => !isDefined(val) || utils.isArray(val), paramInvalid, 'array of strings, ex: ["IAB1-5","IAB1-6"]'); return true; } catch (e) { utils.logError(e.message); return false; } } /** * Shortcut object property and check if required characters count was deleted * * @param {number} extraCharacters, count of characters to remove * @param {object} target, object on which string property length should be reduced * @param {string} propertyName, name of property to reduce * @return {number} 0 if required characters count was removed otherwise count of how many left */ function shortcutProperty(extraCharacters, target, propertyName) { if (target[propertyName].length > extraCharacters) { target[propertyName] = target[propertyName].substring(0, target[propertyName].length - extraCharacters); return 0 } const charactersLeft = extraCharacters - target[propertyName].length; target[propertyName] = ''; return charactersLeft; }