mk9-prebid
Version:
Header Bidding Management Library
436 lines (384 loc) • 12.5 kB
JavaScript
import * as utils from '../src/utils.js';
import { ajax } from '../src/ajax.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER } from '../src/mediaTypes.js';
import strIncludes from 'core-js-pure/features/string/includes.js';
const BIDDER_CODE = 'sspBC';
const BIDDER_URL = 'https://ssp.wp.pl/bidder/';
const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync';
const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify';
const TMAX = 450;
const BIDDER_VERSION = '5.0';
const W = window;
const { navigator } = W;
const oneCodeDetection = {};
const adSizesCalled = {};
var consentApiVersion;
/**
* Get bid parameters for notification
* @param {*} bidData - bid (bidWon), or array of bids (timeout)
*/
const getNotificationPayload = bidData => {
if (bidData) {
const bids = utils.isArray(bidData) ? bidData : [bidData];
if (bids.length > 0) {
const result = {
requestId: undefined,
siteId: [],
adUnit: [],
slotId: [],
}
bids.forEach(bid => {
let params = utils.isArray(bid.params) ? bid.params[0] : bid.params;
params = params || {};
// check for stored detection
if (oneCodeDetection[bid.requestId]) {
params.siteId = oneCodeDetection[bid.requestId][0];
params.id = oneCodeDetection[bid.requestId][1];
}
if (params.siteId) {
result.siteId.push(params.siteId);
}
if (params.id) {
result.slotId.push(params.id);
}
if (bid.cpm) {
const meta = bid.meta || {};
result.cpm = bid.cpm;
result.creativeId = bid.creativeId;
result.adomain = meta.advertiserDomains && meta.advertiserDomains[0];
result.networkName = meta.networkName;
}
result.adUnit.push(bid.adUnitCode)
result.requestId = bid.auctionId || result.requestId;
result.timeout = bid.timeout || result.timeout;
})
return result;
}
}
}
const cookieSupport = () => {
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent);
const useCookies = navigator.cookieEnabled || !!document.cookie.length;
return !isSafari && useCookies;
};
const applyClientHints = ortbRequest => {
const { connection = {}, deviceMemory, userAgentData = {} } = navigator;
const viewport = W.visualViewport || false;
const segments = [];
const hints = {
'CH-Ect': connection.effectiveType,
'CH-Rtt': connection.rtt,
'CH-SaveData': connection.saveData,
'CH-Downlink': connection.downlink,
'CH-DeviceMemory': deviceMemory,
'CH-Dpr': W.devicePixelRatio,
'CH-ViewportWidth': viewport.width,
'CH-BrowserBrands': JSON.stringify(userAgentData.brands),
'CH-isMobile': userAgentData.mobile,
};
Object.keys(hints).forEach(key => {
const hint = hints[key];
if (hint) {
segments.push({
name: key,
value: hint.toString(),
});
}
});
const data = [
{
id: '12',
name: 'NetInfo',
segment: segments,
}];
ortbRequest.user = Object.assign(ortbRequest.user, { data });
};
/**
* Add GDPR data to oRTB request
* Store conset API version (will be required by user sync)
*/
const applyGdpr = (bidderRequest, ortbRequest) => {
const { gdprConsent } = bidderRequest;
if (gdprConsent) {
const { apiVersion, gdprApplies, consentString } = gdprConsent;
consentApiVersion = apiVersion;
ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': gdprApplies ? 1 : 0 });
ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': consentString });
}
}
/**
* Get value for first occurence of key within the collection
*/
const setOnAny = (collection, key) => collection.reduce((prev, next) => prev || utils.deepAccess(next, key), false);
/**
* Send payload to notification endpoint
*/
const sendNotification = payload => {
ajax(NOTIFY_URL, null, JSON.stringify(payload), {
withCredentials: false,
method: 'POST',
crossOrigin: true
});
}
/**
* @param {object} slot Ad Unit Params by Prebid
* @returns {object} Banner by OpenRTB 2.5 §3.2.6
*/
const mapBanner = slot => {
if (slot.mediaType === 'banner' ||
utils.deepAccess(slot, 'mediaTypes.banner') ||
(!slot.mediaType && !slot.mediaTypes)) {
const format = slot.sizes.map(size => ({
w: size[0],
h: size[1],
}));
return {
format,
id: slot.bidId,
};
}
}
const mapImpression = slot => {
const { adUnitCode, bidId, params = {}, ortb2Imp = {} } = slot;
const { id, siteId } = params;
const { ext = {} } = ortb2Imp;
/*
check max size for this imp, and check/store number this size was called (for current view)
send this info as ext.pbsize
*/
const slotSize = slot.sizes.length ? slot.sizes.reduce((prev, next) => prev[0] * prev[1] <= next[0] * next[1] ? next : prev).join('x') : '1x1';
adSizesCalled[slotSize] = adSizesCalled[slotSize] ? adSizesCalled[slotSize] + 1 : 1;
ext.data = Object.assign({ pbsize: `${slotSize}_${adSizesCalled[slotSize]}` }, ext.data);
const imp = {
id: id && siteId ? id : 'bidid-' + bidId,
banner: mapBanner(slot),
// native: mapNative(slot),
tagid: adUnitCode,
ext,
};
// Check floorprices for this imp
if (typeof slot.getFloor === 'function') {
let bannerFloor = 0;
// sspBC adapter accepts only floor per imp - check for maximum value for requested ad types and sizes
if (slot.sizes.length) {
bannerFloor = slot.sizes.reduce((prev, next) => {
const currentFloor = slot.getFloor({ mediaType: 'banner', size: next }).floor;
return prev > currentFloor ? prev : currentFloor;
}, 0);
}
imp.bidfloor = bannerFloor;
}
return imp;
}
const renderCreative = (site, auctionId, bid, seat, request) => {
let gam;
const mcad = {
id: auctionId,
seat,
seatbid: [{
bid: [bid],
}],
};
const mcbase = btoa(encodeURI(JSON.stringify(mcad)));
if (bid.adm) {
// parse adm for gam config
try {
gam = JSON.parse(bid.adm).gam;
if (!gam || !Object.keys(gam).length) {
gam = undefined;
} else {
gam.namedSizes = ['fluid'];
gam.div = 'div-gpt-ad-x01';
gam.targeting = Object.assign(gam.targeting || {}, {
OAS_retarg: '0',
PREBID_ON: '1',
emptygaf: '0',
});
}
if (gam && !gam.targeting) {
gam.targeting = {};
}
} catch (err) {
utils.logWarn('Could not parse adm data', bid.adm);
}
}
let adcode = `<head>
<title></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
background-color: transparent;
margin: 0;
padding: 0;
}
</style>
<script>
window.rekid = ${site.id};
window.slot = ${parseInt(site.slot, 10)};
window.wp_sn = "${site.sn}";
window.mcad = JSON.parse(decodeURI(atob("${mcbase}")));
window.gdpr = ${JSON.stringify(request.gdprConsent)};
window.page = "${site.page}";
window.ref = "${site.ref}";
`;
if (gam) {
adcode += `window.gam = ${JSON.stringify(gam)};`;
}
adcode += `</script>
</head>
<body>
<div id="c"></div>
<script id="wpjslib" crossorigin src="//std.wpcdn.pl/wpjslib/wpjslib-inline.js" async defer></script>
</body>
</html>`;
return adcode;
}
const spec = {
code: BIDDER_CODE,
aliases: [],
supportedMediaTypes: [BANNER],
isBidRequestValid(bid) {
// as per OneCode integration, bids without params are valid
return true;
},
buildRequests(validBidRequests, bidderRequest) {
if ((!validBidRequests) || (validBidRequests.length < 1)) {
return false;
}
const siteId = setOnAny(validBidRequests, 'params.siteId');
const publisherId = setOnAny(validBidRequests, 'params.publisherId');
const page = setOnAny(validBidRequests, 'params.page') || bidderRequest.refererInfo.referer;
const domain = setOnAny(validBidRequests, 'params.domain') || utils.parseUrl(page).hostname;
const tmax = setOnAny(validBidRequests, 'params.tmax') ? parseInt(setOnAny(validBidRequests, 'params.tmax'), 10) : TMAX;
const pbver = '$prebid.version$';
const testMode = setOnAny(validBidRequests, 'params.test') ? 1 : undefined;
let ref;
try {
if (W.self === W.top && document.referrer) { ref = document.referrer; }
} catch (e) {
}
const payload = {
id: bidderRequest.auctionId,
site: {
id: siteId,
publisher: publisherId ? { id: publisherId } : undefined,
page,
domain,
ref
},
imp: validBidRequests.map(slot => mapImpression(slot)),
tmax,
user: {},
regs: {},
test: testMode,
};
applyGdpr(bidderRequest, payload);
applyClientHints(payload);
return {
method: 'POST',
url: `${BIDDER_URL}?cs=${cookieSupport()}&bdver=${BIDDER_VERSION}&pbver=${pbver}&inver=0`,
data: JSON.stringify(payload),
bidderRequest,
};
},
interpretResponse(serverResponse, request) {
const { bidderRequest } = request;
const response = serverResponse.body;
const bids = [];
const site = JSON.parse(request.data).site; // get page and referer data from request
site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn)
let seat;
if (response.seatbid !== undefined) {
/*
Match response to request, by comparing bid id's
'bidid-' prefix indicates oneCode (parameterless) request and response
*/
response.seatbid.forEach(seatbid => {
seat = seatbid.seat;
seatbid.bid.forEach(serverBid => {
// get data from bid response
const { adomain, crid = `mcad_${bidderRequest.auctionId}_${site.slot}`, impid, exp = 300, ext, price, w, h } = serverBid;
const bidRequest = bidderRequest.bids.filter(b => {
const { bidId, params = {} } = b;
const { id, siteId } = params;
const currentBidId = id && siteId ? id : 'bidid-' + bidId;
return currentBidId === impid;
})[0];
// get data from linked bidRequest
const { bidId, params } = bidRequest || {};
// get slot id for current bid
site.slot = params && params.id;
if (ext) {
/*
bid response might include ext object containing siteId / slotId, as detected by OneCode
update site / slot data in this case
*/
const { siteid, slotid } = ext;
site.id = siteid || site.id;
site.slot = slotid || site.slot;
}
if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) {
// found a matching request; add this bid
// store site data for future notification
oneCodeDetection[bidId] = [site.id, site.slot];
const bid = {
requestId: bidId,
creativeId: crid,
cpm: price,
currency: response.cur,
ttl: exp,
width: w,
height: h,
bidderCode: BIDDER_CODE,
mediaType: 'banner',
meta: {
advertiserDomains: adomain,
networkName: seat,
},
netRevenue: true,
ad: renderCreative(site, response.id, serverBid, seat, bidderRequest),
};
if (bid.cpm > 0) {
bids.push(bid);
}
} else {
utils.logWarn('Discarding response - no matching request / site id', serverBid.impid);
}
});
});
}
return bids;
},
getUserSyncs(syncOptions) {
if (syncOptions.iframeEnabled && consentApiVersion != 1) {
return [{
type: 'iframe',
url: `${SYNC_URL}?tcf=${consentApiVersion}`,
}];
} else {
utils.logWarn('sspBC adapter requires iframe based user sync.');
}
},
onTimeout(timeoutData) {
const payload = getNotificationPayload(timeoutData);
if (payload) {
payload.event = 'timeout';
sendNotification(payload);
return payload;
}
},
onBidWon(bid) {
const payload = getNotificationPayload(bid);
if (payload) {
payload.event = 'bidWon';
sendNotification(payload);
return payload;
}
},
};
registerBidder(spec);
export {
spec,
};