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 Core: switch native assets to ortb2 format #7847

Closed
56 changes: 31 additions & 25 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,8 @@ Object.assign(ORTB2.prototype, {
this.adUnitsByImp[impressionId] = adUnit;

const nativeParams = adUnit.nativeParams;
let nativeAssets;
if (nativeParams) {
let nativeAssets = nativeAssetCache[impressionId] = deepAccess(nativeParams, 'ortb.assets');
if (nativeParams && !nativeAssets) {
try {
nativeAssets = nativeAssetCache[impressionId] = Object.keys(nativeParams).reduce((assets, type) => {
let params = nativeParams[type];
Expand Down Expand Up @@ -981,30 +981,36 @@ Object.assign(ORTB2.prototype, {
}

if (isPlainObject(adm) && Array.isArray(adm.assets)) {
let origAssets = nativeAssetCache[bid.impid];
bidObject.native = cleanObj(adm.assets.reduce((native, asset) => {
let origAsset = origAssets[asset.id];
if (isPlainObject(asset.img)) {
native[origAsset.img.type ? nativeImgIdMap[origAsset.img.type] : 'image'] = pick(
asset.img,
['url', 'w as width', 'h as height']
);
} else if (isPlainObject(asset.title)) {
native['title'] = asset.title.text
} else if (isPlainObject(asset.data)) {
nativeDataNames.forEach(dataType => {
if (nativeDataIdMap[dataType] === origAsset.data.type) {
native[dataType] = asset.data.value;
}
});
if (deepAccess(bidRequest, 'mediaTypes.native.ortb')) {
bidObject.native = {
ortb: adm,
}
return native;
}, cleanObj({
clickUrl: adm.link,
clickTrackers: deepAccess(adm, 'link.clicktrackers'),
impressionTrackers: trackers[nativeEventTrackerMethodMap.img],
javascriptTrackers: trackers[nativeEventTrackerMethodMap.js]
})));
} else {
let origAssets = nativeAssetCache[bid.impid];
bidObject.native = cleanObj(adm.assets.reduce((native, asset) => {
let origAsset = origAssets[asset.id];
if (isPlainObject(asset.img)) {
native[origAsset.img.type ? nativeImgIdMap[origAsset.img.type] : 'image'] = pick(
asset.img,
['url', 'w as width', 'h as height']
);
} else if (isPlainObject(asset.title)) {
native['title'] = asset.title.text
} else if (isPlainObject(asset.data)) {
nativeDataNames.forEach(dataType => {
if (nativeDataIdMap[dataType] === origAsset.data.type) {
native[dataType] = asset.data.value;
}
});
}
return native;
}, cleanObj({
clickUrl: adm.link,
clickTrackers: deepAccess(adm, 'link.clicktrackers'),
impressionTrackers: trackers[nativeEventTrackerMethodMap.img],
javascriptTrackers: trackers[nativeEventTrackerMethodMap.js]
})));
}
} else {
logError('prebid server native response contained no assets');
}
Expand Down
140 changes: 136 additions & 4 deletions src/native.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deepAccess, getKeyByValue, insertHtmlIntoIframe, logError, triggerPixel } from './utils.js';
import { deepAccess, getKeyByValue, insertHtmlIntoIframe, isNumber, isPlainObject, logError, triggerPixel } from './utils.js';
import includes from 'prebidjs-polyfill/includes.js';
import {auctionManager} from './auctionManager.js';

Expand All @@ -11,6 +11,50 @@ export const NATIVE_TARGETING_KEYS = Object.keys(CONSTANTS.NATIVE_KEYS).map(
);

const IMAGE = {
ortb: {
ver: '1.2',
assets: [
{
required: 1,
id: 1,
img: {
type: 3,
wmin: 100,
hmin: 100,
}
},
{
required: 1,
id: 2,
title: {
len: 140,
}
},
{
required: 1,
id: 3,
data: {
type: 1,
}
},
{
required: 0,
id: 4,
data: {
type: 2,
}
},
{
required: 0,
id: 5,
img: {
type: 1,
wmin: 20,
hmin: 20,
}
},
],
},
image: { required: true },
title: { required: true },
sponsoredBy: { required: true },
Expand All @@ -30,9 +74,12 @@ const SUPPORTED_TYPES = {
*/
export function processNativeAdUnitParams(params) {
if (params && params.type && typeIsSupported(params.type)) {
return SUPPORTED_TYPES[params.type];
params = SUPPORTED_TYPES[params.type];
}

if (params && params.ortb && !isOpenRTBBidRequestValid(params.ortb)) {
return;
}
return params;
}

Expand All @@ -45,6 +92,61 @@ export function decorateAdUnitsWithNativeParams(adUnits) {
}
});
}
export function isOpenRTBBidRequestValid(ortb) {
const assets = ortb.assets;
if (!Array.isArray(assets) || assets.length === 0) {
logError(`assets in mediaTypes.native.ortb is not an array, or it's empty. Assets: `, assets);
return false;
}

// validate that ids exist, that they are unique and that they are numbers
const ids = assets.map(asset => asset.id);
if (assets.length !== new Set(ids).size || ids.some(id => id !== parseInt(id, 10))) {
logError(`each asset object must have 'id' property, it must be unique and it must be an integer`);
return false;
}

if (ortb.hasOwnProperty('eventtrackers') && !Array.isArray(ortb.eventtrackers)) {
logError('ortb.eventtrackers is not an array. Eventtrackers: ', ortb.eventtrackers);
return false;
}

return assets.every(asset => isOpenRTBAssetValid(asset))
}

function isOpenRTBAssetValid(asset) {
if (!isPlainObject(asset)) {
logError(`asset must be an object. Provided asset: `, asset);
return false;
}
if (asset.img) {
if (!isNumber(asset.img.w) && !isNumber(asset.img.wmin)) {
logError(`for img asset there must be 'w' or 'wmin' property`);
return false;
}
if (!isNumber(asset.img.h) && !isNumber(asset.img.hmin)) {
logError(`for img asset there must be 'h' or 'hmin' property`);
return false;
}
} else if (asset.title) {
if (!isNumber(asset.title.len)) {
logError(`for title asset there must be 'len' property defined`);
return false;
}
} else if (asset.data) {
if (!isNumber(asset.data.type)) {
logError(`for data asset 'type' property must be a number`);
return false;
}
} else if (asset.video) {
if (!Array.isArray(asset.video.mimes) || !Array.isArray(asset.video.protocols) ||
!isNumber(asset.video.minduration) || !isNumber(asset.video.maxduration)) {
logError('video asset is not properly configured');
return false;
}
}
return true;
}

/**
* Check if the native type specified in the adUnit is supported by Prebid.
Expand Down Expand Up @@ -80,12 +182,18 @@ export const hasNonNativeBidder = adUnit =>
* @return {Boolean} If object is valid
*/
export function nativeBidIsValid(bid, {index = auctionManager.index} = {}) {
const bidRequest = index.getAdUnit(bid);
if (!bidRequest) { return false; }

if (deepAccess(bid, 'native.ortb') && deepAccess(bidRequest, 'nativeParams.ortb')) {
return isNativeOpenRTBBidValid(bid.native.ortb, bidRequest.nativeParams.ortb);
}
// all native bid responses must define a landing page url
if (!deepAccess(bid, 'native.clickUrl')) {
return false;
}

const requestedAssets = index.getAdUnit(bid).nativeParams;
const requestedAssets = bidRequest.nativeParams;
if (!requestedAssets) {
return true;
}
Expand All @@ -100,6 +208,23 @@ export function nativeBidIsValid(bid, {index = auctionManager.index} = {}) {
return requiredAssets.every(asset => includes(returnedAssets, asset));
}

export function isNativeOpenRTBBidValid(bidORTB, bidRequestORTB) {
if (!deepAccess(bidORTB, 'link.url')) {
logError(`native response doesn't have 'link' property. Ortb response: `, bidORTB);
return false;
}

let requiredAssetIds = bidRequestORTB.assets.filter(asset => asset.required === 1).map(a => a.id);
let returnedAssetIds = bidORTB.assets.map(asset => asset.id);

const match = requiredAssetIds.every(assetId => includes(returnedAssetIds, assetId));
if (!match) {
logError(`didn't receive a bid with all required assets. Required ids: ${requiredAssetIds}, but received ids in response: ${returnedAssetIds}`);
}

return match;
}

/*
* Native responses may have associated impression or click trackers.
* This retrieves the appropriate tracker urls for the given ad object and
Expand Down Expand Up @@ -230,9 +355,16 @@ export function getAllAssetsMessage(data, adObject) {
const message = {
message: 'assetResponse',
adId: data.adId,
assets: []
};

if (adObject.native.ortb) {
Object.keys(adObject.native).forEach(key => {
message[key] = adObject.native[key];
});
return message;
}
message.assets = [];

Object.keys(adObject.native).forEach(function(key, index) {
if (key === 'adTemplate' && adObject.native[key]) {
message.adTemplate = getAssetValue(adObject.native[key]);
Expand Down
15 changes: 9 additions & 6 deletions src/secureCreatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js';

const BID_WON = constants.EVENTS.BID_WON;
const STALE_RENDER = constants.EVENTS.STALE_RENDER;
const WON_AD_IDS = new Set();

const HANDLER_MAP = {
'Prebid Request': handleRenderRequest,
Expand Down Expand Up @@ -109,6 +110,13 @@ function handleNativeRequest(reply, data, adObject) {
logError(`Cannot find ad '${data.adId}' for x-origin event request`);
return;
}

if (!WON_AD_IDS.has(adObject.adId)) {
WON_AD_IDS.add(adObject.adId);
auctionManager.addWinningBid(adObject);
events.emit(BID_WON, adObject);
}

switch (data.action) {
case 'assetRequest':
reply(getAssetMessage(data, adObject));
Expand All @@ -122,12 +130,7 @@ function handleNativeRequest(reply, data, adObject) {
resizeRemoteCreative(adObject);
break;
default:
const trackerType = fireNativeTrackers(data, adObject);
if (trackerType === 'click') {
return;
}
auctionManager.addWinningBid(adObject);
events.emit(BID_WON, adObject);
fireNativeTrackers(data, adObject);
}
}

Expand Down
78 changes: 77 additions & 1 deletion test/spec/native_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
nativeBidIsValid,
getAssetMessage,
getAllAssetsMessage,
decorateAdUnitsWithNativeParams
decorateAdUnitsWithNativeParams,
isOpenRTBBidRequestValid,
isNativeOpenRTBBidValid
} from 'src/native.js';
import CONSTANTS from 'src/constants.json';
import {stubAuctionIndex} from '../helpers/indexStub.js';
Expand Down Expand Up @@ -393,6 +395,80 @@ describe('native.js', function () {
});
});

describe('validate native openRTB', function () {
it('should validate openRTB request', function() {
let openRTBNativeRequest = {assets: []};
// assets array can't be empty
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false);
openRTBNativeRequest.assets.push({
id: 1.5,
required: 1,
title: {},
});

// asset.id must be integer
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false);
openRTBNativeRequest.assets[0].id = 1;
// title must have 'len' property
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false);
openRTBNativeRequest.assets[0].title.len = 140;
// openRTB request is valid
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(true);

openRTBNativeRequest.assets.push({
id: 2,
required: 1,
video: {
mimes: [],
protocols: [],
minduration: 50,
},
});
// video asset should have all required properties
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(false);
openRTBNativeRequest.assets[1].video.maxduration = 60;
expect(isOpenRTBBidRequestValid(openRTBNativeRequest)).to.eq(true);
});

it('should validate openRTB native bid', function () {
const openRTBRequest = {
assets: [
{
id: 1,
required: 1,
},
{
id: 2,
required: 0,
},
{
id: 3,
required: 1,
},
]
}
let openRTBBid = {
assets: [
{
id: 1,
},
{
id: 2,
}
],
};

// link is missing
expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(false);
openRTBBid.link = { url: 'www.foo.bar' }
// required id == 3 is missing
expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(false);

openRTBBid.assets[1].id = 3;
expect(isNativeOpenRTBBidValid(openRTBBid, openRTBRequest)).to.eq(true);
});
});

describe('validate native', function () {
const adUnit = {
transactionId: 'test_adunit',
Expand Down
Loading