UNPKG

mk9-prebid

Version:

Header Bidding Management Library

601 lines (531 loc) 18.3 kB
import { Renderer } from '../src/Renderer.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration', 'startdelay', 'skippable', 'playback_method', 'frameworks']; const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately const SOURCE = 'pbjs'; const MAX_IMPS_PER_REQUEST = 15; const NATIVE_MAPPING = { body: 'description', body2: 'desc2', cta: 'ctatext', image: { serverName: 'main_image', requiredParams: { required: true } }, icon: { serverName: 'icon', requiredParams: { required: true } }, sponsoredBy: 'sponsored_by', privacyLink: 'privacy_link', salePrice: 'saleprice', displayUrl: 'displayurl' }; export const spec = { code: BIDDER_CODE, aliases: ['adr', 'adsmart', 'compariola'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. * * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { return !!(bid.params.placementId); }, /** * 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. * @return ServerRequest Info describing the request to the server. */ buildRequests: function(bidRequests, bidderRequest) { const tags = bidRequests.map(bidToTag); const userObjBid = find(bidRequests, hasUserInfo); let userObj; if (config.getConfig('coppa') === true) { userObj = {'coppa': true}; } if (userObjBid) { userObj = {}; Object.keys(userObjBid.params.user) .filter(param => includes(USER_PARAMS, param)) .forEach(param => userObj[param] = userObjBid.params.user[param]); } const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); let appDeviceObj; if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) { appDeviceObj = {}; Object.keys(appDeviceObjBid.params.app) .filter(param => includes(APP_DEVICE_PARAMS, param)) .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]); } const appIdObjBid = find(bidRequests, hasAppId); let appIdObj; if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { appIdObj = { appid: appIdObjBid.params.app.id }; } const payload = { tags: [...tags], user: userObj, sdk: { source: SOURCE, version: '$prebid.version$' } }; if (appDeviceObjBid) { payload.device = appDeviceObj } if (appIdObjBid) { payload.app = appIdObj; } if (bidderRequest && bidderRequest.gdprConsent) { // note - objects for impbus use underscore instead of camelCase payload.gdpr_consent = { consent_string: bidderRequest.gdprConsent.consentString, consent_required: bidderRequest.gdprConsent.gdprApplies }; } if (bidderRequest && bidderRequest.refererInfo) { let refererinfo = { rd_ref: encodeURIComponent(bidderRequest.refererInfo.referer), rd_top: bidderRequest.refererInfo.reachedTop, rd_ifs: bidderRequest.refererInfo.numIframes, rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') } payload.referrer_detection = refererinfo; } let fpdcfg = config.getLegacyFpd(config.getConfig('ortb2')); if (fpdcfg && fpdcfg.context) { let fdata = { keywords: fpdcfg.context.keywords || '', category: fpdcfg.context.category || '' } payload.fpd = fdata; } const request = formatRequest(payload, bidderRequest); return request; }, /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, {bidderRequest}) { serverResponse = serverResponse.body; const bids = []; if (!serverResponse || serverResponse.error) { let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; } utils.logError(errorMessage); return bids; } if (serverResponse.tags) { serverResponse.tags.forEach(serverBid => { const rtbBid = getRtbBid(serverBid); if (rtbBid) { if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { const bid = newBid(serverBid, rtbBid, bidderRequest); bid.mediaType = parseMediaType(rtbBid); bids.push(bid); } } }); } return bids; }, transformBidParams: function(params, isOpenRtb) { params = utils.convertTypes({ 'placementId': 'number', 'keywords': utils.transformBidderParamKeywords }, params); if (isOpenRtb) { params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; if (params.usePaymentRule) { delete params.usePaymentRule; } if (isPopulatedArray(params.keywords)) { params.keywords.forEach(deleteValues); } Object.keys(params).forEach(paramKey => { let convertedKey = utils.convertCamelToUnderscore(paramKey); if (convertedKey !== paramKey) { params[convertedKey] = params[paramKey]; delete params[paramKey]; } }); } return params; } } function isPopulatedArray(arr) { return !!(utils.isArray(arr) && arr.length > 0); } function deleteValues(keyPairObj) { if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { delete keyPairObj.value; } } function formatRequest(payload, bidderRequest) { let request = []; if (payload.tags.length > MAX_IMPS_PER_REQUEST) { const clonedPayload = utils.deepClone(payload); utils.chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { clonedPayload.tags = tags; const payloadString = JSON.stringify(clonedPayload); request.push({ method: 'POST', url: URL, data: payloadString, bidderRequest }); }); } else { const payloadString = JSON.stringify(payload); request = { method: 'POST', url: URL, data: payloadString, bidderRequest }; } return request; } function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { const renderer = Renderer.install({ id: rtbBid.renderer_id, url: rtbBid.renderer_url, config: rendererOptions, loaded: false, adUnitCode }); try { renderer.setRender(outstreamRender); } catch (err) { utils.logWarn('Prebid Error calling setRender on renderer', err); } renderer.setEventHandlers({ impression: () => utils.logMessage('AdRelevantis outstream video impression event'), loaded: () => utils.logMessage('AdRelevantis outstream video loaded event'), ended: () => { utils.logMessage('AdRelevantis outstream renderer video event'); document.querySelector(`#${adUnitCode}`).style.display = 'none'; } }); return renderer; } /** * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. * @param {string} elementId element id */ function hidedfpContainer(elementId) { var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); if (el[0]) { el[0].style.setProperty('display', 'none'); } } function outstreamRender(bid) { // push to render queue because ANOutstreamVideo may not be loaded yet hidedfpContainer(bid.adUnitCode); bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], targetId: bid.adUnitCode, // target div id to render video uuid: bid.adResponse.uuid, adResponse: bid.adResponse, rendererOptions: bid.renderer.getConfig() }, handleOutstreamRendererEvents.bind(null, bid)); }); } function handleOutstreamRendererEvents(bid, id, eventName) { bid.renderer.handleVideoEvent({ id, eventName }); } /** * Unpack the Server's Bid into a Prebid-compatible one. * @param serverBid * @param rtbBid * @param bidderRequest * @return Bid */ function newBid(serverBid, rtbBid, bidderRequest) { const bidRequest = utils.getBidRequest(serverBid.uuid, [bidderRequest]); const bid = { requestId: serverBid.uuid, cpm: rtbBid.cpm, creativeId: rtbBid.creative_id, dealId: rtbBid.deal_id, currency: 'USD', netRevenue: true, ttl: 300, adUnitCode: bidRequest.adUnitCode, adrelevantis: { buyerMemberId: rtbBid.buyer_member_id, dealPriority: rtbBid.deal_priority, dealCode: rtbBid.deal_code } }; if (rtbBid.advertiser_id) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } if (rtbBid.rtb.video) { Object.assign(bid, { width: rtbBid.rtb.video.player_width, height: rtbBid.rtb.video.player_height, vastImpUrl: rtbBid.notify_url, ttl: 3600 }); const videoContext = utils.deepAccess(bidRequest, 'mediaTypes.video.context'); switch (videoContext) { case OUTSTREAM: bid.adResponse = serverBid; bid.adResponse.ad = bid.adResponse.ads[0]; bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; bid.vastXml = rtbBid.rtb.video.content; if (rtbBid.renderer_url) { const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); const rendererOptions = utils.deepAccess(videoBid, 'renderer.options'); bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); } break; case INSTREAM: bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); break; } } else if (rtbBid.rtb[NATIVE]) { const nativeAd = rtbBid.rtb[NATIVE]; // setting up the jsTracker: // we put it as a data-src attribute so that the tracker isn't called // until we have the adId (see onBidWon) let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); let jsTrackers = nativeAd.javascript_trackers; if (jsTrackers == undefined) { jsTrackers = jsTrackerDisarmed; } else if (utils.isStr(jsTrackers)) { jsTrackers = [jsTrackers, jsTrackerDisarmed]; } else { jsTrackers.push(jsTrackerDisarmed); } bid[NATIVE] = { title: nativeAd.title, body: nativeAd.desc, body2: nativeAd.desc2, cta: nativeAd.ctatext, rating: nativeAd.rating, sponsoredBy: nativeAd.sponsored, privacyLink: nativeAd.privacy_link, address: nativeAd.address, downloads: nativeAd.downloads, likes: nativeAd.likes, phone: nativeAd.phone, price: nativeAd.price, salePrice: nativeAd.saleprice, clickUrl: nativeAd.link.url, displayUrl: nativeAd.displayurl, clickTrackers: nativeAd.link.click_trackers, impressionTrackers: nativeAd.impression_trackers, javascriptTrackers: jsTrackers }; if (nativeAd.main_img) { bid['native'].image = { url: nativeAd.main_img.url, height: nativeAd.main_img.height, width: nativeAd.main_img.width, }; } if (nativeAd.icon) { bid['native'].icon = { url: nativeAd.icon.url, height: nativeAd.icon.height, width: nativeAd.icon.width, }; } } else { Object.assign(bid, { width: rtbBid.rtb.banner.width, height: rtbBid.rtb.banner.height, ad: rtbBid.rtb.banner.content }); try { const url = rtbBid.rtb.trackers[0].impression_urls[0]; const tracker = utils.createTrackPixelHtml(url); bid.ad += tracker; } catch (error) { utils.logError('Error appending tracking pixel', error); } } return bid; } function bidToTag(bid) { const tag = {}; tag.sizes = transformSizes(bid.sizes); tag.primary_size = tag.sizes[0]; tag.ad_types = []; tag.uuid = bid.bidId; if (bid.params.placementId) { tag.id = parseInt(bid.params.placementId, 10); } if (bid.params.cpm) { tag.cpm = bid.params.cpm; } tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; tag.use_pmt_rule = bid.params.usePaymentRule || false tag.prebid = true; tag.disable_psa = true; if (bid.params.position) { tag.position = {'above': 1, 'below': 2}[bid.params.position] || 0; } if (bid.params.trafficSourceCode) { tag.traffic_source_code = bid.params.trafficSourceCode; } if (bid.params.privateSizes) { tag.private_sizes = transformSizes(bid.params.privateSizes); } if (bid.params.supplyType) { tag.supply_type = bid.params.supplyType; } if (bid.params.pubClick) { tag.pubclick = bid.params.pubClick; } if (bid.params.extInvCode) { tag.ext_inv_code = bid.params.extInvCode; } if (bid.params.externalImpId) { tag.external_imp_id = bid.params.externalImpId; } if (!utils.isEmpty(bid.params.keywords)) { let keywords = utils.transformBidderParamKeywords(bid.params.keywords); if (keywords.length > 0) { keywords.forEach(deleteValues); } tag.keywords = keywords; } if (bid.params.category) { tag.category = bid.params.category; } if (bid.mediaType === NATIVE || utils.deepAccess(bid, `mediaTypes.${NATIVE}`)) { tag.ad_types.push(NATIVE); if (tag.sizes.length === 0) { tag.sizes = transformSizes([1, 1]); } if (bid.nativeParams) { const nativeRequest = buildNativeRequest(bid.nativeParams); tag[NATIVE] = {layouts: [nativeRequest]}; } } const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); tag.hb_source = 1; if (bid.mediaType === VIDEO || videoMediaType) { tag.ad_types.push(VIDEO); } // instream gets vastUrl, outstream gets vastXml if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { tag.require_asset_url = true; } if (bid.params.video) { tag.video = {}; // place any valid video params on the tag Object.keys(bid.params.video) .filter(param => includes(VIDEO_TARGETING, param)) .forEach(param => tag.video[param] = bid.params.video[param]); } if (bid.renderer) { tag.video = Object.assign({}, tag.video, {custom_renderer_present: true}); } if ( (utils.isEmpty(bid.mediaType) && utils.isEmpty(bid.mediaTypes)) || (bid.mediaType === BANNER || (bid.mediaTypes && bid.mediaTypes[BANNER])) ) { tag.ad_types.push(BANNER); } return tag; } /* Turn bid request sizes into ut-compatible format */ function transformSizes(requestSizes) { let sizes = []; let sizeObj = {}; if (utils.isArray(requestSizes) && requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { sizeObj.width = parseInt(requestSizes[0], 10); sizeObj.height = parseInt(requestSizes[1], 10); sizes.push(sizeObj); } else if (typeof requestSizes === 'object') { for (let i = 0; i < requestSizes.length; i++) { let size = requestSizes[i]; sizeObj = {}; sizeObj.width = parseInt(size[0], 10); sizeObj.height = parseInt(size[1], 10); sizes.push(sizeObj); } } return sizes; } function hasUserInfo(bid) { return !!bid.params.user; } function hasAppDeviceInfo(bid) { if (bid.params) { return !!bid.params.app } } function hasAppId(bid) { if (bid.params && bid.params.app) { return !!bid.params.app.id } return !!bid.params.app } function getRtbBid(tag) { return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); } function buildNativeRequest(params) { const request = {}; // map standard prebid native asset identifier to /ut parameters // e.g., tag specifies `body` but /ut only knows `description`. // mapping may be in form {tag: '<server name>'} or // {tag: {serverName: '<server name>', requiredParams: {...}}} Object.keys(params).forEach(key => { // check if one of the <server name> forms is used, otherwise // a mapping wasn't specified so pass the key straight through const requestKey = (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || NATIVE_MAPPING[key] || key; // required params are always passed on request const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; request[requestKey] = Object.assign({}, requiredParams, params[key]); // convert the sizes of image/icon assets to proper format (if needed) const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); if (isImageAsset && request[requestKey].sizes) { let sizes = request[requestKey].sizes; if (utils.isArrayOfNums(sizes) || (utils.isArray(sizes) && sizes.length > 0 && sizes.every(sz => utils.isArrayOfNums(sz)))) { request[requestKey].sizes = transformSizes(request[requestKey].sizes); } } if (requestKey === NATIVE_MAPPING.privacyLink) { request.privacy_supported = true; } }); return request; } function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; if (adType === VIDEO) { return VIDEO; } else { return BANNER; } } registerBidder(spec);