mk9-prebid
Version:
Header Bidding Management Library
646 lines (564 loc) • 19.1 kB
JavaScript
/*
* Module for getting and setting Prebid configuration.
*/
/**
* @typedef {Object} MediaTypePriceGranularity
*
* @property {(string|Object)} [banner]
* @property {(string|Object)} [native]
* @property {(string|Object)} [video]
* @property {(string|Object)} [video-instream]
* @property {(string|Object)} [video-outstream]
*/
import { isValidPriceConfig } from './cpmBucketManager.js';
import find from 'core-js-pure/features/array/find.js';
import includes from 'core-js-pure/features/array/includes.js';
import Set from 'core-js-pure/features/set';
import { mergeDeep } from './utils.js';
const from = require('core-js-pure/features/array/from.js');
const utils = require('./utils.js');
const CONSTANTS = require('./constants.json');
const DEFAULT_DEBUG = utils.getParameterByName(CONSTANTS.DEBUG_MODE).toUpperCase() === 'TRUE';
const DEFAULT_BIDDER_TIMEOUT = 3000;
const DEFAULT_PUBLISHER_DOMAIN = window.location.origin;
const DEFAULT_ENABLE_SEND_ALL_BIDS = true;
const DEFAULT_DISABLE_AJAX_TIMEOUT = false;
const DEFAULT_BID_CACHE = false;
const DEFAULT_DEVICE_ACCESS = true;
const DEFAULT_MAX_NESTED_IFRAMES = 10;
const DEFAULT_TIMEOUTBUFFER = 400;
export const RANDOM = 'random';
const FIXED = 'fixed';
const VALID_ORDERS = {};
VALID_ORDERS[RANDOM] = true;
VALID_ORDERS[FIXED] = true;
const DEFAULT_BIDDER_SEQUENCE = RANDOM;
const GRANULARITY_OPTIONS = {
LOW: 'low',
MEDIUM: 'medium',
HIGH: 'high',
AUTO: 'auto',
DENSE: 'dense',
CUSTOM: 'custom'
};
const ALL_TOPICS = '*';
/**
* @typedef {object} PrebidConfig
*
* @property {string} cache.url Set a url if we should use prebid-cache to store video bids before adding
* bids to the auction. **NOTE** This must be set if you want to use the dfpAdServerVideo module.
*/
export function newConfig() {
let listeners = [];
let defaults;
let config;
let bidderConfig;
let currBidder = null;
function resetConfig() {
defaults = {};
let newConfig = {
// `debug` is equivalent to legacy `pbjs.logging` property
_debug: DEFAULT_DEBUG,
get debug() {
return this._debug;
},
set debug(val) {
this._debug = val;
},
// default timeout for all bids
_bidderTimeout: DEFAULT_BIDDER_TIMEOUT,
get bidderTimeout() {
return this._bidderTimeout;
},
set bidderTimeout(val) {
this._bidderTimeout = val;
},
// domain where prebid is running for cross domain iframe communication
_publisherDomain: DEFAULT_PUBLISHER_DOMAIN,
get publisherDomain() {
return this._publisherDomain;
},
set publisherDomain(val) {
this._publisherDomain = val;
},
// calls existing function which may be moved after deprecation
_priceGranularity: GRANULARITY_OPTIONS.MEDIUM,
set priceGranularity(val) {
if (validatePriceGranularity(val)) {
if (typeof val === 'string') {
this._priceGranularity = (hasGranularity(val)) ? val : GRANULARITY_OPTIONS.MEDIUM;
} else if (utils.isPlainObject(val)) {
this._customPriceBucket = val;
this._priceGranularity = GRANULARITY_OPTIONS.CUSTOM;
utils.logMessage('Using custom price granularity');
}
}
},
get priceGranularity() {
return this._priceGranularity;
},
_customPriceBucket: {},
get customPriceBucket() {
return this._customPriceBucket;
},
/**
* mediaTypePriceGranularity
* @type {MediaTypePriceGranularity}
*/
_mediaTypePriceGranularity: {},
get mediaTypePriceGranularity() {
return this._mediaTypePriceGranularity;
},
set mediaTypePriceGranularity(val) {
this._mediaTypePriceGranularity = Object.keys(val).reduce((aggregate, item) => {
if (validatePriceGranularity(val[item])) {
if (typeof val === 'string') {
aggregate[item] = (hasGranularity(val[item])) ? val[item] : this._priceGranularity;
} else if (utils.isPlainObject(val)) {
aggregate[item] = val[item];
utils.logMessage(`Using custom price granularity for ${item}`);
}
} else {
utils.logWarn(`Invalid price granularity for media type: ${item}`);
}
return aggregate;
}, {});
},
_sendAllBids: DEFAULT_ENABLE_SEND_ALL_BIDS,
get enableSendAllBids() {
return this._sendAllBids;
},
set enableSendAllBids(val) {
this._sendAllBids = val;
},
_useBidCache: DEFAULT_BID_CACHE,
get useBidCache() {
return this._useBidCache;
},
set useBidCache(val) {
this._useBidCache = val;
},
/**
* deviceAccess set to false will disable setCookie, getCookie, hasLocalStorage
* @type {boolean}
*/
_deviceAccess: DEFAULT_DEVICE_ACCESS,
get deviceAccess() {
return this._deviceAccess;
},
set deviceAccess(val) {
this._deviceAccess = val;
},
_bidderSequence: DEFAULT_BIDDER_SEQUENCE,
get bidderSequence() {
return this._bidderSequence;
},
set bidderSequence(val) {
if (VALID_ORDERS[val]) {
this._bidderSequence = val;
} else {
utils.logWarn(`Invalid order: ${val}. Bidder Sequence was not set.`);
}
},
// timeout buffer to adjust for bidder CDN latency
_timeoutBuffer: DEFAULT_TIMEOUTBUFFER,
get timeoutBuffer() {
return this._timeoutBuffer;
},
set timeoutBuffer(val) {
this._timeoutBuffer = val;
},
_disableAjaxTimeout: DEFAULT_DISABLE_AJAX_TIMEOUT,
get disableAjaxTimeout() {
return this._disableAjaxTimeout;
},
set disableAjaxTimeout(val) {
this._disableAjaxTimeout = val;
},
// default max nested iframes for referer detection
_maxNestedIframes: DEFAULT_MAX_NESTED_IFRAMES,
get maxNestedIframes() {
return this._maxNestedIframes;
},
set maxNestedIframes(val) {
this._maxNestedIframes = val;
},
_auctionOptions: {},
get auctionOptions() {
return this._auctionOptions;
},
set auctionOptions(val) {
if (validateauctionOptions(val)) {
this._auctionOptions = val;
}
},
};
if (config) {
callSubscribers(
Object.keys(config).reduce((memo, topic) => {
if (config[topic] !== newConfig[topic]) {
memo[topic] = newConfig[topic] || {};
}
return memo;
},
{})
);
}
config = newConfig;
bidderConfig = {};
function hasGranularity(val) {
return find(Object.keys(GRANULARITY_OPTIONS), option => val === GRANULARITY_OPTIONS[option]);
}
function validatePriceGranularity(val) {
if (!val) {
utils.logError('Prebid Error: no value passed to `setPriceGranularity()`');
return false;
}
if (typeof val === 'string') {
if (!hasGranularity(val)) {
utils.logWarn('Prebid Warning: setPriceGranularity was called with invalid setting, using `medium` as default.');
}
} else if (utils.isPlainObject(val)) {
if (!isValidPriceConfig(val)) {
utils.logError('Invalid custom price value passed to `setPriceGranularity()`');
return false;
}
}
return true;
}
function validateauctionOptions(val) {
if (!utils.isPlainObject(val)) {
utils.logWarn('Auction Options must be an object')
return false
}
for (let k of Object.keys(val)) {
if (k !== 'secondaryBidders' && k !== 'suppressStaleRender') {
utils.logWarn(`Auction Options given an incorrect param: ${k}`)
return false
}
if (k === 'secondaryBidders') {
if (!utils.isArray(val[k])) {
utils.logWarn(`Auction Options ${k} must be of type Array`);
return false
} else if (!val[k].every(utils.isStr)) {
utils.logWarn(`Auction Options ${k} must be only string`);
return false
}
} else if (k === 'suppressStaleRender') {
if (!utils.isBoolean(val[k])) {
utils.logWarn(`Auction Options ${k} must be of type boolean`);
return false;
}
}
}
return true;
}
}
/**
* Returns base config with bidder overrides (if there is currently a bidder)
* @private
*/
function _getConfig() {
if (currBidder && bidderConfig && utils.isPlainObject(bidderConfig[currBidder])) {
let currBidderConfig = bidderConfig[currBidder];
const configTopicSet = new Set(Object.keys(config).concat(Object.keys(currBidderConfig)));
return from(configTopicSet).reduce((memo, topic) => {
if (typeof currBidderConfig[topic] === 'undefined') {
memo[topic] = config[topic];
} else if (typeof config[topic] === 'undefined') {
memo[topic] = currBidderConfig[topic];
} else {
if (utils.isPlainObject(currBidderConfig[topic])) {
memo[topic] = mergeDeep({}, config[topic], currBidderConfig[topic]);
} else {
memo[topic] = currBidderConfig[topic];
}
}
return memo;
}, {});
}
return Object.assign({}, config);
}
/*
* Returns configuration object if called without parameters,
* 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 ? utils.deepAccess(_getConfig(), option) : _getConfig();
}
return subscribe(...args);
}
/**
* Internal API for modules (such as prebid-server) that might need access to all bidder config
*/
function getBidderConfig() {
return bidderConfig;
}
/**
* Returns backwards compatible FPD data for modules
*/
function getLegacyFpd(obj) {
if (typeof obj !== 'object') return;
let duplicate = {};
Object.keys(obj).forEach((type) => {
let prop = (type === 'site') ? 'context' : type;
duplicate[prop] = (prop === 'context' || prop === 'user') ? Object.keys(obj[type]).filter(key => key !== 'data').reduce((result, key) => {
if (key === 'ext') {
utils.mergeDeep(result, obj[type][key]);
} else {
utils.mergeDeep(result, {[key]: obj[type][key]});
}
return result;
}, {}) : obj[type];
});
return duplicate;
}
/**
* Returns backwards compatible FPD data for modules
*/
function getLegacyImpFpd(obj) {
if (typeof obj !== 'object') return;
let duplicate = {};
if (utils.deepAccess(obj, 'ext.data')) {
Object.keys(obj.ext.data).forEach((key) => {
if (key === 'pbadslot') {
utils.mergeDeep(duplicate, {context: {pbAdSlot: obj.ext.data[key]}});
} else if (key === 'adserver') {
utils.mergeDeep(duplicate, {context: {adServer: obj.ext.data[key]}});
} else {
utils.mergeDeep(duplicate, {context: {data: {[key]: obj.ext.data[key]}}});
}
});
}
return duplicate;
}
/**
* Copy FPD over to OpenRTB standard format in config
*/
function convertFpd(opt) {
let duplicate = {};
Object.keys(opt).forEach((type) => {
let prop = (type === 'context') ? 'site' : type;
duplicate[prop] = (prop === 'site' || prop === 'user') ? Object.keys(opt[type]).reduce((result, key) => {
if (key === 'data') {
utils.mergeDeep(result, {ext: {data: opt[type][key]}});
} else {
utils.mergeDeep(result, {[key]: opt[type][key]});
}
return result;
}, {}) : opt[type];
});
return duplicate;
}
/**
* Copy Impression FPD over to OpenRTB standard format in config
* Only accepts bid level context.data values with pbAdSlot and adServer exceptions
*/
function convertImpFpd(opt) {
let duplicate = {};
Object.keys(opt).filter(prop => prop === 'context').forEach((type) => {
Object.keys(opt[type]).forEach((key) => {
if (key === 'data') {
utils.mergeDeep(duplicate, {ext: {data: opt[type][key]}});
} else {
if (typeof opt[type][key] === 'object' && !Array.isArray(opt[type][key])) {
Object.keys(opt[type][key]).forEach(data => {
utils.mergeDeep(duplicate, {ext: {data: {[key.toLowerCase()]: {[data.toLowerCase()]: opt[type][key][data]}}}});
});
} else {
utils.mergeDeep(duplicate, {ext: {data: {[key.toLowerCase()]: opt[type][key]}}});
}
}
});
});
return duplicate;
}
/**
* Copy FPD over to OpenRTB standard format in each adunit
*/
function convertAdUnitFpd(arr) {
let convert = [];
arr.forEach((adunit) => {
if (adunit.fpd) {
(adunit['ortb2Imp']) ? utils.mergeDeep(adunit['ortb2Imp'], convertImpFpd(adunit.fpd)) : adunit['ortb2Imp'] = convertImpFpd(adunit.fpd);
convert.push((({ fpd, ...duplicate }) => duplicate)(adunit));
} else {
convert.push(adunit);
}
});
return convert;
}
/*
* Sets configuration given an object containing key-value pairs and calls
* listeners that were added by the `subscribe` function
*/
function setConfig(options) {
if (!utils.isPlainObject(options)) {
utils.logError('setConfig options must be an object');
return;
}
let topics = Object.keys(options);
let topicalConfig = {};
topics.forEach(topic => {
let prop = (topic === 'fpd') ? 'ortb2' : topic;
let option = (topic === 'fpd') ? convertFpd(options[topic]) : options[topic];
if (utils.isPlainObject(defaults[prop]) && utils.isPlainObject(option)) {
option = Object.assign({}, defaults[prop], option);
}
topicalConfig[prop] = config[prop] = option;
});
callSubscribers(topicalConfig);
}
/**
* Sets configuration defaults which setConfig values can be applied on top of
* @param {object} options
*/
function setDefaults(options) {
if (!utils.isPlainObject(defaults)) {
utils.logError('defaults must be an object');
return;
}
Object.assign(defaults, options);
// Add default values to config as well
Object.assign(config, options);
}
/*
* Adds a function to a set of listeners that are invoked whenever `setConfig`
* is called. The subscribed function will be passed the options object that
* was used in the `setConfig` call. Topics can be subscribed to to only get
* updates when specific properties are updated by passing a topic string as
* the first parameter.
*
* Returns an `unsubscribe` function for removing the subscriber from the
* set of listeners
*
* Example use:
* // subscribe to all configuration changes
* subscribe((config) => console.log('config set:', config));
*
* // subscribe to only 'logging' changes
* subscribe('logging', (config) => console.log('logging set:', config));
*
* // unsubscribe
* const unsubscribe = subscribe(...);
* unsubscribe(); // no longer listening
*/
function subscribe(topic, listener) {
let callback = listener;
if (typeof topic !== 'string') {
// first param should be callback function in this case,
// meaning it gets called for any config change
callback = topic;
topic = ALL_TOPICS;
}
if (typeof callback !== 'function') {
utils.logError('listener must be a function');
return;
}
const nl = { topic, callback };
listeners.push(nl);
// save and call this function to remove the listener
return function unsubscribe() {
listeners.splice(listeners.indexOf(nl), 1);
};
}
/*
* Calls listeners that were added by the `subscribe` function
*/
function callSubscribers(options) {
const TOPICS = Object.keys(options);
// call subscribers of a specific topic, passing only that configuration
listeners
.filter(listener => includes(TOPICS, listener.topic))
.forEach(listener => {
listener.callback({ [listener.topic]: options[listener.topic] });
});
// call subscribers that didn't give a topic, passing everything that was set
listeners
.filter(listener => listener.topic === ALL_TOPICS)
.forEach(listener => listener.callback(options));
}
function setBidderConfig(config) {
try {
check(config);
config.bidders.forEach(bidder => {
if (!bidderConfig[bidder]) {
bidderConfig[bidder] = {};
}
Object.keys(config.config).forEach(topic => {
let prop = (topic === 'fpd') ? 'ortb2' : topic;
let option = (topic === 'fpd') ? convertFpd(config.config[topic]) : config.config[topic];
if (utils.isPlainObject(option)) {
bidderConfig[bidder][prop] = Object.assign({}, bidderConfig[bidder][prop] || {}, option);
} else {
bidderConfig[bidder][prop] = option;
}
});
});
} catch (e) {
utils.logError(e);
}
function check(obj) {
if (!utils.isPlainObject(obj)) {
throw 'setBidderConfig bidder options must be an object';
}
if (!(Array.isArray(obj.bidders) && obj.bidders.length)) {
throw 'setBidderConfig bidder options must contain a bidders list with at least 1 bidder';
}
if (!utils.isPlainObject(obj.config)) {
throw 'setBidderConfig bidder options must contain a config object';
}
}
}
/**
* Internal functions for core to execute some synchronous code while having an active bidder set.
*/
function runWithBidder(bidder, fn) {
currBidder = bidder;
try {
return fn();
} finally {
resetBidder();
}
}
function callbackWithBidder(bidder) {
return function(cb) {
return function(...args) {
if (typeof cb === 'function') {
return runWithBidder(bidder, utils.bind.call(cb, this, ...args))
} else {
utils.logWarn('config.callbackWithBidder callback is not a function');
}
}
}
}
function getCurrentBidder() {
return currBidder;
}
function resetBidder() {
currBidder = null;
}
resetConfig();
return {
getCurrentBidder,
resetBidder,
getConfig,
setConfig,
setDefaults,
resetConfig,
runWithBidder,
callbackWithBidder,
setBidderConfig,
getBidderConfig,
convertAdUnitFpd,
getLegacyFpd,
getLegacyImpFpd
};
}
export const config = newConfig();