modules/tappxBidAdapter.js
'use strict';
import { logWarn, deepAccess, isFn, isPlainObject, getDNT, isBoolean, isNumber, isStr, isArray } from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { config } from '../src/config.js';
import { Renderer } from '../src/Renderer.js';
import { parseDomain } from '../src/refererDetection.js';
import { getGlobal } from '../src/prebidGlobal.js';
/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
*/
const BIDDER_CODE = 'tappx';
const GVLID_CODE = 628;
const TTL = 360;
const CUR = 'USD';
const TAPPX_BIDDER_VERSION = '0.1.3';
const TYPE_CNN = 'prebidjs';
const LOG_PREFIX = '[TAPPX]: ';
const VIDEO_SUPPORT = ['instream', 'outstream'];
const DATA_TYPES = {
'NUMBER': 'number',
'STRING': 'string',
'BOOLEAN': 'boolean',
'ARRAY': 'array',
'OBJECT': 'object'
};
const VIDEO_CUSTOM_PARAMS = {
'minduration': DATA_TYPES.NUMBER,
'maxduration': DATA_TYPES.NUMBER,
'startdelay': DATA_TYPES.NUMBER,
'playbackmethod': DATA_TYPES.ARRAY,
'api': DATA_TYPES.ARRAY,
'protocols': DATA_TYPES.ARRAY,
'w': DATA_TYPES.NUMBER,
'h': DATA_TYPES.NUMBER,
'battr': DATA_TYPES.ARRAY,
'linearity': DATA_TYPES.NUMBER,
'plcmt': DATA_TYPES.NUMBER,
'minbitrate': DATA_TYPES.NUMBER,
'maxbitrate': DATA_TYPES.NUMBER,
'skip': DATA_TYPES.NUMBER
}
var hostDomain;
export const spec = {
code: BIDDER_CODE,
gvlid: GVLID_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) {
// bid.params.host
if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { // New endpoint
if ((new RegExp(`^(zz.*)\\.*$`, 'i')).test(bid.params.host)) return validBasic(bid)
else return validBasic(bid) && validMediaType(bid)
} else { // This is backward compatible feature. It will be remove in the future
if ((new RegExp(`^(ZZ.*)\\.*$`, 'i')).test(bid.params.endpoint)) return validBasic(bid)
else return validBasic(bid) && validMediaType(bid)
}
},
/**
* Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test.
* Make a server request from the list of BidRequests.
*
* @param {*} validBidRequests
* @param {*} bidderRequest
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: function(validBidRequests, bidderRequest) {
let requests = [];
validBidRequests.forEach(oneValidRequest => {
requests.push(buildOneRequest(oneValidRequest, bidderRequest));
});
return requests;
},
/**
* Parse the response and generate one or more bid objects.
*
* @param {*} serverResponse
* @param {*} originalRequest
*/
interpretResponse: function(serverResponse, originalRequest) {
const responseBody = serverResponse.body;
if (!serverResponse.body) {
logWarn(LOG_PREFIX, 'Empty response body HTTP 204, no bids');
return [];
}
const bids = [];
responseBody.seatbid.forEach(serverSeatBid => {
serverSeatBid.bid.forEach(serverBid => {
bids.push(interpretBid(serverBid, originalRequest));
});
});
return bids;
},
/**
* If the publisher allows user-sync activity, the platform will call this function and the adapter may register pixels and/or iframe user syncs.
*
* @param {*} syncOptions
* @param {*} serverResponses
* @param {*} gdprConsent
*/
getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => {
let url = `https://${hostDomain}/cs/usersync.php?`;
// GDPR & CCPA
if (gdprConsent) {
url += '&gdpr_optin=' + (gdprConsent.gdprApplies ? 1 : 0);
url += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '');
}
if (uspConsent) {
url += '&us_privacy=' + encodeURIComponent(uspConsent);
}
// SyncOptions
if (syncOptions.iframeEnabled) {
url += '&type=iframe'
return [{
type: 'iframe',
url: url
}];
} else {
url += '&type=img'
return [{
type: 'image',
url: url
}];
}
}
}
function validBasic(bid) {
if (bid.params == null) {
logWarn(LOG_PREFIX, 'Please review the mandatory Tappx parameters.');
return false;
}
if (!bid.params.tappxkey) {
logWarn(LOG_PREFIX, 'Please review the mandatory Tappxkey parameter.');
return false;
}
if (!bid.params.host) {
logWarn(LOG_PREFIX, 'Please review the mandatory Host parameter.');
return false;
}
let classicEndpoint = true;
if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) {
classicEndpoint = false;
}
if (classicEndpoint && !bid.params.endpoint) {
logWarn(LOG_PREFIX, 'Please review the mandatory endpoint Tappx parameters.');
return false;
}
return true;
}
function validMediaType(bid) {
const video = deepAccess(bid, 'mediaTypes.video');
// Video validations
if (typeof video !== 'undefined') {
if (VIDEO_SUPPORT.indexOf(video.context) === -1) {
logWarn(LOG_PREFIX, 'Please review the mandatory Tappx parameters for Video. Video context not supported.');
return false;
}
}
return true;
}
/**
* Parse the response and generate one bid object.
*
* @param {object} serverBid Bid by OpenRTB 2.5
* @returns {object} Prebid banner bidObject
*/
function interpretBid(serverBid, request) {
let bidReturned = {
requestId: request.bids?.bidId,
cpm: serverBid.price,
currency: serverBid.cur ? serverBid.cur : CUR,
width: serverBid.w,
height: serverBid.h,
ttl: TTL,
creativeId: serverBid.crid,
netRevenue: true,
}
if (typeof serverBid.dealId !== 'undefined') { bidReturned.dealId = serverBid.dealId }
if (typeof serverBid.lurl != 'undefined') { bidReturned.lurl = serverBid.lurl }
if (typeof serverBid.nurl != 'undefined') { bidReturned.nurl = serverBid.nurl }
if (typeof serverBid.burl != 'undefined') { bidReturned.burl = serverBid.burl }
if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') {
bidReturned.vastXml = serverBid.adm;
bidReturned.vastUrl = serverBid.lurl;
bidReturned.ad = serverBid.adm;
bidReturned.mediaType = VIDEO;
bidReturned.width = serverBid.w;
bidReturned.height = serverBid.h;
if (request.bids?.mediaTypes.video.context === 'outstream') {
if (!serverBid.ext.purl) {
logWarn(LOG_PREFIX, 'Error getting player outstream from tappx');
return false;
}
bidReturned.renderer = createRenderer(bidReturned, request, serverBid.ext.purl);
}
} else {
bidReturned.ad = serverBid.adm;
bidReturned.mediaType = BANNER;
}
if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) {
bidReturned.meta = { advertiserDomains: request.bids?.adomain };
}
return bidReturned;
}
/**
* Build and makes the request
*
* @param {*} validBidRequests
* @param {*} bidderRequest
* @return response ad
*/
function buildOneRequest(validBidRequests, bidderRequest) {
let hostInfo = _getHostInfo(validBidRequests);
const ENDPOINT = hostInfo.endpoint;
hostDomain = hostInfo.domain;
const TAPPXKEY = deepAccess(validBidRequests, 'params.tappxkey');
const MKTAG = deepAccess(validBidRequests, 'params.mktag');
const BIDFLOOR = deepAccess(validBidRequests, 'params.bidfloor');
const BIDEXTRA = deepAccess(validBidRequests, 'params.ext');
const bannerMediaType = deepAccess(validBidRequests, 'mediaTypes.banner');
const videoMediaType = deepAccess(validBidRequests, 'mediaTypes.video');
const ORTB2 = deepAccess(validBidRequests, 'ortb2');
// let requests = [];
let payload = {};
let publisher = {};
let tagid;
let api = {};
// > App/Site object
if (deepAccess(validBidRequests, 'params.app')) {
let app = {};
app.name = deepAccess(validBidRequests, 'params.app.name');
app.bundle = deepAccess(validBidRequests, 'params.app.bundle');
app.domain = deepAccess(validBidRequests, 'params.app.domain');
publisher.name = deepAccess(validBidRequests, 'params.app.publisher.name');
publisher.domain = deepAccess(validBidRequests, 'params.app.publisher.domain');
tagid = `${app.name}_typeAdBanVid_${getOs()}`;
payload.app = app;
api[0] = deepAccess(validBidRequests, 'params.api') ? deepAccess(validBidRequests, 'params.api') : [3, 5];
} else {
let bundle = _extractPageUrl(validBidRequests, bidderRequest);
let site = deepAccess(validBidRequests, 'params.site') || {};
site.name = bundle;
site.page = bidderRequest?.refererInfo?.page || deepAccess(validBidRequests, 'params.site.page') || bidderRequest?.refererInfo?.topmostLocation || window.location.href || bundle;
site.domain = bundle;
try {
site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || '';
} catch (e) {
site.ref = bidderRequest?.refererInfo?.ref || window.document.referrer || '';
}
site.ext = {};
site.ext.is_amp = bidderRequest?.refererInfo?.isAmp || 0;
site.ext.page_da = deepAccess(validBidRequests, 'params.site.page') || '-';
site.ext.page_rip = bidderRequest?.refererInfo?.page || '-';
site.ext.page_rit = bidderRequest?.refererInfo?.topmostLocation || '-';
site.ext.page_wlh = window.location.href || '-';
publisher.name = bundle;
publisher.domain = bundle;
let sitename = document.getElementsByTagName('meta')['title'];
if (sitename && sitename.content) {
site.name = sitename.content;
}
tagid = `${site.name}_typeAdBanVid_${getOs()}`;
let keywords = document.getElementsByTagName('meta')['keywords'];
if (keywords && keywords.content) {
site.keywords = keywords.content;
}
payload.site = site;
}
// < App/Site object
// > Imp object
let imp = {};
let w;
let h;
if (bannerMediaType) {
if (!Array.isArray(bannerMediaType.sizes)) { logWarn(LOG_PREFIX, 'Banner sizes array not found.'); }
let banner = {};
w = bannerMediaType.sizes[0][0];
h = bannerMediaType.sizes[0][1];
banner.w = w;
banner.h = h;
if (
((bannerMediaType.sizes[0].indexOf(480) >= 0) && (bannerMediaType.sizes[0].indexOf(320) >= 0)) ||
((bannerMediaType.sizes[0].indexOf(768) >= 0) && (bannerMediaType.sizes[0].indexOf(1024) >= 0))) {
banner.pos = 0;
} else {
banner.pos = 0;
}
banner.api = api;
let format = {};
format[0] = {};
format[0].w = w;
format[0].h = h;
banner.format = format;
imp.banner = banner;
}
if (typeof videoMediaType !== 'undefined') {
let video = {};
let videoParams = deepAccess(validBidRequests, 'params.video');
if (typeof videoParams !== 'undefined') {
for (var key in VIDEO_CUSTOM_PARAMS) {
if (videoParams.hasOwnProperty(key)) {
video[key] = _checkParamDataType(key, videoParams[key], VIDEO_CUSTOM_PARAMS[key]);
}
}
}
if ((video.w === undefined || video.w == null || video.w <= 0) ||
(video.h === undefined || video.h == null || video.h <= 0)) {
if (!Array.isArray(videoMediaType.playerSize)) { logWarn(LOG_PREFIX, 'Video playerSize array not found.'); }
w = videoMediaType.playerSize[0][0];
h = videoMediaType.playerSize[0][1];
video.w = w;
video.h = h;
}
video.mimes = videoMediaType.mimes;
let videoExt = {};
if ((typeof videoMediaType.rewarded !== 'undefined') && videoMediaType.rewarded == 1) {
videoExt.rewarded = videoMediaType.rewarded;
}
video.ext = videoExt;
imp.video = video;
}
imp.id = validBidRequests.bidId;
imp.tagid = tagid;
imp.secure = 1;
imp.bidfloor = deepAccess(validBidRequests, 'params.bidfloor');
if (isFn(validBidRequests.getFloor)) {
try {
let floor = validBidRequests.getFloor({
currency: CUR,
mediaType: '*',
size: '*'
});
if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') {
imp.bidfloor = floor.floor;
} else {
logWarn(LOG_PREFIX, 'Currency not valid. Use only USD with Tappx.');
}
} catch (e) {
logWarn(LOG_PREFIX, e);
imp.bidfloor = deepAccess(validBidRequests, 'params.bidfloor'); // Be sure that we have an imp.bidfloor
}
}
let bidder = {};
bidder.endpoint = ENDPOINT;
bidder.host = hostInfo.url;
bidder.bidfloor = BIDFLOOR;
bidder.ext = (typeof BIDEXTRA == 'object') ? BIDEXTRA : undefined;
imp.ext = {};
imp.ext.bidder = bidder;
// < Imp object
// > Device object
let device = {};
// Mandatory
device.os = getOs();
device.ip = 'peer';
device.ua = navigator.userAgent;
device.ifa = validBidRequests.ifa;
// Optional
device.h = screen.height;
device.w = screen.width;
device.dnt = getDNT() ? 1 : 0;
device.language = getLanguage();
device.make = getVendor();
let geo = {};
geo.country = deepAccess(validBidRequests, 'params.geo.country');
// < Device object
let configGeo = {};
configGeo.country = ORTB2?.device?.geo;
if (typeof configGeo.country !== 'undefined') {
device.geo = configGeo;
} else if (typeof geo.country !== 'undefined') {
device.geo = geo;
};
// > GDPR
let user = {};
user.ext = {};
// Universal ID
let eidsArr = deepAccess(validBidRequests, 'userIdAsEids');
if (typeof eidsArr !== 'undefined') {
eidsArr = eidsArr.filter(
uuid =>
(typeof uuid !== 'undefined' && uuid !== null) &&
(typeof uuid.source == 'string' && uuid.source !== null) &&
(typeof uuid.uids[0].id == 'string' && uuid.uids[0].id !== null)
);
user.ext.eids = eidsArr;
};
let regs = {};
regs.gdpr = 0;
if (!(bidderRequest.gdprConsent == null)) {
if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { regs.gdpr = bidderRequest.gdprConsent.gdprApplies; }
if (regs.gdpr) { user.ext.consent = bidderRequest.gdprConsent.consentString; }
}
// CCPA
regs.ext = {};
if (!(bidderRequest.uspConsent == null)) {
regs.ext.us_privacy = bidderRequest.uspConsent;
}
// COPPA compliance
if (config.getConfig('coppa') === true) {
regs.coppa = config.getConfig('coppa') === true ? 1 : 0;
}
// < GDPR
// > Payload Ext
let payloadExt = {};
payloadExt.bidder = {};
payloadExt.bidder.tappxkey = TAPPXKEY;
payloadExt.bidder.mktag = MKTAG;
payloadExt.bidder.bcid = deepAccess(validBidRequests, 'params.bcid');
payloadExt.bidder.bcrid = deepAccess(validBidRequests, 'params.bcrid');
payloadExt.bidder.ext = (typeof BIDEXTRA == 'object') ? BIDEXTRA : {};
if (typeof videoMediaType !== 'undefined') {
payloadExt.bidder.ext.pbvidtype = videoMediaType.context;
}
// < Payload Ext
// > Payload
payload.id = bidderRequest.bidderRequestId;
payload.test = deepAccess(validBidRequests, 'params.test') ? 1 : 0;
payload.at = 1;
payload.tmax = bidderRequest.timeout ? bidderRequest.timeout : 600;
payload.bidder = BIDDER_CODE;
payload.imp = [imp];
payload.user = user;
payload.ext = payloadExt;
payload.device = device;
payload.regs = regs;
// < Payload
let pbjsv = (getGlobal().version !== null) ? encodeURIComponent(getGlobal().version) : -1;
return {
method: 'POST',
url: `${hostInfo.url}?type_cnn=${TYPE_CNN}&v=${TAPPX_BIDDER_VERSION}&pbjsv=${pbjsv}`,
data: JSON.stringify(payload),
bids: validBidRequests
};
}
function getLanguage() {
const language = navigator.language ? 'language' : 'userLanguage';
return navigator[language].split('-')[0];
}
function getOs() {
let ua = navigator.userAgent;
if (ua.match(/Android/)) { return 'Android'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; }
}
function getVendor() {
let ua = navigator.userAgent;
if (ua.indexOf('Chrome') != -1) { return 'Google'; } else if (ua.indexOf('Firefox') != -1) { return 'Mozilla'; } else if (ua.indexOf('Safari') != -1) { return 'Apple'; } else if (ua.indexOf('Edge') != -1) { return 'Microsoft'; } else if (ua.indexOf('MSIE') != -1 || ua.indexOf('Trident') != -1) { return 'Microsoft'; } else { return ''; }
}
export function _getHostInfo(validBidRequests) {
let domainInfo = {};
let endpoint = deepAccess(validBidRequests, 'params.endpoint');
let hostParam = deepAccess(validBidRequests, 'params.host');
domainInfo.domain = hostParam.split('/', 1)[0];
let regexHostParamHttps = new RegExp(`^https:\/\/`);
let regexHostParamHttp = new RegExp(`^http:\/\/`);
let regexNewEndpoints = new RegExp(`^(vz.*|zz.*)\\.[a-z]{3}\\.tappx\\.com$`, 'i');
let regexClassicEndpoints = new RegExp(`^([a-z]{3}|testing)\\.[a-z]{3}\\.tappx\\.com$`, 'i');
if (regexHostParamHttps.test(hostParam)) {
hostParam = hostParam.replace('https://', '');
} else if (regexHostParamHttp.test(hostParam)) {
hostParam = hostParam.replace('http://', '');
}
if (regexNewEndpoints.test(domainInfo.domain)) {
domainInfo.newEndpoint = true;
domainInfo.endpoint = domainInfo.domain.split('.', 1)[0]
domainInfo.url = `https://${hostParam}`
} else if (regexClassicEndpoints.test(domainInfo.domain)) {
domainInfo.newEndpoint = false;
domainInfo.endpoint = endpoint
domainInfo.url = `https://${hostParam}${endpoint}`
}
return domainInfo;
}
function outstreamRender(bid, request) {
let rendererOptions = {};
rendererOptions = (typeof bid.params[0].video != 'undefined') ? bid.params[0].video : {};
rendererOptions.content = bid.vastXml;
bid.renderer.push(() => {
window.tappxOutstream.renderAd({
sizes: [bid.width, bid.height],
targetId: bid.adUnitCode,
adResponse: bid.adResponse,
rendererOptions: rendererOptions
});
});
}
function createRenderer(bid, request, url) {
const rendererInst = Renderer.install({
id: request.id,
url: url,
loaded: false
});
try {
rendererInst.setRender(outstreamRender);
} catch (err) {
logWarn(LOG_PREFIX, 'Prebid Error calling setRender on renderer');
}
return rendererInst;
}
export function _checkParamDataType(key, value, datatype) {
var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value;
var functionToExecute;
switch (datatype) {
case DATA_TYPES.BOOLEAN:
functionToExecute = isBoolean;
break;
case DATA_TYPES.NUMBER:
functionToExecute = isNumber;
break;
case DATA_TYPES.STRING:
functionToExecute = isStr;
break;
case DATA_TYPES.ARRAY:
functionToExecute = isArray;
break;
}
if (functionToExecute(value)) {
return value;
}
logWarn(LOG_PREFIX, errMsg);
return undefined;
}
export function _extractPageUrl(validBidRequests, bidderRequest) {
let url = bidderRequest?.refererInfo?.page || bidderRequest?.refererInfo?.topmostLocation;
return parseDomain(url, {noLeadingWww: true});
}
registerBidder(spec);