mk9-prebid
Version:
Header Bidding Management Library
300 lines (266 loc) • 11.6 kB
JavaScript
import { getGlobal } from '../src/prebidGlobal.js';
import { createBid } from '../src/bidfactory.js';
import { STATUS } from '../src/constants.json';
import { ajax } from '../src/ajax.js';
import * as utils from '../src/utils.js';
import { config } from '../src/config.js';
import { getHook } from '../src/hook.js';
const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$';
const CURRENCY_RATE_PRECISION = 4;
var bidResponseQueue = [];
var conversionCache = {};
var currencyRatesLoaded = false;
var needToCallForCurrencyFile = true;
var adServerCurrency = 'USD';
export var currencySupportEnabled = false;
export var currencyRates = {};
var bidderCurrencyDefault = {};
var defaultRates;
/**
* Configuration function for currency
* @param {string} [config.adServerCurrency = 'USD']
* ISO 4217 3-letter currency code that represents the target currency. (e.g. 'EUR'). If this value is present,
* the currency conversion feature is activated.
* @param {number} [config.granularityMultiplier = 1]
* A decimal value representing how mcuh to scale the price granularity calculations.
* @param {object} config.bidderCurrencyDefault
* An optional argument to specify bid currencies for bid adapters. This option is provided for the transitional phase
* before every bid adapter will specify its own bid currency. If the adapter specifies a bid currency, this value is
* ignored for that bidder.
*
* example:
* {
* rubicon: 'USD'
* }
* @param {string} [config.conversionRateFile = 'URL pointing to conversion file']
* Optional path to a file containing currency conversion data. Prebid.org hosts a file that is used as the default,
* if not specified.
* @param {object} [config.rates]
* This optional argument allows you to specify the rates with a JSON object, subverting the need for a external
* config.conversionRateFile parameter. If this argument is specified, the conversion rate file will not be loaded.
*
* example:
* {
* 'GBP': { 'CNY': 8.8282, 'JPY': 141.7, 'USD': 1.2824 },
* 'USD': { 'CNY': 6.8842, 'GBP': 0.7798, 'JPY': 110.49 }
* }
* @param {object} [config.defaultRates]
* This optional currency rates definition follows the same format as config.rates, however it is only utilized if
* there is an error loading the config.conversionRateFile.
*/
export function setConfig(config) {
let url = DEFAULT_CURRENCY_RATE_URL;
if (typeof config.rates === 'object') {
currencyRates.conversions = config.rates;
currencyRatesLoaded = true;
needToCallForCurrencyFile = false; // don't call if rates are already specified
}
if (typeof config.defaultRates === 'object') {
defaultRates = config.defaultRates;
// set up the default rates to be used if the rate file doesn't get loaded in time
currencyRates.conversions = defaultRates;
currencyRatesLoaded = true;
}
if (typeof config.adServerCurrency === 'string') {
utils.logInfo('enabling currency support', arguments);
adServerCurrency = config.adServerCurrency;
if (config.conversionRateFile) {
utils.logInfo('currency using override conversionRateFile:', config.conversionRateFile);
url = config.conversionRateFile;
}
// see if the url contains a date macro
// this is a workaround to the fact that jsdelivr doesn't currently support setting a 24-hour HTTP cache header
// So this is an approach to let the browser cache a copy of the file each day
// We should remove the macro once the CDN support a day-level HTTP cache setting
const macroLocation = url.indexOf('$$TODAY$$');
if (macroLocation !== -1) {
// get the date to resolve the macro
const d = new Date();
let month = `${d.getMonth() + 1}`;
let day = `${d.getDate()}`;
if (month.length < 2) month = `0${month}`;
if (day.length < 2) day = `0${day}`;
const todaysDate = `${d.getFullYear()}${month}${day}`;
// replace $$TODAY$$ with todaysDate
url = `${url.substring(0, macroLocation)}${todaysDate}${url.substring(macroLocation + 9, url.length)}`;
}
initCurrency(url);
} else {
// currency support is disabled, setting defaults
utils.logInfo('disabling currency support');
resetCurrency();
}
if (typeof config.bidderCurrencyDefault === 'object') {
bidderCurrencyDefault = config.bidderCurrencyDefault;
}
}
config.getConfig('currency', config => setConfig(config.currency));
function errorSettingsRates(msg) {
if (defaultRates) {
utils.logWarn(msg);
utils.logWarn('Currency failed loading rates, falling back to currency.defaultRates');
} else {
utils.logError(msg);
}
}
function initCurrency(url) {
conversionCache = {};
currencySupportEnabled = true;
utils.logInfo('Installing addBidResponse decorator for currency module', arguments);
// Adding conversion function to prebid global for external module and on page use
getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency);
getHook('addBidResponse').before(addBidResponseHook, 100);
// call for the file if we haven't already
if (needToCallForCurrencyFile) {
needToCallForCurrencyFile = false;
ajax(url,
{
success: function (response) {
try {
currencyRates = JSON.parse(response);
utils.logInfo('currencyRates set to ' + JSON.stringify(currencyRates));
currencyRatesLoaded = true;
processBidResponseQueue();
} catch (e) {
errorSettingsRates('Failed to parse currencyRates response: ' + response);
}
},
error: errorSettingsRates
}
);
}
}
function resetCurrency() {
utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments);
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove();
delete getGlobal().convertCurrency;
adServerCurrency = 'USD';
conversionCache = {};
currencySupportEnabled = false;
currencyRatesLoaded = false;
needToCallForCurrencyFile = true;
currencyRates = {};
bidderCurrencyDefault = {};
}
export function addBidResponseHook(fn, adUnitCode, bid) {
if (!bid) {
return fn.call(this, adUnitCode); // if no bid, call original and let it display warnings
}
let bidder = bid.bidderCode || bid.bidder;
if (bidderCurrencyDefault[bidder]) {
let currencyDefault = bidderCurrencyDefault[bidder];
if (bid.currency && currencyDefault !== bid.currency) {
utils.logWarn(`Currency default '${bidder}: ${currencyDefault}' ignored. adapter specified '${bid.currency}'`);
} else {
bid.currency = currencyDefault;
}
}
// default to USD if currency not set
if (!bid.currency) {
utils.logWarn('Currency not specified on bid. Defaulted to "USD"');
bid.currency = 'USD';
}
// used for analytics
bid.getCpmInNewCurrency = function(toCurrency) {
return (parseFloat(this.cpm) * getCurrencyConversion(this.currency, toCurrency)).toFixed(3);
};
// execute immediately if the bid is already in the desired currency
if (bid.currency === adServerCurrency) {
return fn.call(this, adUnitCode, bid);
}
bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid]));
if (!currencySupportEnabled || currencyRatesLoaded) {
processBidResponseQueue();
}
}
function processBidResponseQueue() {
while (bidResponseQueue.length > 0) {
(bidResponseQueue.shift())();
}
}
function wrapFunction(fn, context, params) {
return function() {
let bid = params[1];
if (bid !== undefined && 'currency' in bid && 'cpm' in bid) {
let fromCurrency = bid.currency;
try {
let conversion = getCurrencyConversion(fromCurrency);
if (conversion !== 1) {
bid.cpm = (parseFloat(bid.cpm) * conversion).toFixed(4);
bid.currency = adServerCurrency;
}
} catch (e) {
utils.logWarn('Returning NO_BID, getCurrencyConversion threw error: ', e);
params[1] = createBid(STATUS.NO_BID, {
bidder: bid.bidderCode || bid.bidder,
bidId: bid.requestId
});
}
}
return fn.apply(context, params);
};
}
function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) {
var conversionRate = null;
var rates;
let cacheKey = `${fromCurrency}->${toCurrency}`;
if (cacheKey in conversionCache) {
conversionRate = conversionCache[cacheKey];
utils.logMessage('Using conversionCache value ' + conversionRate + ' for ' + cacheKey);
} else if (currencySupportEnabled === false) {
if (fromCurrency === 'USD') {
conversionRate = 1;
} else {
throw new Error('Prebid currency support has not been enabled and fromCurrency is not USD');
}
} else if (fromCurrency === toCurrency) {
conversionRate = 1;
} else {
if (fromCurrency in currencyRates.conversions) {
// using direct conversion rate from fromCurrency to toCurrency
rates = currencyRates.conversions[fromCurrency];
if (!(toCurrency in rates)) {
// bid should fail, currency is not supported
throw new Error('Specified adServerCurrency in config \'' + toCurrency + '\' not found in the currency rates file');
}
conversionRate = rates[toCurrency];
utils.logInfo('getCurrencyConversion using direct ' + fromCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate);
} else if (toCurrency in currencyRates.conversions) {
// using reciprocal of conversion rate from toCurrency to fromCurrency
rates = currencyRates.conversions[toCurrency];
if (!(fromCurrency in rates)) {
// bid should fail, currency is not supported
throw new Error('Specified fromCurrency \'' + fromCurrency + '\' not found in the currency rates file');
}
conversionRate = roundFloat(1 / rates[fromCurrency], CURRENCY_RATE_PRECISION);
utils.logInfo('getCurrencyConversion using reciprocal ' + fromCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate);
} else {
// first defined currency base used as intermediary
var anyBaseCurrency = Object.keys(currencyRates.conversions)[0];
if (!(fromCurrency in currencyRates.conversions[anyBaseCurrency])) {
// bid should fail, currency is not supported
throw new Error('Specified fromCurrency \'' + fromCurrency + '\' not found in the currency rates file');
}
var toIntermediateConversionRate = 1 / currencyRates.conversions[anyBaseCurrency][fromCurrency];
if (!(toCurrency in currencyRates.conversions[anyBaseCurrency])) {
// bid should fail, currency is not supported
throw new Error('Specified adServerCurrency in config \'' + toCurrency + '\' not found in the currency rates file');
}
var fromIntermediateConversionRate = currencyRates.conversions[anyBaseCurrency][toCurrency];
conversionRate = roundFloat(toIntermediateConversionRate * fromIntermediateConversionRate, CURRENCY_RATE_PRECISION);
utils.logInfo('getCurrencyConversion using intermediate ' + fromCurrency + ' thru ' + anyBaseCurrency + ' to ' + toCurrency + ' conversionRate ' + conversionRate);
}
}
if (!(cacheKey in conversionCache)) {
utils.logMessage('Adding conversionCache value ' + conversionRate + ' for ' + cacheKey);
conversionCache[cacheKey] = conversionRate;
}
return conversionRate;
}
function roundFloat(num, dec) {
var d = 1;
for (let i = 0; i < dec; i++) {
d += '0';
}
return Math.round(num * d) / d;
}