mk9-prebid
Version:
Header Bidding Management Library
448 lines (388 loc) • 13.5 kB
JavaScript
import * as utils from '../src/utils.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {getStorageManager} from '../src/storageManager.js';
const GVLID = 24;
export const storage = getStorageManager(GVLID);
const BIDDER_CODE = 'conversant';
const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25';
export const spec = {
code: BIDDER_CODE,
gvlid: GVLID,
aliases: ['cnvr'], // short code
supportedMediaTypes: [BANNER, VIDEO],
/**
* Determines whether or not the given bid request is valid.
*
* @param {BidRequest} bid - The bid params to validate.
* @return {boolean} True if this is a valid bid, and false otherwise.
*/
isBidRequestValid: function(bid) {
if (!bid || !bid.params) {
utils.logWarn(BIDDER_CODE + ': Missing bid parameters');
return false;
}
if (!utils.isStr(bid.params.site_id)) {
utils.logWarn(BIDDER_CODE + ': site_id must be specified as a string');
return false;
}
if (isVideoRequest(bid)) {
const mimes = bid.params.mimes || utils.deepAccess(bid, 'mediaTypes.video.mimes');
if (!mimes) {
// Give a warning but let it pass
utils.logWarn(BIDDER_CODE + ': mimes should be specified for videos');
} else if (!utils.isArray(mimes) || !mimes.every(s => utils.isStr(s))) {
utils.logWarn(BIDDER_CODE + ': mimes must be an array of strings');
return false;
}
}
return true;
},
/**
* Make a server request from the list of BidRequests.
*
* @param {BidRequest[]} validBidRequests - an array of bids
* @param bidderRequest
* @return {ServerRequest} Info describing the request to the server.
*/
buildRequests: function(validBidRequests, bidderRequest) {
const page = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : '';
let siteId = '';
let requestId = '';
let pubcid = null;
let pubcidName = '_pubcid';
let bidurl = URL;
const conversantImps = validBidRequests.map(function(bid) {
const bidfloor = getBidFloor(bid);
siteId = utils.getBidIdParameter('site_id', bid.params) || siteId;
pubcidName = utils.getBidIdParameter('pubcid_name', bid.params) || pubcidName;
requestId = bid.auctionId;
const imp = {
id: bid.bidId,
secure: 1,
bidfloor: bidfloor || 0,
displaymanager: 'Prebid.js',
displaymanagerver: '$prebid.version$'
};
copyOptProperty(bid.params.tag_id, imp, 'tagid');
if (isVideoRequest(bid)) {
const videoData = utils.deepAccess(bid, 'mediaTypes.video') || {};
const format = convertSizes(videoData.playerSize || bid.sizes);
const video = {};
if (format && format[0]) {
copyOptProperty(format[0].w, video, 'w');
copyOptProperty(format[0].h, video, 'h');
}
copyOptProperty(bid.params.position, video, 'pos');
copyOptProperty(bid.params.mimes || videoData.mimes, video, 'mimes');
copyOptProperty(bid.params.maxduration || videoData.maxduration, video, 'maxduration');
copyOptProperty(bid.params.protocols || videoData.protocols, video, 'protocols');
copyOptProperty(bid.params.api || videoData.api, video, 'api');
imp.video = video;
} else {
const bannerData = utils.deepAccess(bid, 'mediaTypes.banner') || {};
const format = convertSizes(bannerData.sizes || bid.sizes);
const banner = {format: format};
copyOptProperty(bid.params.position, banner, 'pos');
imp.banner = banner;
}
if (bid.userId && bid.userId.pubcid) {
pubcid = bid.userId.pubcid;
} else if (bid.crumbs && bid.crumbs.pubcid) {
pubcid = bid.crumbs.pubcid;
}
if (bid.params.white_label_url) {
bidurl = bid.params.white_label_url;
}
return imp;
});
const payload = {
id: requestId,
imp: conversantImps,
site: {
id: siteId,
mobile: document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0,
page: page
},
device: getDevice(),
at: 1
};
let userExt = {};
if (bidderRequest) {
// Add GDPR flag and consent string
if (bidderRequest.gdprConsent) {
userExt.consent = bidderRequest.gdprConsent.consentString;
if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') {
utils.deepSetValue(payload, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0);
}
}
if (bidderRequest.uspConsent) {
utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent);
}
}
if (!pubcid) {
pubcid = readStoredValue(pubcidName);
}
// Add common id if available
if (pubcid) {
userExt.fpc = pubcid;
}
// Add Eids if available
const eids = collectEids(validBidRequests);
if (eids.length > 0) {
userExt.eids = eids;
}
// Only add the user object if it's not empty
if (!utils.isEmpty(userExt)) {
payload.user = {ext: userExt};
}
return {
method: 'POST',
url: bidurl,
data: payload,
};
},
/**
* Unpack the response from the server into a list of bids.
*
* @param {*} serverResponse A successful response from the server.
* @param bidRequest
* @return {Bid[]} An array of bids which were nested inside the server.
*/
interpretResponse: function(serverResponse, bidRequest) {
const bidResponses = [];
const requestMap = {};
serverResponse = serverResponse.body;
if (bidRequest && bidRequest.data && bidRequest.data.imp) {
utils._each(bidRequest.data.imp, imp => requestMap[imp.id] = imp);
}
if (serverResponse && utils.isArray(serverResponse.seatbid)) {
utils._each(serverResponse.seatbid, function(bidList) {
utils._each(bidList.bid, function(conversantBid) {
const responseCPM = parseFloat(conversantBid.price);
if (responseCPM > 0.0 && conversantBid.impid) {
const responseAd = conversantBid.adm || '';
const responseNurl = conversantBid.nurl || '';
const request = requestMap[conversantBid.impid];
const bid = {
requestId: conversantBid.impid,
currency: serverResponse.cur || 'USD',
cpm: responseCPM,
creativeId: conversantBid.crid || '',
ttl: 300,
netRevenue: true
};
bid.meta = {};
if (conversantBid.adomain && conversantBid.adomain.length > 0) {
bid.meta.advertiserDomains = conversantBid.adomain;
}
if (request.video) {
if (responseAd.charAt(0) === '<') {
bid.vastXml = responseAd;
} else {
bid.vastUrl = responseAd;
}
bid.mediaType = 'video';
bid.width = request.video.w;
bid.height = request.video.h;
} else {
bid.ad = responseAd + '<img src="' + responseNurl + '" />';
bid.width = conversantBid.w;
bid.height = conversantBid.h;
}
bidResponses.push(bid);
}
})
});
}
return bidResponses;
},
/**
* Covert bid param types for S2S
* @param {Object} params bid params
* @param {Boolean} isOpenRtb boolean to check openrtb2 protocol
* @return {Object} params bid params
*/
transformBidParams: function(params, isOpenRtb) {
return utils.convertTypes({
'site_id': 'string',
'secure': 'number',
'mobile': 'number'
}, params);
},
/**
* Register User Sync.
*/
getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) {
let params = {};
const syncs = [];
// Attaching GDPR Consent Params in UserSync url
if (gdprConsent) {
params.gdpr = (gdprConsent.gdprApplies) ? 1 : 0;
params.gdpr_consent = encodeURIComponent(gdprConsent.consentString || '');
}
// CCPA
if (uspConsent) {
params.us_privacy = encodeURIComponent(uspConsent);
}
if (responses && responses.ext) {
const pixels = [{urls: responses.ext.fsyncs, type: 'iframe'}, {urls: responses.ext.psyncs, type: 'image'}]
.filter((entry) => {
return entry.urls &&
((entry.type === 'iframe' && syncOptions.iframeEnabled) ||
(entry.type === 'image' && syncOptions.pixelEnabled));
})
.map((entry) => {
return entry.urls.map((endpoint) => {
let urlInfo = utils.parseUrl(endpoint);
utils.mergeDeep(urlInfo.search, params);
if (Object.keys(urlInfo.search).length === 0) {
delete urlInfo.search; // empty search object causes buildUrl to add a trailing ? to the url
}
return {type: entry.type, url: utils.buildUrl(urlInfo)};
})
.reduce((x, y) => x.concat(y), []);
})
.reduce((x, y) => x.concat(y), []);
syncs.push(...pixels);
}
return syncs;
}
};
/**
* Determine do-not-track state
*
* @returns {boolean}
*/
function getDNT() {
return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes';
}
/**
* Return openrtb device object that includes ua, width, and height.
*
* @returns {Device} Openrtb device object
*/
function getDevice() {
const language = navigator.language ? 'language' : 'userLanguage';
return {
h: screen.height,
w: screen.width,
dnt: getDNT() ? 1 : 0,
language: navigator[language].split('-')[0],
make: navigator.vendor ? navigator.vendor : '',
ua: navigator.userAgent
};
}
/**
* Convert arrays of widths and heights to an array of objects with w and h properties.
*
* [[300, 250], [300, 600]] => [{w: 300, h: 250}, {w: 300, h: 600}]
*
* @param {Array.<Array.<number>>} bidSizes - arrays of widths and heights
* @returns {object[]} Array of objects with w and h
*/
function convertSizes(bidSizes) {
let format;
if (Array.isArray(bidSizes)) {
if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') {
format = [{w: bidSizes[0], h: bidSizes[1]}];
} else {
format = utils._map(bidSizes, d => { return {w: d[0], h: d[1]}; });
}
}
return format;
}
/**
* Check if it's a video bid request
*
* @param {BidRequest} bid - Bid request generated from ad slots
* @returns {boolean} True if it's a video bid
*/
function isVideoRequest(bid) {
return bid.mediaType === 'video' || !!utils.deepAccess(bid, 'mediaTypes.video');
}
/**
* Copy property if exists from src to dst
*
* @param {object} src - source object
* @param {object} dst - destination object
* @param {string} dstName - destination property name
*/
function copyOptProperty(src, dst, dstName) {
if (src) {
dst[dstName] = src;
}
}
/**
* Collect IDs from validBidRequests and store them as an extended id array
* @param bidRequests valid bid requests
*/
function collectEids(bidRequests) {
const request = bidRequests[0]; // bidRequests have the same userId object
const eids = [];
if (utils.isArray(request.userIdAsEids) && request.userIdAsEids.length > 0) {
// later following white-list can be converted to block-list if needed
const requiredSourceValues = {
'adserver.org': 1,
'liveramp.com': 1,
'criteo.com': 1,
'id5-sync.com': 1,
'parrable.com': 1,
'liveintent.com': 1
};
request.userIdAsEids.forEach(function(eid) {
if (requiredSourceValues.hasOwnProperty(eid.source)) {
eids.push(eid);
}
});
}
return eids;
}
/**
* Look for a stored value from both cookie and local storage and return the first value found.
* @param key Key for the search
* @return {string} Stored value
*/
function readStoredValue(key) {
let storedValue;
try {
// check cookies first
storedValue = storage.getCookie(key);
if (!storedValue) {
// check expiration time before reading local storage
const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`);
if (storedValueExp === '' || (storedValueExp && (new Date(storedValueExp)).getTime() - Date.now() > 0)) {
storedValue = storage.getDataFromLocalStorage(key);
storedValue = storedValue ? decodeURIComponent(storedValue) : storedValue;
}
}
// deserialize JSON if needed
if (utils.isStr(storedValue) && storedValue.charAt(0) === '{') {
storedValue = JSON.parse(storedValue);
}
} catch (e) {
utils.logError(e);
}
return storedValue;
}
/**
* Get the floor price from bid.params for backward compatibility.
* If not found, then check floor module.
* @param bid A valid bid object
* @returns {*|number} floor price
*/
function getBidFloor(bid) {
let floor = utils.getBidIdParameter('bidfloor', bid.params);
if (!floor && utils.isFn(bid.getFloor)) {
const floorObj = bid.getFloor({
currency: 'USD',
mediaType: '*',
size: '*'
});
if (utils.isPlainObject(floorObj) && !isNaN(floorObj.floor) && floorObj.currency === 'USD') {
floor = floorObj.floor;
}
}
return floor
}
registerBidder(spec);