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

Prebid server support for OpenRTB Native bids #3145

Merged
merged 11 commits into from
Jul 30, 2019
167 changes: 152 additions & 15 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { ajax } from '../../src/ajax';
import { STATUS, S2S, EVENTS } from '../../src/constants';
import adapterManager from '../../src/adapterManager';
import { config } from '../../src/config';
import { VIDEO } from '../../src/mediaTypes';
import { VIDEO, NATIVE } from '../../src/mediaTypes';
import { processNativeAdUnitParams } from '../../src/native';
import { isValid } from '../../src/adapters/bidderFactory';
import events from '../../src/events';
import includes from 'core-js/library/fn/array/includes';
Expand Down Expand Up @@ -431,6 +432,34 @@ const LEGACY_PROTOCOL = {
}
};

// https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40
let nativeDataIdMap = {
sponsoredBy: 1, // sponsored
body: 2, // desc
rating: 3,
likes: 4,
downloads: 5,
price: 6,
salePrice: 7,
phone: 8,
address: 9,
body2: 10, // desc2
cta: 12 // ctatext
};
let nativeDataNames = Object.keys(nativeDataIdMap);

let nativeImgIdMap = {
icon: 1,
image: 3
};

// enable reverse lookup
[nativeDataIdMap, nativeImgIdMap].forEach(map => {
Object.keys(map).forEach(key => {
map[map[key]] = key;
});
});

/*
* Protocol spec for OpenRTB endpoint
* e.g., https://<prebid-server-url>/v1/openrtb2/auction
Expand All @@ -454,11 +483,12 @@ const OPEN_RTB_PROTOCOL = {
}
});

let banner;
let mediaTypes = {};

// default to banner if mediaTypes isn't defined
if (utils.isEmpty(adUnit.mediaTypes)) {
const sizeObjects = adUnit.sizes.map(size => ({ w: size[0], h: size[1] }));
banner = {format: sizeObjects};
mediaTypes['banner'] = {format: sizeObjects};
}

const bannerParams = utils.deepAccess(adUnit, 'mediaTypes.banner');
Expand All @@ -473,13 +503,90 @@ const OPEN_RTB_PROTOCOL = {
return { w, h };
});

banner = {format};
mediaTypes['banner'] = {format};
}

let video;
const videoParams = utils.deepAccess(adUnit, 'mediaTypes.video');
if (!utils.isEmpty(videoParams)) {
video = videoParams;
if (videoParams.context === 'outstream' && !adUnit.renderer) {
// Don't push oustream w/o renderer to request object.
utils.logError('Outstream bid without renderer cannot be sent to Prebid Server.');
} else {
mediaTypes['video'] = videoParams;
}
}

const nativeParams = processNativeAdUnitParams(utils.deepAccess(adUnit, 'mediaTypes.native'));
if (nativeParams) {
try {
mediaTypes['native'] = {
request: JSON.stringify({
// TODO: determine best way to pass these and if we allow defaults
context: nativeParams.context || 1,
plcmttype: nativeParams.plcmttype || 1,
// TODO: figure out how to support privacy field
// privacy: int
snapwich marked this conversation as resolved.
Show resolved Hide resolved
assets: Object.keys(nativeParams).reduce((assets, type, index) => {
let params = nativeParams[type];

function newAsset(obj) {
return Object.assign({
required: params.required ? 1 : 0
}, obj ? utils.cleanObj(obj) : {});
}

if (
(type === 'image' || type === 'icon')
) {
let imgTypeId = nativeImgIdMap[type];
let asset = utils.cleanObj({
type: imgTypeId,
w: utils.deepAccess(params, 'sizes.0'),
h: utils.deepAccess(params, 'sizes.1'),
wmin: utils.deepAccess(params, 'aspect_ratios.0.min_width')
});
if (!(asset.w || asset.wmin)) {
throw 'invalid img sizes (must provided sizes or aspect_ratios)';
}
if (Array.isArray(params.aspect_ratios)) {
// pass aspect_ratios as ext data I guess?
asset.ext = {
aspectratios: params.aspect_ratios.map(
ratio => `${ratio.ratio_width}:${ratio.ratio_height}`
)
}
}
assets.push(newAsset({
img: asset
}));
} else if (type === 'title') {
if (!params.len) {
throw utils.logWarn('invalid title.len');
}
assets.push(newAsset({
title: {
len: params.len
}
}));
} else {
let dataAssetTypeId = nativeDataIdMap[type];
if (dataAssetTypeId) {
assets.push(newAsset({
data: {
type: dataAssetTypeId,
len: params.len
}
}))
}
}
return assets;
}, [])
}),
ver: '1.2'
}
} catch (e) {
utils.logError('error creating native request: ' + String(e))
}
}

// get bidder params in form { <bidder code>: {...params} }
Expand All @@ -494,16 +601,11 @@ const OPEN_RTB_PROTOCOL = {

const imp = { id: adUnit.code, ext, secure: _s2sConfig.secure };

if (banner) { imp.banner = banner; }
if (video) {
if (video.context === 'outstream' && !adUnit.renderer) {
// Don't push oustream w/o renderer to request object.
utils.logError('Outstream bid without renderer cannot be sent to Prebid Server.');
} else {
imp.video = video;
}
Object.assign(imp, mediaTypes);

if (imp.banner || imp.video || imp.native) {
imps.push(imp);
}
if (imp.banner || imp.video) { imps.push(imp); }
});

if (!imps.length) {
Expand Down Expand Up @@ -653,6 +755,41 @@ const OPEN_RTB_PROTOCOL = {

if (bid.adm) { bidObject.vastXml = bid.adm; }
if (!bidObject.vastUrl && bid.nurl) { bidObject.vastUrl = bid.nurl; }
} else if (utils.deepAccess(bid, 'ext.prebid.type') === NATIVE) {
bidObject.mediaType = NATIVE;
let adm;
if (typeof bid.adm === 'string') {
adm = bidObject.adm = JSON.parse(bid.adm);
} else {
adm = bidObject.adm = bid.adm;
}

if (utils.isPlainObject(adm) && Array.isArray(adm.assets)) {
bidObject.native = utils.cleanObj(adm.assets.reduce((native, asset) => {
if (utils.isPlainObject(asset.img)) {
native[nativeImgIdMap[asset.img.type]] = utils.pick(
asset.img,
['url', 'w as width', 'h as height']
);
} else if (utils.isPlainObject(asset.title)) {
native['title'] = asset.title.text
} else if (utils.isPlainObject(asset.data)) {
nativeDataNames.forEach(dataType => {
if (nativeDataIdMap[dataType] === asset.data.type) {
native[dataType] = asset.data.value;
}
});
}
return native;
}, utils.cleanObj({
clickUrl: adm.link,
clickTrackers: adm.clickTrackers,
impressionTrackers: adm.impressionTrackers,
javascriptTrackers: adm.javascriptTrackers
})));
} else {
utils.logError('prebid server native response contained no assets');
}
} else { // banner
if (bid.adm && bid.nurl) {
bidObject.ad = bid.adm;
Expand Down
49 changes: 11 additions & 38 deletions modules/rubiconAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,6 @@ const cache = {
timeouts: {},
};

// basically lodash#pick that also allows transformation functions and property renaming
function _pick(obj, properties) {
return properties.reduce((newObj, prop, i) => {
if (typeof prop === 'function') {
return newObj;
}

let newProp = prop;
let match = prop.match(/^(.+?)\sas\s(.+?)$/i);

if (match) {
prop = match[1];
newProp = match[2];
}

let value = obj[prop];
if (typeof properties[i + 1] === 'function') {
value = properties[i + 1](value, newObj);
}
if (typeof value !== 'undefined') {
newObj[newProp] = value;
}

return newObj;
}, {});
}

function stringProperties(obj) {
return Object.keys(obj).reduce((newObj, prop) => {
let value = obj[prop];
Expand Down Expand Up @@ -98,7 +71,7 @@ function formatSource(src) {

function sendMessage(auctionId, bidWonId) {
function formatBid(bid) {
return _pick(bid, [
return utils.pick(bid, [
'bidder',
'bidId',
'status',
Expand All @@ -113,7 +86,7 @@ function sendMessage(auctionId, bidWonId) {
'clientLatencyMillis',
'serverLatencyMillis',
'params',
'bidResponse', bidResponse => bidResponse ? _pick(bidResponse, [
'bidResponse', bidResponse => bidResponse ? utils.pick(bidResponse, [
'bidPriceUSD',
'dealId',
'dimensions',
Expand All @@ -122,7 +95,7 @@ function sendMessage(auctionId, bidWonId) {
]);
}
function formatBidWon(bid) {
return Object.assign(formatBid(bid), _pick(bid.adUnit, [
return Object.assign(formatBid(bid), utils.pick(bid.adUnit, [
'adUnitCode',
'transactionId',
'videoAdFormat', () => bid.videoAdFormat,
Expand All @@ -149,7 +122,7 @@ function sendMessage(auctionId, bidWonId) {
let bid = auctionCache.bids[bidId];
let adUnit = adUnits[bid.adUnit.adUnitCode];
if (!adUnit) {
adUnit = adUnits[bid.adUnit.adUnitCode] = _pick(bid.adUnit, [
adUnit = adUnits[bid.adUnit.adUnitCode] = utils.pick(bid.adUnit, [
'adUnitCode',
'transactionId',
'mediaTypes',
Expand Down Expand Up @@ -187,7 +160,7 @@ function sendMessage(auctionId, bidWonId) {
// This allows the bidWon events to have these params even in the case of a delayed render
Object.keys(auctionCache.bids).forEach(function (bidId) {
let adCode = auctionCache.bids[bidId].adUnit.adUnitCode;
Object.assign(auctionCache.bids[bidId], _pick(adUnitMap[adCode], ['accountId', 'siteId', 'zoneId']));
Object.assign(auctionCache.bids[bidId], utils.pick(adUnitMap[adCode], ['accountId', 'siteId', 'zoneId']));
});

let auction = {
Expand Down Expand Up @@ -233,7 +206,7 @@ function sendMessage(auctionId, bidWonId) {
}

export function parseBidResponse(bid) {
return _pick(bid, [
return utils.pick(bid, [
'bidPriceUSD', () => {
if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') {
return Number(bid.cpm);
Expand All @@ -247,7 +220,7 @@ export function parseBidResponse(bid) {
'dealId',
'status',
'mediaType',
'dimensions', () => _pick(bid, [
'dimensions', () => utils.pick(bid, [
'width',
'height'
])
Expand Down Expand Up @@ -323,7 +296,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
case AUCTION_INIT:
// set the rubicon aliases
setRubiconAliases(adapterManager.aliasRegistry);
let cacheEntry = _pick(args, [
let cacheEntry = utils.pick(args, [
'timestamp',
'timeout'
]);
Expand All @@ -336,7 +309,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
// mark adUnits we expect bidWon events for
cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false;

memo[bid.bidId] = _pick(bid, [
memo[bid.bidId] = utils.pick(bid, [
'bidder', bidder => bidder.toLowerCase(),
'bidId',
'status', () => 'no-bid', // default a bid to no-bid until response is recieved or bid is timed out
Expand All @@ -345,7 +318,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
switch (bid.bidder) {
// specify bidder params we want here
case 'rubicon':
return _pick(params, [
return utils.pick(params, [
'accountId',
'siteId',
'zoneId'
Expand Down Expand Up @@ -376,7 +349,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
}
}
},
'adUnit', () => _pick(bid, [
'adUnit', () => utils.pick(bid, [
'adUnitCode',
'transactionId',
'sizes as dimensions', sizes => sizes.map(sizeToDimensions),
Expand Down
Loading