UNPKG

mk9-prebid

Version:

Header Bidding Management Library

715 lines (657 loc) 23.8 kB
import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import { createEidsArray } from './userId/eids.js'; import includes from 'core-js-pure/features/array/includes.js'; const BIDDER_CODE = 'improvedigital'; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const VIDEO_TARGETING = ['skip', 'skipmin', 'skipafter']; export const spec = { version: '7.4.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** * 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 && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); }, /** * 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) { let normalizedBids = bidRequests.map((bidRequest) => { return getNormalizedBidRequest(bidRequest); }); let idClient = new ImproveDigitalAdServerJSClient('hb'); let requestParameters = { singleRequestMode: (config.getConfig('improvedigital.singleRequest') === true), returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT, libVersion: this.version }; if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { requestParameters.gdpr = bidderRequest.gdprConsent.consentString; } if (bidderRequest && bidderRequest.uspConsent) { requestParameters.usPrivacy = bidderRequest.uspConsent; } if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { requestParameters.referrer = bidderRequest.refererInfo.referer; } requestParameters.schain = bidRequests[0].schain; if (bidRequests[0].userId) { const eids = createEidsArray(bidRequests[0].userId); if (eids.length) { utils.deepSetValue(requestParameters, 'user.ext.eids', eids); } } let requestObj = idClient.createRequest( normalizedBids, // requestObject requestParameters ); if (requestObj.errors && requestObj.errors.length > 0) { utils.logError('ID WARNING 0x01'); } requestObj.requests.forEach(request => request.bidderRequest = bidderRequest); return requestObj.requests; }, /** * 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}) { const bids = []; utils._each(serverResponse.body.bid, function (bidObject) { if (!bidObject.price || bidObject.price === null || bidObject.hasOwnProperty('errorCode') || (!bidObject.adm && !bidObject.native)) { return; } const bidRequest = utils.getBidRequest(bidObject.id, [bidderRequest]); const bid = {}; if (bidObject.native) { // Native bid.native = getNormalizedNativeAd(bidObject.native); // Expose raw oRTB response to the client to allow parsing assets not directly supported by Prebid bid.ortbNative = bidObject.native; if (bidObject.nurl) { bid.native.impressionTrackers.unshift(bidObject.nurl); } bid.mediaType = NATIVE; } else if (bidObject.ad_type && bidObject.ad_type === 'video') { bid.vastXml = bidObject.adm; bid.mediaType = VIDEO; if (isOutstreamVideo(bidRequest)) { bid.adResponse = { content: bid.vastXml, height: bidObject.h, width: bidObject.w }; bid.renderer = createRenderer(bidRequest); } } else { // Banner let nurl = ''; if (bidObject.nurl && bidObject.nurl.length > 0) { nurl = `<img src="${bidObject.nurl}" width="0" height="0" style="display:none">`; } bid.ad = `${nurl}<script>${bidObject.adm}</script>`; bid.mediaType = BANNER; } // Common properties bid.cpm = parseFloat(bidObject.price); bid.creativeId = bidObject.crid; bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; // Deal ID. Composite ads can have multiple line items and the ID of the first // dealID line item will be used. if (utils.isNumber(bidObject.lid) && bidObject.buying_type && bidObject.buying_type !== 'rtb') { bid.dealId = bidObject.lid; } else if (Array.isArray(bidObject.lid) && Array.isArray(bidObject.buying_type) && bidObject.lid.length === bidObject.buying_type.length) { let isDeal = false; bidObject.buying_type.forEach((bt, i) => { if (isDeal) return; if (bt && bt !== 'rtb') { isDeal = true; bid.dealId = bidObject.lid[i]; } }); } bid.height = bidObject.h; bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; bid.requestId = bidObject.id; bid.ttl = 300; bid.width = bidObject.w; if (!bid.width || !bid.height) { bid.width = 1; bid.height = 1; } if (bidObject.adomain) { bid.meta = { advertiserDomains: bidObject.adomain }; } bids.push(bid); }); return bids; }, /** * Register the user sync pixels which should be dropped after the auction. * * @param {SyncOptions} syncOptions Which user syncs are allowed? * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function(syncOptions, serverResponses) { if (syncOptions.pixelEnabled) { const syncs = []; serverResponses.forEach(response => { response.body.bid.forEach(bidObject => { if (utils.isArray(bidObject.sync)) { bidObject.sync.forEach(syncElement => { if (syncs.indexOf(syncElement) === -1) { syncs.push(syncElement); } }); } }); }); return syncs.map(sync => ({ type: 'image', url: sync })); } return []; } }; function isInstreamVideo(bid) { const mediaTypes = Object.keys(utils.deepAccess(bid, 'mediaTypes', {})); const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); return bid.mediaType === 'video' || (mediaTypes.length === 1 && videoMediaType && context !== 'outstream'); } function isOutstreamVideo(bid) { const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); return videoMediaType && context === 'outstream'; } function getVideoTargetingParams(bid) { const result = {}; Object.keys(Object(bid.mediaTypes.video)) .filter(key => includes(VIDEO_TARGETING, key)) .forEach(key => { result[ key ] = bid.mediaTypes.video[ key ]; }); Object.keys(Object(bid.params.video)) .filter(key => includes(VIDEO_TARGETING, key)) .forEach(key => { result[ key ] = bid.params.video[ key ]; }); return result; } function getBidFloor(bid) { if (!utils.isFn(bid.getFloor)) { return null; } const floor = bid.getFloor({ currency: 'USD', mediaType: '*', size: '*' }); if (utils.isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { return floor.floor; } return null; } function outstreamRender(bid) { bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ sizes: [bid.width, bid.height], targetId: bid.adUnitCode, adResponse: bid.adResponse, rendererOptions: bid.renderer.getConfig() }, handleOutstreamRendererEvents.bind(null, bid)); }); } function handleOutstreamRendererEvents(bid, id, eventName) { bid.renderer.handleVideoEvent({ id, eventName }); } function createRenderer(bidRequest) { const renderer = Renderer.install({ id: bidRequest.adUnitCode, url: RENDERER_URL, loaded: false, config: utils.deepAccess(bidRequest, 'renderer.options'), adUnitCode: bidRequest.adUnitCode }); try { renderer.setRender(outstreamRender); } catch (err) { utils.logWarn('Prebid Error calling setRender on renderer', err); } return renderer; } function getNormalizedBidRequest(bid) { let adUnitId = utils.getBidIdParameter('adUnitCode', bid) || null; let placementId = utils.getBidIdParameter('placementId', bid.params) || null; let publisherId = null; let placementKey = null; if (placementId === null) { publisherId = utils.getBidIdParameter('publisherId', bid.params) || null; placementKey = utils.getBidIdParameter('placementKey', bid.params) || null; } const keyValues = utils.getBidIdParameter('keyValues', bid.params) || null; const singleSizeFilter = utils.getBidIdParameter('size', bid.params) || null; const bidId = utils.getBidIdParameter('bidId', bid); const transactionId = utils.getBidIdParameter('transactionId', bid); const currency = config.getConfig('currency.adServerCurrency'); let normalizedBidRequest = {}; if (isInstreamVideo(bid)) { normalizedBidRequest.adTypes = [ VIDEO ]; } if (isInstreamVideo(bid) || isOutstreamVideo(bid)) { normalizedBidRequest.video = getVideoTargetingParams(bid); } if (placementId) { normalizedBidRequest.placementId = placementId; } else { if (publisherId) { normalizedBidRequest.publisherId = publisherId; } if (placementKey) { normalizedBidRequest.placementKey = placementKey; } } if (keyValues) { normalizedBidRequest.keyValues = keyValues; } if (config.getConfig('improvedigital.usePrebidSizes') === true && !isInstreamVideo(bid) && !isOutstreamVideo(bid) && bid.sizes && bid.sizes.length > 0) { normalizedBidRequest.format = bid.sizes; } else if (singleSizeFilter && singleSizeFilter.w && singleSizeFilter.h) { normalizedBidRequest.size = {}; normalizedBidRequest.size.h = singleSizeFilter.h; normalizedBidRequest.size.w = singleSizeFilter.w; } if (bidId) { normalizedBidRequest.id = bidId; } if (adUnitId) { normalizedBidRequest.adUnitId = adUnitId; } if (transactionId) { normalizedBidRequest.transactionId = transactionId; } if (currency) { normalizedBidRequest.currency = currency; } // Floor let bidFloor = getBidFloor(bid); let bidFloorCur = null; if (!bidFloor) { bidFloor = utils.getBidIdParameter('bidFloor', bid.params); bidFloorCur = utils.getBidIdParameter('bidFloorCur', bid.params); } if (bidFloor) { normalizedBidRequest.bidFloor = bidFloor; normalizedBidRequest.bidFloorCur = bidFloorCur ? bidFloorCur.toUpperCase() : 'USD'; } return normalizedBidRequest; } function getNormalizedNativeAd(rawNative) { const native = {}; if (!rawNative || !utils.isArray(rawNative.assets)) { return null; } // Assets rawNative.assets.forEach(asset => { if (asset.title) { native.title = asset.title.text; } else if (asset.data) { switch (asset.data.type) { case 1: native.sponsoredBy = asset.data.value; break; case 2: native.body = asset.data.value; break; case 3: native.rating = asset.data.value; break; case 4: native.likes = asset.data.value; break; case 5: native.downloads = asset.data.value; break; case 6: native.price = asset.data.value; break; case 7: native.salePrice = asset.data.value; break; case 8: native.phone = asset.data.value; break; case 9: native.address = asset.data.value; break; case 10: native.body2 = asset.data.value; break; case 11: native.displayUrl = asset.data.value; break; case 12: native.cta = asset.data.value; break; } } else if (asset.img) { switch (asset.img.type) { case 2: native.icon = { url: asset.img.url, width: asset.img.w, height: asset.img.h }; break; case 3: native.image = { url: asset.img.url, width: asset.img.w, height: asset.img.h }; break; } } }); // Trackers if (rawNative.eventtrackers) { native.impressionTrackers = []; rawNative.eventtrackers.forEach(tracker => { // Only handle impression event. Viewability events are not supported yet. if (tracker.event !== 1) return; switch (tracker.method) { case 1: // img native.impressionTrackers.push(tracker.url); break; case 2: // js // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. native.javascriptTrackers = `<script src=\"${tracker.url}\"></script>`; break; } }); } else { native.impressionTrackers = rawNative.imptrackers || []; native.javascriptTrackers = rawNative.jstracker; } if (rawNative.link) { native.clickUrl = rawNative.link.url; native.clickTrackers = rawNative.link.clicktrackers; } if (rawNative.privacy) { native.privacyLink = rawNative.privacy; } return native; } registerBidder(spec); export function ImproveDigitalAdServerJSClient(endPoint) { this.CONSTANTS = { AD_SERVER_BASE_URL: 'ice.360yield.com', END_POINT: endPoint || 'hb', AD_SERVER_URL_PARAM: 'jsonp=', CLIENT_VERSION: 'JS-6.4.0', MAX_URL_LENGTH: 2083, ERROR_CODES: { MISSING_PLACEMENT_PARAMS: 2, LIB_VERSION_MISSING: 3 }, RETURN_OBJ_TYPE: { DEFAULT: 0, URL_PARAMS_SPLIT: 1 } }; this.getErrorReturn = function(errorCode) { return { idMappings: {}, requests: {}, 'errorCode': errorCode }; }; this.createRequest = function(requestObject, requestParameters, extraRequestParameters) { if (!requestParameters.libVersion) { return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); } requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; requestParameters.adServerBaseUrl = 'https://' + (requestParameters.adServerBaseUrl || this.CONSTANTS.AD_SERVER_BASE_URL); let impressionObjects = []; let impressionObject; if (utils.isArray(requestObject)) { for (let counter = 0; counter < requestObject.length; counter++) { impressionObject = this.createImpressionObject(requestObject[counter]); impressionObjects.push(impressionObject); } } else { impressionObject = this.createImpressionObject(requestObject); impressionObjects.push(impressionObject); } let returnIdMappings = true; if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT) { returnIdMappings = false; } let returnObject = {}; returnObject.requests = []; if (returnIdMappings) { returnObject.idMappings = []; } let errors = null; let baseUrl = `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; let bidRequestObject = { bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) }; for (let counter = 0; counter < impressionObjects.length; counter++) { impressionObject = impressionObjects[counter]; if (impressionObject.errorCode) { errors = errors || []; errors.push({ errorCode: impressionObject.errorCode, adUnitId: impressionObject.adUnitId }); } else { if (returnIdMappings) { returnObject.idMappings.push({ adUnitId: impressionObject.adUnitId, id: impressionObject.impressionObject.id }); } bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); let writeLongRequest = false; const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)); if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) { writeLongRequest = true; if (bidRequestObject.bid_request.imp.length > 1) { // Pop the current request and process it again in the next iteration bidRequestObject.bid_request.imp.pop(); if (returnIdMappings) { returnObject.idMappings.pop(); } counter--; } } if (writeLongRequest || !requestParameters.singleRequestMode || counter === impressionObjects.length - 1) { returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); bidRequestObject = { bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) }; } } } if (errors) { returnObject.errors = errors; } return returnObject; }; this.formatRequest = function(requestParameters, bidRequestObject) { switch (requestParameters.returnObjType) { case this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT: return { method: 'GET', url: `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}`, data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${encodeURIComponent(JSON.stringify(bidRequestObject))}` }; default: const baseUrl = `${requestParameters.adServerBaseUrl}/` + `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; return { url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) } } }; this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) { let impressionBidRequestObject = {}; impressionBidRequestObject.secure = 1; if (requestParameters.requestId) { impressionBidRequestObject.id = requestParameters.requestId; } else { impressionBidRequestObject.id = utils.getUniqueIdentifierStr(); } if (requestParameters.domain) { impressionBidRequestObject.domain = requestParameters.domain; } if (requestParameters.page) { impressionBidRequestObject.page = requestParameters.page; } if (requestParameters.ref) { impressionBidRequestObject.ref = requestParameters.ref; } if (requestParameters.callback) { impressionBidRequestObject.callback = requestParameters.callback; } if (requestParameters.libVersion) { impressionBidRequestObject.version = requestParameters.libVersion + '-' + this.CONSTANTS.CLIENT_VERSION; } if (requestParameters.referrer) { impressionBidRequestObject.referrer = requestParameters.referrer; } if (requestParameters.gdpr || requestParameters.gdpr === 0) { impressionBidRequestObject.gdpr = requestParameters.gdpr; } if (requestParameters.usPrivacy) { impressionBidRequestObject.us_privacy = requestParameters.usPrivacy; } if (requestParameters.schain) { impressionBidRequestObject.schain = requestParameters.schain; } if (requestParameters.user) { impressionBidRequestObject.user = requestParameters.user; } if (extraRequestParameters) { for (let prop in extraRequestParameters) { impressionBidRequestObject[prop] = extraRequestParameters[prop]; } } return impressionBidRequestObject; }; this.createImpressionObject = function(placementObject) { let outputObject = {}; let impressionObject = {}; outputObject.impressionObject = impressionObject; if (placementObject.id) { impressionObject.id = placementObject.id; } else { impressionObject.id = utils.getUniqueIdentifierStr(); } if (placementObject.adTypes) { impressionObject.ad_types = placementObject.adTypes; } if (placementObject.adUnitId) { outputObject.adUnitId = placementObject.adUnitId; } if (placementObject.currency) { impressionObject.currency = placementObject.currency.toUpperCase(); } if (placementObject.bidFloor) { impressionObject.bidfloor = placementObject.bidFloor; } if (placementObject.bidFloorCur) { impressionObject.bidfloorcur = placementObject.bidFloorCur.toUpperCase(); } if (placementObject.placementId) { impressionObject.pid = placementObject.placementId; } if (placementObject.publisherId) { impressionObject.pubid = placementObject.publisherId; } if (placementObject.placementKey) { impressionObject.pkey = placementObject.placementKey; } if (placementObject.transactionId) { impressionObject.tid = placementObject.transactionId; } if (!utils.isEmpty(placementObject.video)) { const video = Object.assign({}, placementObject.video); // skip must be 0 or 1 if (video.skip !== 1) { delete video.skipmin; delete video.skipafter; if (video.skip !== 0) { utils.logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); delete video.skip; } } if (!utils.isEmpty(video)) { impressionObject.video = video; } } if (placementObject.keyValues) { for (let key in placementObject.keyValues) { for (let valueCounter = 0; valueCounter < placementObject.keyValues[key].length; valueCounter++) { impressionObject.kvw = impressionObject.kvw || {}; impressionObject.kvw[key] = impressionObject.kvw[key] || []; impressionObject.kvw[key].push(placementObject.keyValues[key][valueCounter]); } } } impressionObject.banner = {}; if (placementObject.size && placementObject.size.w && placementObject.size.h) { impressionObject.banner.w = placementObject.size.w; impressionObject.banner.h = placementObject.size.h; } // Set of desired creative sizes // Input Format: array of pairs, i.e. [[300, 250], [250, 250]] if (placementObject.format && utils.isArray(placementObject.format)) { const format = placementObject.format .filter(sizePair => sizePair.length === 2 && utils.isInteger(sizePair[0]) && utils.isInteger(sizePair[1]) && sizePair[0] >= 0 && sizePair[1] >= 0) .map(sizePair => { return { w: sizePair[0], h: sizePair[1] } }); if (format.length > 0) { impressionObject.banner.format = format; } } if (!impressionObject.pid && !impressionObject.pubid && !impressionObject.pkey && !(impressionObject.banner && impressionObject.banner.w && impressionObject.banner.h)) { outputObject.impressionObject = null; outputObject.errorCode = this.CONSTANTS.ERROR_CODES.MISSING_PLACEMENT_PARAMS; } return outputObject; }; }