-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
currency module #1374
currency module #1374
Changes from all commits
2794825
ba0d29c
b44f994
bf5e92a
d30b610
5dfe5d9
338d154
72a42ba
dc82805
1c633d9
74f500f
1742243
44fb5ec
51dd8c3
296b84b
95d5bb6
373b498
aa39ac7
62cc571
f27e0f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
import bidfactory from 'src/bidfactory'; | ||
import { STATUS } from 'src/constants'; | ||
import { ajax } from 'src/ajax'; | ||
import * as utils from 'src/utils'; | ||
import bidmanager from 'src/bidmanager'; | ||
import { config } from 'src/config'; | ||
|
||
const DEFAULT_CURRENCY_RATE_URL = 'http://currency.prebid.org/latest.json'; | ||
const CURRENCY_RATE_PRECISION = 4; | ||
|
||
var bidResponseQueue = []; | ||
var conversionCache = {}; | ||
var currencyRatesLoaded = false; | ||
var adServerCurrency = 'USD'; | ||
|
||
// Used as reference to the original bidmanager.addBidResponse | ||
var originalBidResponse; | ||
|
||
export var currencySupportEnabled = false; | ||
export var currencyRates = {}; | ||
var bidderCurrencyDefault = {}; | ||
|
||
/** | ||
* 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 = 'http://currency.prebid.org/latest.json'] | ||
* 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 } | ||
* } | ||
*/ | ||
export function setConfig(config) { | ||
let url = DEFAULT_CURRENCY_RATE_URL; | ||
|
||
if (typeof config.rates === 'object') { | ||
currencyRates.conversions = config.rates; | ||
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; | ||
} | ||
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 initCurrency(url) { | ||
conversionCache = {}; | ||
currencySupportEnabled = true; | ||
|
||
if (!originalBidResponse) { | ||
utils.logInfo('Installing addBidResponse decorator for currency module', arguments); | ||
|
||
originalBidResponse = bidmanager.addBidResponse; | ||
bidmanager.addBidResponse = addBidResponseDecorator(bidmanager.addBidResponse); | ||
} | ||
|
||
if (!currencyRates.conversions) { | ||
ajax(url, function (response) { | ||
try { | ||
currencyRates = JSON.parse(response); | ||
utils.logInfo('currencyRates set to ' + JSON.stringify(currencyRates)); | ||
currencyRatesLoaded = true; | ||
processBidResponseQueue(); | ||
} catch (e) { | ||
utils.logError('failed to parse currencyRates response: ' + response); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
function resetCurrency() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A
can always do:
instead. They're functionally equivalent, but the latter is conceptually simpler. It's also easier to test. To rigorously test an object with a To rigorously test an object with a |
||
if (originalBidResponse) { | ||
utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); | ||
|
||
bidmanager.addBidResponse = originalBidResponse; | ||
originalBidResponse = undefined; | ||
} | ||
|
||
adServerCurrency = 'USD'; | ||
conversionCache = {}; | ||
currencySupportEnabled = false; | ||
currencyRatesLoaded = false; | ||
currencyRates = {}; | ||
bidderCurrencyDefault = {}; | ||
} | ||
|
||
export function addBidResponseDecorator(fn) { | ||
return function(adUnitCode, bid) { | ||
if (!bid) { | ||
return fn.apply(this, arguments); // 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'; | ||
} | ||
|
||
// execute immediately if the bid is already in the desired currency | ||
if (bid.currency === adServerCurrency) { | ||
return fn.apply(this, arguments); | ||
} | ||
|
||
bidResponseQueue.push(wrapFunction(fn, this, arguments)); | ||
if (!currencySupportEnabled || currencyRatesLoaded) { | ||
processBidResponseQueue(); | ||
} | ||
} | ||
} | ||
|
||
function processBidResponseQueue() { | ||
while (bidResponseQueue.length > 0) { | ||
(bidResponseQueue.shift())(); | ||
} | ||
} | ||
|
||
function wrapFunction(fn, context, params) { | ||
return function() { | ||
var bid = params[1]; | ||
if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { | ||
var fromCurrency = bid.currency; | ||
try { | ||
var conversion = getCurrencyConversion(fromCurrency); | ||
bid.originalCpm = bid.cpm; | ||
bid.originalCurrency = bid.currency; | ||
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] = bidfactory.createBid(STATUS.NO_BID, { | ||
bidder: bid.bidderCode || bid.bidder, | ||
bidId: bid.adId | ||
}); | ||
} | ||
} | ||
return fn.apply(context, params); | ||
}; | ||
} | ||
|
||
function getCurrencyConversion(fromCurrency) { | ||
var conversionRate = null; | ||
var rates; | ||
|
||
if (fromCurrency in conversionCache) { | ||
conversionRate = conversionCache[fromCurrency]; | ||
utils.logMessage('Using conversionCache value ' + conversionRate + ' for fromCurrency ' + fromCurrency); | ||
} 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 === adServerCurrency) { | ||
conversionRate = 1; | ||
} else { | ||
var toCurrency = adServerCurrency; | ||
|
||
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 (!(fromCurrency in conversionCache)) { | ||
utils.logMessage('Adding conversionCache value ' + conversionRate + ' for fromCurrency ' + fromCurrency); | ||
conversionCache[fromCurrency] = 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ | |
* Defining and access properties in this way is now deprecated, but these will | ||
* continue to work during a deprecation window. | ||
*/ | ||
import { isValidePriceConfig } from './cpmBucketManager'; | ||
import { isValidPriceConfig } from './cpmBucketManager'; | ||
const utils = require('./utils'); | ||
|
||
const DEFAULT_DEBUG = false; | ||
|
@@ -134,7 +134,7 @@ export function newConfig() { | |
utils.logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using `medium` as default.'); | ||
} | ||
} else if (typeof val === 'object') { | ||
if (!isValidePriceConfig(val)) { | ||
if (!isValidPriceConfig(val)) { | ||
utils.logError('Invalid custom price value passed to `setPriceGranularity()`'); | ||
return false; | ||
} | ||
|
@@ -144,16 +144,16 @@ export function newConfig() { | |
|
||
/* | ||
* Returns configuration object if called without parameters, | ||
* or single configuration property if given a string matching a configuartion | ||
* property name. | ||
* or single configuration property if given a string matching a configuration | ||
* property name. Allows deep access e.g. getConfig('currency.adServerCurrency') | ||
* | ||
* If called with callback parameter, or a string and a callback parameter, | ||
* subscribes to configuration updates. See `subscribe` function for usage. | ||
*/ | ||
function getConfig(...args) { | ||
if (args.length <= 1 && typeof args[0] !== 'function') { | ||
const option = args[0]; | ||
return option ? config[option] : config; | ||
return option ? utils.deepAccess(config, option) : config; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the JSDocs with the new functionality |
||
} | ||
|
||
return subscribe(...args); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking over the issue @snapwich brought up here: I don't think we should support modules which monkey-patch functions in core.
Reason being: I've heard that we expect other people to write their own external modules. If so, then this behavior means that all of Prebid's internal APIs are now external APIs. If we remove a method or change a signature on anything we export, they could reasonably claim that it's a breaking change to the library.
I propose that we deliberately define all the "plugin points" in prebid-core, which are functions which we expect modules to call. For example,
registerBidAdapter()
is a plugin point today.@snapwich @mkendall07 What do you guys think?
Off the top of my head, something like
addBidPostprocessor((bid) => bid))
looks like a reasonable "plugin point" for this feature--although I'm not sure it fits with the spirit of the project. I just think that whatever it is, it should be explicit.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true but the same would be true if we created a plugin system. I do think creating a standardized way to implement a plugin to Prebid.js core would be nice though; although a big undertaking, so I wonder if we should wait until we have a few more use cases other than currency?
In the current state I'm not sure how we could do this without this decorator. I considered using the event system, but that would require mutating the bid objects, which seemed hacky; plus I don't think there would be any way using events to defer the bid response (which is required if currency file hasn't loaded yet).
If there's a better way to do the currently provided functionality I'm open to hearing it. Otherwise, maybe we could put a plugin system on the backlog for the future when we have more modules that require such features and we have a more formal insight into their needs?
It's possible the need for that plugin system is now though if we can't figure out a solution for this module and concurrent auctions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah... I agree, a standardized way to implement plugins is way outside of the scope. I'm not even sure it's possible... but if it is, we definitely need more use-cases.
My suggestion would be something like this (not sure if you need sync or async functions... so I described both):
In the currency module, replace
bidmanager.addBidResponse = addBidResponseDecorator(bidmanager.addBidResponse);
with an equivalentbidAdjustments.register(impl)
. I think this is doable, but correct me if I'm wrong.In #1421, @jaiminpanchal27 can call
bidAdjustments.adjust(bid)
inside theaddBidResponse
function. OrbidAdjustments.adjust(bid, addBidResponse)
if it's async.Does that sound like it'd work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi. I'm relatively new to the project and have followed this discussion with a lot of interest.
We are currently facing the issue of converting currencies as well and this what we are planning to do,
which might be of interest as well.
Use bidderSettings
At this point using the the
pbjs.bidderSettings
to update our CPMs. If the currency module would simply be a library to call then integrating this would be a breeze. Our code looks something like this:If the currency module can just be imported like
General approach for bid responses
Regarding a general plugin architecture. We have had very good experience with redux middlewares.
Basically you they are functions you can chain together. A
BidResponseMiddleware
would be a niceway to abstract over all the stuff that is currently configured somewhere and applied somehow.
Also a middleware could do all the side-effects, which makes these things trivial to test, because
you can just swap them.
Example
A middleware could have the shape of
By definition a middleware applies some side effects and calls the next
middleware. A middleware can also change the bidResponse, e.g. with currency calculations
or other stuff.
It would also be nice to have access to the raw response from the actual SSP to work around
bugs or missing implementations. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the ideas @muuki88, but we've decided to move forward with the design as noted in #1089, modified slightly to use the new setConfig() approach. We think this has the advantage of preserving the bidCpmAdjustment for bidder-specific gross-vs-net conversions, separating currency which really isn't bidder-specific.
As for debugging, @snapwich has already implemented what we think is a pretty solid approach -- the bidResponse gets two new attributes: originalCpm and originalCurrency. Then cpm and currency are overwritten with the converted values. So you'll be able to access the before/after bidResponse attributes from the console.