mk9-prebid
Version:
Header Bidding Management Library
601 lines (531 loc) • 18.3 kB
JavaScript
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);