mk9-prebid
Version:
Header Bidding Management Library
584 lines (500 loc) • 18.6 kB
JavaScript
import {config} from '../src/config.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import * as utils from '../src/utils.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import includes from 'core-js-pure/features/array/includes.js'
const SUPPORTED_AD_TYPES = [BANNER, VIDEO];
const VIDEO_TARGETING = ['startdelay', 'mimes', 'minduration', 'maxduration',
'startdelay', 'skippable', 'playbackmethod', 'api', 'protocols', 'boxingallowed',
'linearity', 'delivery', 'protocol', 'placement', 'minbitrate', 'maxbitrate'];
const BIDDER_CODE = 'openx';
const BIDDER_CONFIG = 'hb_pb';
const BIDDER_VERSION = '3.0.3';
const DEFAULT_CURRENCY = 'USD';
export const USER_ID_CODE_TO_QUERY_ARG = {
britepoolid: 'britepoolid', // BritePool ID
criteoId: 'criteoid', // CriteoID
fabrickId: 'nuestarid', // Fabrick ID by Nuestar
haloId: 'audigentid', // Halo ID from Audigent
id5id: 'id5id', // ID5 ID
idl_env: 'lre', // LiveRamp IdentityLink
IDP: 'zeotapid', // zeotapIdPlus ID+
idxId: 'idxid', // idIDx,
intentIqId: 'intentiqid', // IntentIQ ID
lipb: 'lipbid', // LiveIntent ID
lotamePanoramaId: 'lotameid', // Lotame Panorama ID
merkleId: 'merkleid', // Merkle ID
netId: 'netid', // netID
parrableId: 'parrableid', // Parrable ID
pubcid: 'pubcid', // PubCommon ID
quantcastId: 'quantcastid', // Quantcast ID
tapadId: 'tapadid', // Tapad Id
tdid: 'ttduuid', // The Trade Desk Unified ID
uid2: 'uid2', // Unified ID 2.0
flocId: 'floc', // Chrome FLoC,
admixerId: 'admixerid', // AdMixer ID
deepintentId: 'deepintentid', // DeepIntent ID
dmdId: 'dmdid', // DMD Marketing Corp ID
nextrollId: 'nextrollid', // NextRoll ID
novatiq: 'novatiqid', // Novatiq ID
mwOpenLinkId: 'mwopenlinkid', // MediaWallah OpenLink ID
dapId: 'dapid', // Akamai DAP ID
amxId: 'amxid' // AMX RTB ID
};
export const spec = {
code: BIDDER_CODE,
gvlid: 69,
supportedMediaTypes: SUPPORTED_AD_TYPES,
isBidRequestValid: function (bidRequest) {
const hasDelDomainOrPlatform = bidRequest.params.delDomain || bidRequest.params.platform;
if (utils.deepAccess(bidRequest, 'mediaTypes.banner') && hasDelDomainOrPlatform) {
return !!bidRequest.params.unit || utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0;
}
return !!(bidRequest.params.unit && hasDelDomainOrPlatform);
},
buildRequests: function (bidRequests, bidderRequest) {
if (bidRequests.length === 0) {
return [];
}
let requests = [];
let [videoBids, bannerBids] = partitionByVideoBids(bidRequests);
// build banner requests
if (bannerBids.length > 0) {
requests.push(buildOXBannerRequest(bannerBids, bidderRequest));
}
// build video requests
if (videoBids.length > 0) {
videoBids.forEach(videoBid => {
requests.push(buildOXVideoRequest(videoBid, bidderRequest))
});
}
return requests;
},
interpretResponse: function ({body: oxResponseObj}, serverRequest) {
let mediaType = getMediaTypeFromRequest(serverRequest);
return mediaType === VIDEO ? createVideoBidResponses(oxResponseObj, serverRequest.payload)
: createBannerBidResponses(oxResponseObj, serverRequest.payload);
},
getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) {
if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) {
let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image';
let url = utils.deepAccess(responses, '0.body.ads.pixels') ||
utils.deepAccess(responses, '0.body.pixels') ||
generateDefaultSyncUrl(gdprConsent, uspConsent);
return [{
type: pixelType,
url: url
}];
}
},
transformBidParams: function(params, isOpenRtb) {
return utils.convertTypes({
'unit': 'string',
'customFloor': 'number'
}, params);
}
};
function generateDefaultSyncUrl(gdprConsent, uspConsent) {
let url = 'https://u.openx.net/w/1.0/pd';
let queryParamStrings = [];
if (gdprConsent) {
queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0));
queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
}
// CCPA
if (uspConsent) {
queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent));
}
return `${url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}`;
}
function isVideoRequest(bidRequest) {
return (utils.deepAccess(bidRequest, 'mediaTypes.video') && !utils.deepAccess(bidRequest, 'mediaTypes.banner')) || bidRequest.mediaType === VIDEO;
}
function createBannerBidResponses(oxResponseObj, {bids, startTime}) {
let adUnits = oxResponseObj.ads.ad;
let bidResponses = [];
for (let i = 0; i < adUnits.length; i++) {
let adUnit = adUnits[i];
let adUnitIdx = parseInt(adUnit.idx, 10);
let bidResponse = {};
bidResponse.requestId = bids[adUnitIdx].bidId;
if (adUnit.pub_rev) {
bidResponse.cpm = Number(adUnit.pub_rev) / 1000;
} else {
// No fill, do not add the bidresponse
continue;
}
let creative = adUnit.creative[0];
if (creative) {
bidResponse.width = creative.width;
bidResponse.height = creative.height;
}
bidResponse.creativeId = creative.id;
bidResponse.ad = adUnit.html;
if (adUnit.deal_id) {
bidResponse.dealId = adUnit.deal_id;
}
// default 5 mins
bidResponse.ttl = 300;
// true is net, false is gross
bidResponse.netRevenue = true;
bidResponse.currency = adUnit.currency;
// additional fields to add
if (adUnit.tbd) {
bidResponse.tbd = adUnit.tbd;
}
bidResponse.ts = adUnit.ts;
bidResponse.meta = {};
if (adUnit.brand_id) {
bidResponse.meta.brandId = adUnit.brand_id;
}
if (adUnit.adomain && length(adUnit.adomain) > 0) {
bidResponse.meta.advertiserDomains = adUnit.adomain;
} else {
bidResponse.meta.advertiserDomains = [];
}
if (adUnit.adv_id) {
bidResponse.meta.dspid = adUnit.adv_id;
}
bidResponses.push(bidResponse);
}
return bidResponses;
}
function getViewportDimensions(isIfr) {
let width;
let height;
let tWin = window;
let tDoc = document;
let docEl = tDoc.documentElement;
let body;
if (isIfr) {
try {
tWin = window.top;
tDoc = window.top.document;
} catch (e) {
return;
}
docEl = tDoc.documentElement;
body = tDoc.body;
width = tWin.innerWidth || docEl.clientWidth || body.clientWidth;
height = tWin.innerHeight || docEl.clientHeight || body.clientHeight;
} else {
docEl = tDoc.documentElement;
width = tWin.innerWidth || docEl.clientWidth;
height = tWin.innerHeight || docEl.clientHeight;
}
return `${width}x${height}`;
}
function formatCustomParms(customKey, customParams) {
let value = customParams[customKey];
if (utils.isArray(value)) {
// if value is an array, join them with commas first
value = value.join(',');
}
// return customKey=customValue format, escaping + to . and / to _
return (customKey.toLowerCase() + '=' + value.toLowerCase()).replace('+', '.').replace('/', '_')
}
function partitionByVideoBids(bidRequests) {
return bidRequests.reduce(function (acc, bid) {
// Fallback to banner ads if nothing specified
if (isVideoRequest(bid)) {
acc[0].push(bid);
} else {
acc[1].push(bid);
}
return acc;
}, [[], []]);
}
function getMediaTypeFromRequest(serverRequest) {
return /avjp$/.test(serverRequest.url) ? VIDEO : BANNER;
}
function buildCommonQueryParamsFromBids(bids, bidderRequest) {
const isInIframe = utils.inIframe();
let defaultParams;
defaultParams = {
ju: config.getConfig('pageUrl') || bidderRequest.refererInfo.referer,
ch: document.charSet || document.characterSet,
res: `${screen.width}x${screen.height}x${screen.colorDepth}`,
ifr: isInIframe,
tz: new Date().getTimezoneOffset(),
tws: getViewportDimensions(isInIframe),
be: 1,
bc: bids[0].params.bc || `${BIDDER_CONFIG}_${BIDDER_VERSION}`,
dddid: utils._map(bids, bid => bid.transactionId).join(','),
nocache: new Date().getTime()
};
const firstPartyData = config.getConfig('ortb2.user.data')
if (Array.isArray(firstPartyData) && firstPartyData.length > 0) {
// extract and merge valid segments by provider/taxonomy
const fpd = firstPartyData
.filter(
data => (Array.isArray(data.segment) &&
data.segment.length > 0 &&
data.name !== undefined &&
data.name.length > 0)
)
.reduce((acc, data) => {
const name = typeof data.ext === 'object' && data.ext.segtax ? `${data.name}/${data.ext.segtax}` : data.name;
acc[name] = (acc[name] || []).concat(data.segment.map(seg => seg.id));
return acc;
}, {})
const sm = Object.keys(fpd)
.map((name, _) => name + ':' + fpd[name].join('|'))
.join(',')
if (sm.length > 0) {
defaultParams.sm = encodeURIComponent(sm);
}
}
if (bids[0].params.platform) {
defaultParams.ph = bids[0].params.platform;
}
if (bidderRequest.gdprConsent) {
let gdprConsentConfig = bidderRequest.gdprConsent;
if (gdprConsentConfig.consentString !== undefined) {
defaultParams.gdpr_consent = gdprConsentConfig.consentString;
}
if (gdprConsentConfig.gdprApplies !== undefined) {
defaultParams.gdpr = gdprConsentConfig.gdprApplies ? 1 : 0;
}
if (config.getConfig('consentManagement.cmpApi') === 'iab') {
defaultParams.x_gdpr_f = 1;
}
}
if (bidderRequest && bidderRequest.uspConsent) {
defaultParams.us_privacy = bidderRequest.uspConsent;
}
// normalize publisher common id
if (utils.deepAccess(bids[0], 'crumbs.pubcid')) {
utils.deepSetValue(bids[0], 'userId.pubcid', utils.deepAccess(bids[0], 'crumbs.pubcid'));
}
defaultParams = appendUserIdsToQueryParams(defaultParams, bids[0].userId);
// supply chain support
if (bids[0].schain) {
defaultParams.schain = serializeSupplyChain(bids[0].schain);
}
return defaultParams;
}
function appendUserIdsToQueryParams(queryParams, userIds) {
utils._each(userIds, (userIdObjectOrValue, userIdProviderKey) => {
const key = USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey];
if (USER_ID_CODE_TO_QUERY_ARG.hasOwnProperty(userIdProviderKey)) {
switch (userIdProviderKey) {
case 'flocId':
queryParams[key] = userIdObjectOrValue.id;
break;
case 'uid2':
queryParams[key] = userIdObjectOrValue.id;
break;
case 'lipb':
queryParams[key] = userIdObjectOrValue.lipbid;
if (Array.isArray(userIdObjectOrValue.segments) && userIdObjectOrValue.segments.length > 0) {
const liveIntentSegments = 'liveintent:' + userIdObjectOrValue.segments.join('|')
queryParams.sm = `${queryParams.sm ? queryParams.sm + encodeURIComponent(',') : ''}${encodeURIComponent(liveIntentSegments)}`;
}
break;
case 'parrableId':
queryParams[key] = userIdObjectOrValue.eid;
break;
case 'id5id':
queryParams[key] = userIdObjectOrValue.uid;
break;
case 'novatiq':
queryParams[key] = userIdObjectOrValue.snowflake;
break;
default:
queryParams[key] = userIdObjectOrValue;
}
}
});
return queryParams;
}
function serializeSupplyChain(supplyChain) {
return `${supplyChain.ver},${supplyChain.complete}!${serializeSupplyChainNodes(supplyChain.nodes)}`;
}
function serializeSupplyChainNodes(supplyChainNodes) {
const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain'];
return supplyChainNodes.map(supplyChainNode => {
return supplyChainNodePropertyOrder.map(property => supplyChainNode[property] || '')
.join(',');
}).join('!');
}
function buildOXBannerRequest(bids, bidderRequest) {
let customParamsForAllBids = [];
let hasCustomParam = false;
let queryParams = buildCommonQueryParamsFromBids(bids, bidderRequest);
let auids = utils._map(bids, bid => bid.params.unit);
queryParams.aus = utils._map(bids, bid => utils.parseSizesInput(bid.mediaTypes.banner.sizes).join(',')).join('|');
queryParams.divids = utils._map(bids, bid => encodeURIComponent(bid.adUnitCode)).join(',');
// gpid
queryParams.aucs = utils._map(bids, function (bid) {
let gpid = utils.deepAccess(bid, 'ortb2Imp.ext.data.pbadslot');
return encodeURIComponent(gpid || '')
}).join(',');
if (auids.some(auid => auid)) {
queryParams.auid = auids.join(',');
}
if (bids.some(bid => bid.params.doNotTrack)) {
queryParams.ns = 1;
}
if (config.getConfig('coppa') === true || bids.some(bid => bid.params.coppa)) {
queryParams.tfcd = 1;
}
bids.forEach(function (bid) {
if (bid.params.customParams) {
let customParamsForBid = utils._map(Object.keys(bid.params.customParams), customKey => formatCustomParms(customKey, bid.params.customParams));
let formattedCustomParams = window.btoa(customParamsForBid.join('&'));
hasCustomParam = true;
customParamsForAllBids.push(formattedCustomParams);
} else {
customParamsForAllBids.push('');
}
});
if (hasCustomParam) {
queryParams.tps = customParamsForAllBids.join(',');
}
enrichQueryWithFloors(queryParams, BANNER, bids);
let url = queryParams.ph
? `https://u.openx.net/w/1.0/arj`
: `https://${bids[0].params.delDomain}/w/1.0/arj`;
return {
method: 'GET',
url: url,
data: queryParams,
payload: {'bids': bids, 'startTime': new Date()}
};
}
function buildOXVideoRequest(bid, bidderRequest) {
let oxVideoParams = generateVideoParameters(bid, bidderRequest);
let url = oxVideoParams.ph
? `https://u.openx.net/v/1.0/avjp`
: `https://${bid.params.delDomain}/v/1.0/avjp`;
return {
method: 'GET',
url: url,
data: oxVideoParams,
payload: {'bid': bid, 'startTime': new Date()}
};
}
function generateVideoParameters(bid, bidderRequest) {
const videoMediaType = utils.deepAccess(bid, `mediaTypes.video`);
let queryParams = buildCommonQueryParamsFromBids([bid], bidderRequest);
let oxVideoConfig = utils.deepAccess(bid, 'params.video') || {};
let context = utils.deepAccess(bid, 'mediaTypes.video.context');
let playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize');
let width;
let height;
// normalize config for video size
if (utils.isArray(bid.sizes) && bid.sizes.length === 2 && !utils.isArray(bid.sizes[0])) {
width = parseInt(bid.sizes[0], 10);
height = parseInt(bid.sizes[1], 10);
} else if (utils.isArray(bid.sizes) && utils.isArray(bid.sizes[0]) && bid.sizes[0].length === 2) {
width = parseInt(bid.sizes[0][0], 10);
height = parseInt(bid.sizes[0][1], 10);
} else if (utils.isArray(playerSize) && playerSize.length === 2) {
width = parseInt(playerSize[0], 10);
height = parseInt(playerSize[1], 10);
}
let openRtbParams = {w: width, h: height};
// legacy openrtb params could be in video, openrtb, or video.openrtb
let legacyParams = bid.params.video || bid.params.openrtb || {};
if (legacyParams.openrtb) {
legacyParams = legacyParams.openrtb;
}
// support for video object or full openrtb object
if (utils.isArray(legacyParams.imp)) {
legacyParams = legacyParams.imp[0].video;
}
Object.keys(legacyParams)
.filter(param => includes(VIDEO_TARGETING, param))
.forEach(param => openRtbParams[param] = legacyParams[param]);
// 5.0 openrtb video params
Object.keys(videoMediaType)
.filter(param => includes(VIDEO_TARGETING, param))
.forEach(param => openRtbParams[param] = videoMediaType[param]);
let openRtbReq = {
imp: [
{
video: openRtbParams
}
]
}
queryParams['openrtb'] = JSON.stringify(openRtbReq);
queryParams.auid = bid.params.unit;
// override prebid config with openx config if available
queryParams.vwd = width || oxVideoConfig.vwd;
queryParams.vht = height || oxVideoConfig.vht;
if (context === 'outstream') {
queryParams.vos = '101';
}
if (oxVideoConfig.mimes) {
queryParams.vmimes = oxVideoConfig.mimes;
}
if (bid.params.test) {
queryParams.vtest = 1;
}
let gpid = utils.deepAccess(bid, 'ortb2Imp.ext.data.pbadslot');
if (gpid) {
queryParams.aucs = encodeURIComponent(gpid)
}
// each video bid makes a separate request
enrichQueryWithFloors(queryParams, VIDEO, [bid]);
return queryParams;
}
function createVideoBidResponses(response, {bid, startTime}) {
let bidResponses = [];
if (response !== undefined && response.vastUrl !== '' && response.pub_rev > 0) {
let vastQueryParams = utils.parseUrl(response.vastUrl).search || {};
let bidResponse = {};
bidResponse.requestId = bid.bidId;
if (response.deal_id) {
bidResponse.dealId = response.deal_id;
}
// default 5 mins
bidResponse.ttl = 300;
// true is net, false is gross
bidResponse.netRevenue = true;
bidResponse.currency = response.currency;
bidResponse.cpm = parseInt(response.pub_rev, 10) / 1000;
bidResponse.width = parseInt(response.width, 10);
bidResponse.height = parseInt(response.height, 10);
bidResponse.creativeId = response.adid;
bidResponse.vastUrl = response.vastUrl;
bidResponse.mediaType = VIDEO;
// enrich adunit with vast parameters
response.ph = vastQueryParams.ph;
response.colo = vastQueryParams.colo;
response.ts = vastQueryParams.ts;
bidResponses.push(bidResponse);
}
return bidResponses;
}
function enrichQueryWithFloors(queryParams, mediaType, bids) {
let customFloorsForAllBids = [];
let hasCustomFloor = false;
bids.forEach(function (bid) {
let floor = getBidFloor(bid, mediaType);
if (floor) {
customFloorsForAllBids.push(floor);
hasCustomFloor = true;
} else {
customFloorsForAllBids.push(0);
}
});
if (hasCustomFloor) {
queryParams.aumfs = customFloorsForAllBids.join(',');
}
}
function getBidFloor(bidRequest, mediaType) {
let floorInfo = {};
const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY;
if (typeof bidRequest.getFloor === 'function') {
floorInfo = bidRequest.getFloor({
currency: currency,
mediaType: mediaType,
size: '*'
});
}
let floor = floorInfo.floor || bidRequest.params.customFloor || 0;
return Math.round(floor * 1000); // normalize to micro currency
}
registerBidder(spec);