modules/spotxBidAdapter.js
import {
logError,
deepAccess,
isArray,
getDNT,
deepSetValue,
isEmpty,
_each,
logMessage,
logWarn,
isBoolean,
isNumber,
isPlainObject,
isFn,
setScriptAttributes,
getBidIdParameter
} from '../src/utils.js';
import { config } from '../src/config.js';
import { Renderer } from '../src/Renderer.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { VIDEO } from '../src/mediaTypes.js';
import { loadExternalScript } from '../src/adloader.js';
/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
* @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
* @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest
*/
const BIDDER_CODE = 'spotx';
const URL = 'https://search.spotxchange.com/openrtb/2.3/dados/';
const ORTB_VERSION = '2.3';
export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15', '20', '22', '35', '43', '46', '48', '55', '57', '61', '62', '66', '70', '80', '83', '85', '86', '89', '93', '108', '122', '124', '125', '126', '131', '134', '135', '136', '143', '144', '147', '149', '153', '154', '159', '161', '162', '165', '167', '171', '178', '184', '188', '192', '195', '196', '202', '209', '211', '218', '221', '228', '229', '230', '236', '239', '241', '253', '255', '259', '266', '271', '272', '274', '286', '291', '294', '303', '308', '310', '311', '313', '314', '316', '317', '322', '323', '327', '336', '338', '340', '348', '350', '358', '359', '363', '367', '370', '371', '384', '385', '389', '393', '394', '397', '398', '407', '414', '415', '424', '429', '430', '432', '436', '438', '440', '442', '443', '445', '448', '449', '453', '459', '479', '482', '486', '491', '492', '494', '495', '503', '505', '510', '522', '523', '528', '537', '540', '550', '559', '560', '568', '571', '574', '575', '576', '584', '585', '587', '588', '590', '591', '592', '595', '609', '621', '624', '723', '725', '733', '737', '776', '780', '782', '787', '797', '798', '802', '803', '814', '817', '820', '821', '827', '829', '839', '853', '864', '867', '874', '899', '904', '922', '926', '931', '932', '933', '938', '955', '973', '976', '979', '981', '985', '987', '991', '1003', '1024', '1025', '1027', '1028', '1029', '1033', '1034', '1040', '1047', '1048', '1051', '1052', '1053', '1054', '1062', '1063', '1067', '1072', '1085', '1092', '1095', '1097', '1099', '1100', '1107', '1126', '1127', '1143', '1149', '1152', '1162', '1166', '1167', '1170', '1171', '1172', '1188', '1192', '1199', '1201', '1204', '1205', '1211', '1212', '1215', '1220', '1225', '1226', '1227', '1230', '1232', '1236', '1241', '1248', '1250', '1252', '1268', '1275', '1276', '1284', '1286', '1298', '1301', '1307', '1312', '1313', '1317', '1329', '1336', '1344', '1345', '1356', '1362', '1365', '1375', '1403', '1409', '1411', '1415', '1416', '1419', '1423', '1440', '1442', '1449', '1451', '1455', '1456', '1468', '1496', '1503', '1509', '1512', '1514', '1517', '1520', '1525', '1540', '1547', '1548', '1555', '1558', '1570', '1575', '1577', '1579', '1583', '1584', '1591', '1598', '1603', '1608', '1613', '1616', '1626', '1631', '1633', '1638', '1642', '1648', '1651', '1652', '1653', '1660', '1665', '1667', '1669', '1671', '1674', '1677', '1678', '1682', '1684', '1697', '1703', '1705', '1716', '1720', '1721', '1722', '1725', '1732', '1733', '1735', '1739', '1741', '1745', '1750', '1753', '1760', '1765', '1769', '1776', '1780', '1782', '1786', '1791', '1794', '1799', '1800', '1801', '1810', '1827', '1831', '1832', '1834', '1837', '1840', '1843', '1844', '1845', '1858', '1859', '1863', '1866', '1870', '1872', '1875', '1878', '1880', '1882', '1883', '1889', '1892', '1896', '1898', '1899', '1902', '1905', '1911', '1922', '1928', '1929', '1934', '1942', '1943', '1944', '1945', '1958', '1960', '1962', '1963', '1964', '1967', '1968', '1978', '1985', '1986', '1987', '1998', '2003', '2007', '2012', '2013', '2027', '2035', '2038', '2039', '2044', '2047', '2052', '2056', '2059', '2062', '2064', '2068', '2070', '2072', '2078', '2079', '2084', '2088', '2090', '2095', '2100', '2103', '2107', '2109', '2113', '2115', '2121', '2127', '2130', '2133', '2137', '2140', '2141', '2145', '2147', '2150', '2156', '2166', '2170', '2171', '2176', '2177', '2179', '2183', '2186', '2192', '2198', '2202', '2205', '2214', '2216', '2219', '2220', '2222', '2223', '2224', '2225', '2227', '2228', '2234', '2238', '2247', '2251', '2253', '2262', '2264', '2271', '2276', '2278', '2279', '2282', '2290', '2292', '2295', '2299', '2305', '2306', '2310', '2311', '2312', '2315', '2320', '2325', '2328', '2331', '2334', '2335', '2336', '2337', '2343', '2346', '2354', '2357', '2358', '2359', '2366', '2370', '2373', '2376', '2377', '2380', '2382', '2387', '2389', '2392', '2394', '2400', '2403', '2405', '2406', '2407', '2410', '2411', '2413', '2414', '2415', '2416', '2418', '2422', '2425', '2427', '2435', '2437', '2440', '2441', '2447', '2453', '2459', '2461', '2462', '2464', '2467', '2468', '2472', '2477', '2481', '2484', '2486', '2492', '2493', '2496', '2497', '2498', '2499', '2504', '2506', '2510', '2511', '2512', '2517', '2526', '2527', '2531', '2532', '2534', '2542', '2544', '2552', '2555', '2559', '2563', '2564', '2567', '2568', '2569', '2571', '2572', '2573', '2575', '2577', '2579', '2583', '2584', '2586', '2589', '2595', '2596', '2597', '2601', '2604', '2605', '2609', '2610', '2612', '2614', '2621', '2622', '2624', '2628', '2629', '2632', '2634', '2636', '2639', '2643', '2645', '2646', '2647', '2649', '2650', '2651', '2652', '2656', '2657', '2658', '2660', '2661', '2662', '2663', '2664', '2669', '2670', '2673', '2676', '2677', '2678', '2681', '2682', '2684', '2685', '2686', '2689', '2690', '2691', '2695', '2698', '2699', '2702', '2704', '2705', '2706', '2707', '2709', '2710', '2713', '2714', '2727', '2729', '2739', '2758', '2765', '2766', '2767', '2768', '2770', '2771', '2772', '2776', '2777', '2778', '2779', '2780', '2783', '2784', '2786', '2787', '2791', '2792', '2793', '2797', '2798', '2801', '2802', '2803', '2805', '2808', '2809', '2810', '2811', '2812', '2813', '2814', '2817', '2818', '2824', '2826', '2827', '2829', '2830', '2831', '2832', '2834', '2836', '2838', '2840', '2842', '2843', '2844', '2850', '2851', '2852', '2854', '2858', '2860', '2862', '2864', '2865', '2866', '2867', '2868', '2869', '2871'] };
export const spec = {
code: BIDDER_CODE,
gvlid: 165,
supportedMediaTypes: [VIDEO],
/**
* Determines whether or not the given bid request is valid.
* From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid).
*
* @param {object} bid The bid to validate.
* @return {boolean} True if this is a valid bid, and false otherwise.
*/
isBidRequestValid: function(bid) {
if (bid && typeof bid.params !== 'object') {
logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.');
return false;
}
if (!deepAccess(bid, 'mediaTypes.video')) {
logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.');
return false;
}
const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
if (!playerSize || !isArray(playerSize)) {
logError(BIDDER_CODE + ': mediaTypes.video.playerSize is not defined in the bidder settings.');
return false;
}
if (!getBidIdParameter('channel_id', bid.params)) {
logError(BIDDER_CODE + ': channel_id is not present in bidder params');
return false;
}
if (deepAccess(bid, 'mediaTypes.video.context') == 'outstream' || deepAccess(bid, 'params.ad_unit') == 'outstream') {
if (!getBidIdParameter('outstream_function', bid.params)) {
if (!getBidIdParameter('outstream_options', bid.params)) {
logError(BIDDER_CODE + ': please define outstream_options parameter or override the default SpotX outstream rendering by defining your own Outstream function using field outstream_function.');
return false;
}
if (!getBidIdParameter('slot', bid.params.outstream_options)) {
logError(BIDDER_CODE + ': please define parameter slot in outstream_options object in the configuration.');
return false;
}
}
}
return true;
},
/**
* Make a server request from the list of BidRequests.
* from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test.
*
* @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
* @param {object} bidderRequest - The master bidRequest object.
* @return {ServerRequest} Info describing the request to the server.
*/
buildRequests: function(bidRequests, bidderRequest) {
// TODO: does the fallback make sense here?
const referer = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation;
const isPageSecure = !!referer.match(/^https:/);
const siteId = '';
const spotxRequests = bidRequests.map(function(bid) {
let page;
if (getBidIdParameter('page', bid.params)) {
page = getBidIdParameter('page', bid.params);
} else {
page = referer;
}
const channelId = getBidIdParameter('channel_id', bid.params);
let pubcid = null;
const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
const contentWidth = playerSize[0][0];
const contentHeight = playerSize[0][1];
const secure = isPageSecure || (getBidIdParameter('secure', bid.params) ? 1 : 0);
const ext = {
sdk_name: 'Prebid 1+',
versionOrtb: ORTB_VERSION
};
if (getBidIdParameter('hide_skin', bid.params) != '') {
ext.hide_skin = +!!getBidIdParameter('hide_skin', bid.params);
}
if (getBidIdParameter('ad_volume', bid.params) != '') {
ext.ad_volume = getBidIdParameter('ad_volume', bid.params);
}
if (getBidIdParameter('ad_unit', bid.params) != '') {
ext.ad_unit = getBidIdParameter('ad_unit', bid.params);
}
if (getBidIdParameter('outstream_options', bid.params) != '') {
ext.outstream_options = getBidIdParameter('outstream_options', bid.params);
}
if (getBidIdParameter('outstream_function', bid.params) != '') {
ext.outstream_function = getBidIdParameter('outstream_function', bid.params);
}
if (getBidIdParameter('custom', bid.params) != '') {
ext.custom = getBidIdParameter('custom', bid.params);
}
if (getBidIdParameter('pre_market_bids', bid.params) != '' && isArray(getBidIdParameter('pre_market_bids', bid.params))) {
const preMarketBids = getBidIdParameter('pre_market_bids', bid.params);
ext.pre_market_bids = [];
for (let i in preMarketBids) {
const preMarketBid = preMarketBids[i];
let vastStr = '';
if (preMarketBid['vast_url']) {
vastStr = '<?xml version="1.0" encoding="utf-8"?><VAST version="2.0"><Ad><Wrapper><VASTAdTagURI>' + preMarketBid['vast_url'] + '</VASTAdTagURI></Wrapper></Ad></VAST>';
} else if (preMarketBid['vast_string']) {
vastStr = preMarketBid['vast_string'];
}
ext.pre_market_bids.push({
id: preMarketBid['deal_id'],
seatbid: [{
bid: [{
impid: Date.now(),
dealid: preMarketBid['deal_id'],
price: preMarketBid['price'],
adm: vastStr
}]
}],
cur: preMarketBid['currency'],
ext: {
event_log: [{}]
}
});
}
}
const mimes = getBidIdParameter('mimes', bid.params) || deepAccess(bid, 'mediaTypes.video.mimes') || ['application/javascript', 'video/mp4', 'video/webm'];
const spotxReq = {
id: bid.bidId,
secure: secure,
video: {
w: contentWidth,
h: contentHeight,
ext: ext,
mimes: mimes
}
};
if (isFn(bid.getFloor)) {
let floorInfo = bid.getFloor({
currency: 'USD',
mediaType: 'video',
size: '*'
});
if (floorInfo.currency === 'USD') {
spotxReq.bidfloor = floorInfo.floor;
}
} else if (getBidIdParameter('price_floor', bid.params) != '') {
spotxReq.bidfloor = getBidIdParameter('price_floor', bid.params);
}
const startdelay = getBidIdParameter('start_delay', bid.params) || deepAccess(bid, 'mediaTypes.video.startdelay');
if (startdelay) {
spotxReq.video.startdelay = 0 + Boolean(startdelay);
}
const minduration = getBidIdParameter('min_duration', bid.params) || deepAccess(bid, 'mediaTypes.video.minduration');
if (minduration) {
spotxReq.video.minduration = minduration;
}
const maxduration = getBidIdParameter('max_duration', bid.params) || deepAccess(bid, 'mediaTypes.video.maxduration');
if (maxduration) {
spotxReq.video.maxduration = maxduration;
}
const placement = getBidIdParameter('placement_type', bid.params) || deepAccess(bid, 'mediaTypes.video.placement');
if (placement) {
spotxReq.video.ext.placement = placement;
}
const position = getBidIdParameter('position', bid.params) || deepAccess(bid, 'mediaTypes.video.pos');
if (position) {
spotxReq.video.ext.pos = position;
}
if (bid.crumbs && bid.crumbs.pubcid) {
pubcid = bid.crumbs.pubcid;
}
const language = navigator.language ? 'language' : 'userLanguage';
const device = {
h: screen.height,
w: screen.width,
dnt: getDNT() ? 1 : 0,
language: navigator[language].split('-')[0],
make: navigator.vendor ? navigator.vendor : '',
ua: navigator.userAgent
};
const requestPayload = {
id: channelId,
imp: spotxReq,
site: {
id: siteId,
page: page,
content: 'content',
},
device: device,
ext: {
wrap_response: 1
}
};
// If the publisher asks to ignore the bidder cache key we need to return the full vast xml
// so that it can be cached on the publishes specified server.
if (!!config.getConfig('cache') && !!config.getConfig('cache.url') && (config.getConfig('cache.ignoreBidderCacheKey') === true)) {
requestPayload['ext']['wrap_response'] = 0;
}
if (getBidIdParameter('number_of_ads', bid.params)) {
requestPayload['ext']['number_of_ads'] = getBidIdParameter('number_of_ads', bid.params);
}
const userExt = {};
if (getBidIdParameter('spotx_all_google_consent', bid.params) == 1) {
userExt['consented_providers_settings'] = GOOGLE_CONSENT;
}
// Add GDPR flag and consent string
if (bidderRequest && bidderRequest.gdprConsent) {
userExt.consent = bidderRequest.gdprConsent.consentString;
if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') {
deepSetValue(requestPayload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0));
}
}
if (bidderRequest && bidderRequest.uspConsent) {
deepSetValue(requestPayload, 'regs.ext.us_privacy', bidderRequest.uspConsent);
}
if (bid.userIdAsEids) {
userExt.eids = bid.userIdAsEids;
userExt.eids.forEach(eid => {
if (eid.source === 'uidapi.com') {
eid.uids.forEach(uid => {
uid.ext = uid.ext || {};
uid.ext.rtiPartner = 'UID2'
});
}
});
}
// Add common id if available
if (pubcid) {
userExt.fpc = pubcid;
}
// Add schain object if it is present
if (bid && bid.schain) {
requestPayload['source'] = {
ext: {
schain: bid.schain
}
};
}
// Only add the user object if it's not empty
if (!isEmpty(userExt)) {
requestPayload.user = { ext: userExt };
}
const urlQueryParams = 'src_sys=prebid';
return {
method: 'POST',
url: URL + channelId + '?' + urlQueryParams,
data: requestPayload,
bidRequest: bidderRequest
};
});
return spotxRequests;
},
/**
* 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 bidResponses = [];
const serverResponseBody = serverResponse.body;
if (serverResponseBody && isArray(serverResponseBody.seatbid)) {
_each(serverResponseBody.seatbid, function(bids) {
_each(bids.bid, function(spotxBid) {
let currentBidRequest = {};
for (let i in bidderRequest.bidRequest.bids) {
if (spotxBid.impid == bidderRequest.bidRequest.bids[i].bidId) {
currentBidRequest = bidderRequest.bidRequest.bids[i];
}
}
/**
* Make sure currency and price are the right ones
* TODO: what about the pre_market_bid partners sizes?
*/
_each(currentBidRequest.params.pre_market_bids, function(pmb) {
if (pmb.deal_id == spotxBid.id) {
spotxBid.price = pmb.price;
serverResponseBody.cur = pmb.currency;
}
});
const bid = {
requestId: currentBidRequest.bidId,
currency: serverResponseBody.cur || 'USD',
cpm: spotxBid.price,
creativeId: spotxBid.crid || '',
dealId: spotxBid.dealid || '',
ttl: 360,
netRevenue: true,
channel_id: serverResponseBody.id,
mediaType: VIDEO,
width: spotxBid.w,
height: spotxBid.h
};
if (!!config.getConfig('cache') && !!config.getConfig('cache.url') && (config.getConfig('cache.ignoreBidderCacheKey') === true)) {
bid.vastXml = spotxBid.adm;
} else {
bid.cache_key = spotxBid.ext.cache_key;
bid.vastUrl = 'https://search.spotxchange.com/ad/vast.html?key=' + spotxBid.ext.cache_key;
bid.videoCacheKey = spotxBid.ext.cache_key;
}
bid.meta = bid.meta || {};
if (spotxBid && spotxBid.adomain && spotxBid.adomain.length > 0) {
bid.meta.advertiserDomains = spotxBid.adomain;
}
const context1 = deepAccess(currentBidRequest, 'mediaTypes.video.context');
const context2 = deepAccess(currentBidRequest, 'params.ad_unit');
if (context1 == 'outstream' || context2 == 'outstream') {
const playersize = deepAccess(currentBidRequest, 'mediaTypes.video.playerSize');
const renderer = Renderer.install({
id: 0,
renderNow: true,
url: '/',
config: {
adText: 'SpotX Outstream Video Ad via Prebid.js',
player_width: playersize[0][0],
player_height: playersize[0][1],
content_page_url: deepAccess(bidderRequest, 'data.site.page'),
ad_mute: +!!deepAccess(currentBidRequest, 'params.ad_mute'),
hide_skin: +!!deepAccess(currentBidRequest, 'params.hide_skin'),
outstream_options: deepAccess(currentBidRequest, 'params.outstream_options'),
outstream_function: deepAccess(currentBidRequest, 'params.outstream_function')
}
});
try {
renderer.setRender(outstreamRender);
renderer.setEventHandlers({
impression: function impression() {
return logMessage('SpotX outstream video impression event');
},
loaded: function loaded() {
return logMessage('SpotX outstream video loaded event');
},
ended: function ended() {
logMessage('SpotX outstream renderer video event');
}
});
} catch (err) {
logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err);
}
bid.renderer = renderer;
}
bidResponses.push(bid);
})
});
}
return bidResponses;
}
}
function createOutstreamScript(bid) {
const script = window.document.createElement('script');
let dataSpotXParams = createScriptAttributeMap(bid);
script.type = 'text/javascript';
script.src = 'https://js.spotx.tv/easi/v1/' + bid.channel_id + '.js';
setScriptAttributes(script, dataSpotXParams);
return script;
}
function outstreamRender(bid) {
if (bid.renderer.config.outstream_function != null && typeof bid.renderer.config.outstream_function === 'function') {
const script = createOutstreamScript(bid);
bid.renderer.config.outstream_function(bid, script);
} else {
try {
const inIframe = getBidIdParameter('in_iframe', bid.renderer.config.outstream_options);
const easiUrl = 'https://js.spotx.tv/easi/v1/' + bid.channel_id + '.js';
let attributes = createScriptAttributeMap(bid);
if (inIframe && window.document.getElementById(inIframe).nodeName == 'IFRAME') {
const rawframe = window.document.getElementById(inIframe);
let framedoc = rawframe.contentDocument;
if (!framedoc && rawframe.contentWindow) {
framedoc = rawframe.contentWindow.document;
}
loadExternalScript(easiUrl, BIDDER_CODE, undefined, framedoc, attributes);
} else {
loadExternalScript(easiUrl, BIDDER_CODE, undefined, undefined, attributes);
}
} catch (err) {
logError('[SPOTX][renderer] Error:' + err.message);
}
}
}
function createScriptAttributeMap(bid) {
const slot = getBidIdParameter('slot', bid.renderer.config.outstream_options);
logMessage('[SPOTX][renderer] Handle SpotX outstream renderer');
let dataSpotXParams = {};
dataSpotXParams['data-spotx_channel_id'] = '' + bid.channel_id;
dataSpotXParams['data-spotx_vast_url'] = '' + bid.vastUrl;
dataSpotXParams['data-spotx_content_page_url'] = bid.renderer.config.content_page_url;
dataSpotXParams['data-spotx_ad_unit'] = 'incontent';
logMessage('[SPOTX][renderer] Default behavior');
if (getBidIdParameter('ad_mute', bid.renderer.config.outstream_options)) {
dataSpotXParams['data-spotx_ad_mute'] = '1';
}
dataSpotXParams['data-spotx_collapse'] = '0';
dataSpotXParams['data-spotx_autoplay'] = '1';
dataSpotXParams['data-spotx_blocked_autoplay_override_mode'] = '1';
dataSpotXParams['data-spotx_video_slot_can_autoplay'] = '1';
dataSpotXParams['data-spotx_content_container_id'] = slot;
const playersizeAutoAdapt = getBidIdParameter('playersize_auto_adapt', bid.renderer.config.outstream_options);
if (playersizeAutoAdapt && isBoolean(playersizeAutoAdapt) && playersizeAutoAdapt === true) {
const ratio = bid.width && isNumber(bid.width) && bid.height && isNumber(bid.height) ? bid.width / bid.height : 4 / 3;
const slotClientWidth = window.document.getElementById(slot).clientWidth;
let playerWidth = bid.renderer.config.player_width;
let playerHeight = bid.renderer.config.player_height;
let contentWidth = 0;
let contentHeight = 0;
if (slotClientWidth < playerWidth) {
playerWidth = slotClientWidth;
playerHeight = playerWidth / ratio;
}
if (ratio <= 1) {
contentWidth = Math.round(playerHeight * ratio);
contentHeight = playerHeight;
} else {
contentWidth = playerWidth;
contentHeight = Math.round(playerWidth / ratio);
}
dataSpotXParams['data-spotx_content_width'] = '' + contentWidth;
dataSpotXParams['data-spotx_content_height'] = '' + contentHeight;
}
const customOverride = getBidIdParameter('custom_override', bid.renderer.config.outstream_options);
if (customOverride && isPlainObject(customOverride)) {
logMessage('[SPOTX][renderer] Custom behavior.');
for (let name in customOverride) {
if (customOverride.hasOwnProperty(name)) {
if (name === 'channel_id' || name === 'vast_url' || name === 'content_page_url' || name === 'ad_unit') {
logWarn('[SPOTX][renderer] Custom behavior: following option cannot be overridden: ' + name);
} else {
dataSpotXParams['data-spotx_' + name] = customOverride[name];
}
}
}
}
return dataSpotXParams;
}
registerBidder(spec);