Skip to content
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

Google ad manager support for long-form video #3787

Merged
merged 6 commits into from
Jul 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion gulpHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const BUILD_PATH = './build/dist';
const DEV_PATH = './build/dev';
const ANALYTICS_PATH = '../analytics';


// get only subdirectories that contain package.json with 'main' property
function isModuleDirectory(filePath) {
try {
Expand Down
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"criteortusIdSystem"
],
"adpod": [
"freeWheelAdserverVideo"
"freeWheelAdserverVideo",
"dfpAdServerVideo"
]
}
163 changes: 148 additions & 15 deletions modules/adpod.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { config } from '../src/config';
import { ADPOD } from '../src/mediaTypes';
import Set from 'core-js/library/fn/set';
import find from 'core-js/library/fn/array/find';
import { auctionManager } from '../src/auctionManager';

const from = require('core-js/library/fn/array/from');

Expand Down Expand Up @@ -215,21 +216,21 @@ export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAd
if (!adServerCatId && brandCategoryExclusion) {
utils.logWarn('Detected a bid without meta.adServerCatId while setConfig({adpod.brandCategoryExclusion}) was enabled. This bid has been rejected:', bidResponse)
afterBidAdded();
}

if (config.getConfig('adpod.deferCaching') === false) {
bidCacheRegistry.addBid(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);

updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
} else {
// generate targeting keys for bid
bidCacheRegistry.setupInitialCacheKey(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);
if (config.getConfig('adpod.deferCaching') === false) {
bidCacheRegistry.addBid(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);

// add bid to auction
addBidToAuction(auctionInstance, bidResponse);
afterBidAdded();
updateBidQueue(auctionInstance, bidResponse, afterBidAdded);
} else {
// generate targeting keys for bid
bidCacheRegistry.setupInitialCacheKey(bidResponse);
attachPriceIndustryDurationKeyToBid(bidResponse, brandCategoryExclusion);

// add bid to auction
addBidToAuction(auctionInstance, bidResponse);
afterBidAdded();
}
}
} else {
fn.call(this, auctionInstance, bidResponse, afterBidAdded, bidderRequest);
Expand Down Expand Up @@ -393,6 +394,7 @@ function initAdpodHooks() {
}

initAdpodHooks()

/**
*
* @param {Array[Object]} bids list of 'winning' bids that need to be cached
Expand Down Expand Up @@ -431,11 +433,142 @@ export function sortByPricePerSecond(a, b) {
return 0;
}

/**
* This function returns targeting keyvalue pairs for long-form adserver modules. Freewheel and GAM are currently supporting Prebid long-form
* @param {Object} options
* @param {Array[string]} codes
* @param {function} callback
* @returns targeting kvs for adUnitCodes
*/
export function getTargeting({codes, callback} = {}) {
if (!callback) {
utils.logError('No callback function was defined in the getTargeting call. Aborting getTargeting().');
return;
}
codes = codes || [];
const adPodAdUnits = getAdPodAdUnits(codes);
const bidsReceived = auctionManager.getBidsReceived();
const competiveExclusionEnabled = config.getConfig('adpod.brandCategoryExclusion');
const deferCachingSetting = config.getConfig('adpod.deferCaching');
const deferCachingEnabled = (typeof deferCachingSetting === 'boolean') ? deferCachingSetting : true;

let bids = getBidsForAdpod(bidsReceived, adPodAdUnits);
bids = (competiveExclusionEnabled || deferCachingEnabled) ? getExclusiveBids(bids) : bids;
bids.sort(sortByPricePerSecond);

let targeting = {};
if (deferCachingEnabled === false) {
adPodAdUnits.forEach((adUnit) => {
let adPodTargeting = [];
let adPodDurationSeconds = utils.deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');

bids
.filter((bid) => bid.adUnitCode === adUnit.code)
.forEach((bid, index, arr) => {
if (bid.video.durationBucket <= adPodDurationSeconds) {
adPodTargeting.push({
[TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
});
adPodDurationSeconds -= bid.video.durationBucket;
}
if (index === arr.length - 1 && adPodTargeting.length > 0) {
adPodTargeting.push({
[TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
});
}
});
targeting[adUnit.code] = adPodTargeting;
});

callback(null, targeting);
} else {
let bidsToCache = [];
adPodAdUnits.forEach((adUnit) => {
let adPodDurationSeconds = utils.deepAccess(adUnit, 'mediaTypes.video.adPodDurationSec');

bids
.filter((bid) => bid.adUnitCode === adUnit.code)
.forEach((bid) => {
if (bid.video.durationBucket <= adPodDurationSeconds) {
bidsToCache.push(bid);
adPodDurationSeconds -= bid.video.durationBucket;
}
});
});

callPrebidCacheAfterAuction(bidsToCache, function(error, bidsSuccessfullyCached) {
if (error) {
callback(error, null);
} else {
let groupedBids = utils.groupBy(bidsSuccessfullyCached, 'adUnitCode');
Object.keys(groupedBids).forEach((adUnitCode) => {
let adPodTargeting = [];

groupedBids[adUnitCode].forEach((bid, index, arr) => {
adPodTargeting.push({
[TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]
});

if (index === arr.length - 1 && adPodTargeting.length > 0) {
adPodTargeting.push({
[TARGETING_KEY_CACHE_ID]: bid.adserverTargeting[TARGETING_KEY_CACHE_ID]
});
}
});
targeting[adUnitCode] = adPodTargeting;
});

callback(null, targeting);
}
});
}
return targeting;
}

/**
* This function returns the adunit of mediaType adpod
* @param {Array} codes adUnitCodes
* @returns {Array[Object]} adunits of mediaType adpod
*/
function getAdPodAdUnits(codes) {
return auctionManager.getAdUnits()
.filter((adUnit) => utils.deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD)
.filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true);
}

/**
* This function removes bids of same category. It will be used when competitive exclusion is enabled.
* @param {Array[Object]} bidsReceived
* @returns {Array[Object]} unique category bids
*/
function getExclusiveBids(bidsReceived) {
let bids = bidsReceived
.map((bid) => Object.assign({}, bid, {[TARGETING_KEY_PB_CAT_DUR]: bid.adserverTargeting[TARGETING_KEY_PB_CAT_DUR]}));
bids = utils.groupBy(bids, TARGETING_KEY_PB_CAT_DUR);
let filteredBids = [];
Object.keys(bids).forEach((targetingKey) => {
bids[targetingKey].sort(utils.compareOn('responseTimestamp'));
filteredBids.push(bids[targetingKey][0]);
});
return filteredBids;
}

/**
* This function returns bids for adpod adunits
* @param {Array[Object]} bidsReceived
* @param {Array[Object]} adPodAdUnits
* @returns {Array[Object]} bids of mediaType adpod
*/
function getBidsForAdpod(bidsReceived, adPodAdUnits) {
let adUnitCodes = adPodAdUnits.map((adUnit) => adUnit.code);
return bidsReceived
.filter((bid) => adUnitCodes.indexOf(bid.adUnitCode) != -1 && (bid.video && bid.video.context === ADPOD))
}

const sharedMethods = {
TARGETING_KEY_PB_CAT_DUR: TARGETING_KEY_PB_CAT_DUR,
TARGETING_KEY_CACHE_ID: TARGETING_KEY_CACHE_ID,
'sortByPricePerSecond': sortByPricePerSecond,
'callPrebidCacheAfterAuction': callPrebidCacheAfterAuction
'getTargeting': getTargeting
}
Object.freeze(sharedMethods);

Expand Down
2 changes: 1 addition & 1 deletion modules/categoryTranslation.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export const registerAdserver = hook('async', function(adServer) {
let url;
if (adServer === 'freewheel') {
url = DEFAULT_TRANSLATION_FILE_URL;
initTranslation(url, DEFAULT_IAB_TO_FW_MAPPING_KEY);
}
initTranslation(url, DEFAULT_IAB_TO_FW_MAPPING_KEY);
}, 'registerAdserver');
registerAdserver();

Expand Down
98 changes: 96 additions & 2 deletions modules/dfpAdServerVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { targeting } from '../src/targeting';
import { formatQS, format as buildUrl, parse } from '../src/url';
import { deepAccess, isEmpty, logError, parseSizesInput } from '../src/utils';
import { config } from '../src/config';
import { getHook, submodule } from '../src/hook';
import { auctionManager } from '../src/auctionManager';

/**
* @typedef {Object} DfpVideoParams
Expand Down Expand Up @@ -45,6 +47,8 @@ const defaultParamConstants = {
unviewed_position_start: 1,
};

export const adpodUtils = {};

/**
* Merge all the bid data and publisher-supplied options into a single URL, and then return it.
*
Expand All @@ -56,7 +60,7 @@ const defaultParamConstants = {
* (or the auction's winning bid for this adUnit, if undefined) compete alongside the rest of the
* demand in DFP.
*/
export default function buildDfpVideoUrl(options) {
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;
Expand Down Expand Up @@ -103,6 +107,92 @@ export default function buildDfpVideoUrl(options) {
});
}

export function notifyTranslationModule(fn) {
fn.call(this, 'dfp');
}

getHook('registerAdserver').before(notifyTranslationModule);
jaiminpanchal27 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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 masterTag = buildUrl({
protocol: 'https',
host: 'pubads.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.
Expand Down Expand Up @@ -170,5 +260,9 @@ function getCustParams(bid, options) {
}

registerVideoSupport('dfp', {
buildVideoUrl: buildDfpVideoUrl
buildVideoUrl: buildDfpVideoUrl,
buildAdpodVideoUrl: buildAdpodVideoUrl,
getAdpodTargeting: (args) => adpodUtils.getTargeting(args)
});

submodule('adpod', adpodUtils);
Loading