mk9-prebid
Version:
Header Bidding Management Library
294 lines (251 loc) • 11.2 kB
JavaScript
/**
* This module adds [DFP support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid.
*/
import { registerVideoSupport } from '../src/adServerManager.js';
import { targeting } from '../src/targeting.js';
import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, buildUrl } from '../src/utils.js';
import { config } from '../src/config.js';
import { getHook, submodule } from '../src/hook.js';
import { auctionManager } from '../src/auctionManager.js';
import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js';
import events from '../src/events.js';
import CONSTANTS from '../src/constants.json';
/**
* @typedef {Object} DfpVideoParams
*
* This object contains the params needed to form a URL which hits the
* [DFP API]{@link https://support.google.com/dfp_premium/answer/1068325?hl=en}.
*
* All params (except iu, mentioned below) should be considered optional. This module will choose reasonable
* defaults for all of the other required params.
*
* The cust_params property, if present, must be an object. It will be merged with the rest of the
* standard Prebid targeting params (hb_adid, hb_bidder, etc).
*
* @param {string} iu This param *must* be included, in order for us to create a valid request.
* @param [string] description_url This field is required if you want Ad Exchange to bid on our ad unit...
* but otherwise optional
*/
/**
* @typedef {Object} DfpVideoOptions
*
* @param {Object} adUnit The adUnit which this bid is supposed to help fill.
* @param [Object] bid The bid which should be considered alongside the rest of the adserver's demand.
* If this isn't defined, then we'll use the winning bid for the adUnit.
*
* @param {DfpVideoParams} [params] Query params which should be set on the DFP request.
* These will override this module's defaults whenever they conflict.
* @param {string} [url] video adserver url
*/
/** Safe defaults which work on pretty much all video calls. */
const defaultParamConstants = {
env: 'vp',
gdfp_req: 1,
output: 'vast',
unviewed_position_start: 1,
};
export const adpodUtils = {};
/**
* Merge all the bid data and publisher-supplied options into a single URL, and then return it.
*
* @see [The DFP API]{@link https://support.google.com/dfp_premium/answer/1068325?hl=en#env} for details.
*
* @param {DfpVideoOptions} options Options which should be used to construct the URL.
*
* @return {string} A URL which calls DFP, letting options.bid
* (or the auction's winning bid for this adUnit, if undefined) compete alongside the rest of the
* demand in DFP.
*/
export function buildDfpVideoUrl(options) {
if (!options.params && !options.url) {
logError(`A params object or a url is required to use $$PREBID_GLOBAL$$.adServers.dfp.buildVideoUrl`);
return;
}
const adUnit = options.adUnit;
const bid = options.bid || targeting.getWinningBids(adUnit.code)[0];
let urlComponents = {};
if (options.url) {
// when both `url` and `params` are given, parsed url will be overwriten
// with any matching param components
urlComponents = parseUrl(options.url, {noDecodeWholeURL: true});
if (isEmpty(options.params)) {
return buildUrlFromAdserverUrlComponents(urlComponents, bid, options);
}
}
const derivedParams = {
correlator: Date.now(),
sz: parseSizesInput(deepAccess(adUnit, 'mediaTypes.video.playerSize')).join('|'),
url: encodeURIComponent(location.href),
};
const encodedCustomParams = getCustParams(bid, options);
const queryParams = Object.assign({},
defaultParamConstants,
urlComponents.search,
derivedParams,
options.params,
{ cust_params: encodedCustomParams }
);
const descriptionUrl = getDescriptionUrl(bid, options, 'params');
if (descriptionUrl) { queryParams.description_url = descriptionUrl; }
const gdprConsent = gdprDataHandler.getConsentData();
if (gdprConsent) {
if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); }
if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; }
if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; }
}
const uspConsent = uspDataHandler.getConsentData();
if (uspConsent) { queryParams.us_privacy = uspConsent; }
return buildUrl({
protocol: 'https',
host: 'securepubads.g.doubleclick.net',
pathname: '/gampad/ads',
search: queryParams
});
}
export function notifyTranslationModule(fn) {
fn.call(this, 'dfp');
}
if (config.getConfig('brandCategoryTranslation.translationFile')) { getHook('registerAdserver').before(notifyTranslationModule); }
/**
* @typedef {Object} DfpAdpodOptions
*
* @param {string} code Ad Unit code
* @param {Object} params Query params which should be set on the DFP request.
* These will override this module's defaults whenever they conflict.
* @param {function} callback Callback function to execute when master tag is ready
*/
/**
* Creates master tag url for long-form
* @param {DfpAdpodOptions} options
* @returns {string} A URL which calls DFP with custom adpod targeting key values to compete with rest of the demand in DFP
*/
export function buildAdpodVideoUrl({code, params, callback} = {}) {
if (!params || !callback) {
logError(`A params object and a callback is required to use pbjs.adServers.dfp.buildAdpodVideoUrl`);
return;
}
const derivedParams = {
correlator: Date.now(),
sz: getSizeForAdUnit(code),
url: encodeURIComponent(location.href),
};
function getSizeForAdUnit(code) {
let adUnit = auctionManager.getAdUnits()
.filter((adUnit) => adUnit.code === code)
let sizes = deepAccess(adUnit[0], 'mediaTypes.video.playerSize');
return parseSizesInput(sizes).join('|');
}
adpodUtils.getTargeting({
'codes': [code],
'callback': createMasterTag
});
function createMasterTag(err, targeting) {
if (err) {
callback(err, null);
return;
}
let initialValue = {
[adpodUtils.TARGETING_KEY_PB_CAT_DUR]: undefined,
[adpodUtils.TARGETING_KEY_CACHE_ID]: undefined
}
let customParams = {};
if (targeting[code]) {
customParams = targeting[code].reduce((acc, curValue) => {
if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_PB_CAT_DUR) {
acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] = (typeof acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] !== 'undefined') ? acc[adpodUtils.TARGETING_KEY_PB_CAT_DUR] + ',' + curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR] : curValue[adpodUtils.TARGETING_KEY_PB_CAT_DUR];
} else if (Object.keys(curValue)[0] === adpodUtils.TARGETING_KEY_CACHE_ID) {
acc[adpodUtils.TARGETING_KEY_CACHE_ID] = curValue[adpodUtils.TARGETING_KEY_CACHE_ID]
}
return acc;
}, initialValue);
}
let encodedCustomParams = encodeURIComponent(formatQS(customParams));
const queryParams = Object.assign({},
defaultParamConstants,
derivedParams,
params,
{ cust_params: encodedCustomParams }
);
const gdprConsent = gdprDataHandler.getConsentData();
if (gdprConsent) {
if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); }
if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; }
if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; }
}
const uspConsent = uspDataHandler.getConsentData();
if (uspConsent) { queryParams.us_privacy = uspConsent; }
const masterTag = buildUrl({
protocol: 'https',
host: 'securepubads.g.doubleclick.net',
pathname: '/gampad/ads',
search: queryParams
});
callback(null, masterTag);
}
}
/**
* Builds a video url from a base dfp video url and a winning bid, appending
* Prebid-specific key-values.
* @param {Object} components base video adserver url parsed into components object
* @param {AdapterBidResponse} bid winning bid object to append parameters from
* @param {Object} options Options which should be used to construct the URL (used for custom params).
* @return {string} video url
*/
function buildUrlFromAdserverUrlComponents(components, bid, options) {
const descriptionUrl = getDescriptionUrl(bid, components, 'search');
if (descriptionUrl) { components.search.description_url = descriptionUrl; }
const encodedCustomParams = getCustParams(bid, options);
components.search.cust_params = (components.search.cust_params) ? components.search.cust_params + '%26' + encodedCustomParams : encodedCustomParams;
return buildUrl(components);
}
/**
* Returns the encoded vast url if it exists on a bid object, only if prebid-cache
* is disabled, and description_url is not already set on a given input
* @param {AdapterBidResponse} bid object to check for vast url
* @param {Object} components the object to check that description_url is NOT set on
* @param {string} prop the property of components that would contain description_url
* @return {string | undefined} The encoded vast url if it exists, or undefined
*/
function getDescriptionUrl(bid, components, prop) {
if (config.getConfig('cache.url')) { return; }
if (!deepAccess(components, `${prop}.description_url`)) {
const vastUrl = bid && bid.vastUrl;
if (vastUrl) { return encodeURIComponent(vastUrl); }
} else {
logError(`input cannnot contain description_url`);
}
}
/**
* Returns the encoded `cust_params` from the bid.adserverTargeting and adds the `hb_uuid`, and `hb_cache_id`. Optionally the options.params.cust_params
* @param {AdapterBidResponse} bid
* @param {Object} options this is the options passed in from the `buildDfpVideoUrl` function
* @return {Object} Encoded key value pairs for cust_params
*/
function getCustParams(bid, options) {
const adserverTargeting = (bid && bid.adserverTargeting) || {};
let allTargetingData = {};
const adUnit = options && options.adUnit;
if (adUnit) {
let allTargeting = targeting.getAllTargeting(adUnit.code);
allTargetingData = (allTargeting) ? allTargeting[adUnit.code] : {};
}
const prebidTargetingSet = Object.assign({},
// Why are we adding standard keys here ? Refer https://github.com/prebid/Prebid.js/issues/3664
{ hb_uuid: bid && bid.videoCacheKey },
// hb_cache_id became optional in prebid 5.0 after 4.x enabled the concept of optional keys. Discussion led to reversing the prior expectation of deprecating hb_uuid
{ hb_cache_id: bid && bid.videoCacheKey },
allTargetingData,
adserverTargeting,
);
events.emit(CONSTANTS.EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet});
// merge the prebid + publisher targeting sets
const publisherTargetingSet = deepAccess(options, 'params.cust_params');
const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet);
return encodeURIComponent(formatQS(targetingSet));
}
registerVideoSupport('dfp', {
buildVideoUrl: buildDfpVideoUrl,
buildAdpodVideoUrl: buildAdpodVideoUrl,
getAdpodTargeting: (args) => adpodUtils.getTargeting(args)
});
submodule('adpod', adpodUtils);