From 97b29d022bec63b6648e75c3c82f92bef3786128 Mon Sep 17 00:00:00 2001 From: Rich Snapp Date: Tue, 30 Jul 2019 10:30:42 -0600 Subject: [PATCH] Prebid server support for OpenRTB Native bids (#3145) * initial support for native requests in prebid server * add support for native request in prebid server OpenRTB * fixes and new test for native openrtb responses * updates to prebidServerBidAdapter for native support * resolve conflicts with prebid-server video changes * successfully returning and rendering native ad through prebid server * add example prebid server native page * fix bugs and tests for prebid-server native * resolve unused variable lint alert in native example * allow native adUnits without sizes in prebid server --- .../gpt/prebidServer_native_example.html | 174 +++++++++++++ modules/prebidServerBidAdapter/index.js | 236 ++++++++++++++++-- modules/rubiconAnalyticsAdapter.js | 49 +--- src/utils.js | 47 ++++ .../modules/prebidServerBidAdapter_spec.js | 177 ++++++++++++- 5 files changed, 621 insertions(+), 62 deletions(-) create mode 100644 integrationExamples/gpt/prebidServer_native_example.html diff --git a/integrationExamples/gpt/prebidServer_native_example.html b/integrationExamples/gpt/prebidServer_native_example.html new file mode 100644 index 00000000000..16c7d38a427 --- /dev/null +++ b/integrationExamples/gpt/prebidServer_native_example.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + +

Prebid Native

+
+

No response

+ +
+ +
+
+ +
+

No response

+ +
+ + + + diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 582b12e59d7..2ae32dd1df2 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -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'; @@ -346,7 +347,7 @@ const LEGACY_PROTOCOL = { return request; }, - interpretResponse(result, bidderRequests, requestedBidders) { + interpretResponse(result, bidderRequests) { const bids = []; if (result.status === 'OK' || result.status === 'no_cookie') { if (result.bidder_status) { @@ -431,11 +432,57 @@ 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 +}; + +let nativeEventTrackerEventMap = { + impression: 1, + 'viewable-mrc50': 2, + 'viewable-mrc100': 3, + 'viewable-video50': 4, +}; + +let nativeEventTrackerMethodMap = { + img: 1, + js: 2 +}; + +// enable reverse lookup +[ + nativeDataIdMap, + nativeImgIdMap, + nativeEventTrackerEventMap, + nativeEventTrackerMethodMap +].forEach(map => { + Object.keys(map).forEach(key => { + map[map[key]] = key; + }); +}); + /* * Protocol spec for OpenRTB endpoint * e.g., https:///v1/openrtb2/auction */ let bidIdMap = {}; +let nativeAssetCache = {}; // store processed native params to preserve const OPEN_RTB_PROTOCOL = { buildRequest(s2sBidRequest, bidRequests, adUnits) { let imps = []; @@ -443,6 +490,74 @@ const OPEN_RTB_PROTOCOL = { // transform ad unit into array of OpenRTB impression objects adUnits.forEach(adUnit => { + const nativeParams = processNativeAdUnitParams(utils.deepAccess(adUnit, 'mediaTypes.native')); + let nativeAssets; + if (nativeParams) { + try { + nativeAssets = nativeAssetCache[adUnit.code] = Object.keys(nativeParams).reduce((assets, type) => { + let params = nativeParams[type]; + + function newAsset(obj) { + return Object.assign({ + required: params.required ? 1 : 0 + }, obj ? utils.cleanObj(obj) : {}); + } + + switch (type) { + case 'image': + case '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 + })); + break; + case 'title': + if (!params.len) { + throw 'invalid title.len'; + } + assets.push(newAsset({ + title: { + len: params.len + } + })); + break; + default: + let dataAssetTypeId = nativeDataIdMap[type]; + if (dataAssetTypeId) { + assets.push(newAsset({ + data: { + type: dataAssetTypeId, + len: params.len + } + })) + } + } + return assets; + }, []); + } catch (e) { + utils.logError('error creating native request: ' + String(e)) + } + } + const videoParams = utils.deepAccess(adUnit, 'mediaTypes.video'); + const bannerParams = utils.deepAccess(adUnit, 'mediaTypes.banner'); + adUnit.bids.forEach(bid => { // OpenRTB response contains the adunit code and bidder name. These are // combined to create a unique key for each bid since an id isn't returned @@ -454,14 +569,13 @@ const OPEN_RTB_PROTOCOL = { } }); - let banner; + let mediaTypes = {}; // default to banner if mediaTypes isn't defined - if (utils.isEmpty(adUnit.mediaTypes)) { + if (!(nativeParams || videoParams || bannerParams)) { 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'); if (bannerParams && bannerParams.sizes) { const sizes = utils.parseSizesInput(bannerParams.sizes); @@ -473,13 +587,37 @@ 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; + } + } + + if (nativeAssets) { + try { + mediaTypes['native'] = { + request: JSON.stringify({ + // TODO: determine best way to pass these and if we allow defaults + context: 1, + plcmttype: 1, + eventtrackers: [ + {event: 1, methods: [1]} + ], + // TODO: figure out how to support privacy field + // privacy: int + assets: nativeAssets + }), + ver: '1.2' + } + } catch (e) { + utils.logError('error creating native request: ' + String(e)) + } } // get bidder params in form { : {...params} } @@ -494,16 +632,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) { @@ -609,7 +742,7 @@ const OPEN_RTB_PROTOCOL = { return request; }, - interpretResponse(response, bidderRequests, requestedBidders) { + interpretResponse(response, bidderRequests) { const bids = []; if (response.seatbid) { @@ -665,6 +798,60 @@ 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; + } + + let trackers = { + [nativeEventTrackerMethodMap.img]: adm.imptrackers || [], + [nativeEventTrackerMethodMap.js]: adm.jstracker ? [adm.jstracker] : [] + }; + if (adm.eventtrackers) { + adm.eventtrackers.forEach(tracker => { + switch (tracker.method) { + case nativeEventTrackerMethodMap.img: + trackers[nativeEventTrackerMethodMap.img].push(tracker.url); + break; + case nativeEventTrackerMethodMap.js: + trackers[nativeEventTrackerMethodMap.js].push(tracker.url); + break; + } + }); + } + + if (utils.isPlainObject(adm) && Array.isArray(adm.assets)) { + let origAssets = nativeAssetCache[bidRequest.adUnitCode]; + bidObject.native = utils.cleanObj(adm.assets.reduce((native, asset) => { + let origAsset = origAssets[asset.id]; + if (utils.isPlainObject(asset.img)) { + native[origAsset.img.type ? nativeImgIdMap[origAsset.img.type] : 'image'] = 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] === origAsset.data.type) { + native[dataType] = asset.data.value; + } + }); + } + return native; + }, utils.cleanObj({ + clickUrl: adm.link, + clickTrackers: utils.deepAccess(adm, 'link.clicktrackers'), + impressionTrackers: trackers[nativeEventTrackerMethodMap.img], + javascriptTrackers: trackers[nativeEventTrackerMethodMap.js] + }))); + } else { + utils.logError('prebid server native response contained no assets'); + } } else { // banner if (bid.adm && bid.nurl) { bidObject.ad = bid.adm; @@ -732,10 +919,13 @@ export function PrebidServer() { const adUnits = utils.deepClone(s2sBidRequest.ad_units); // at this point ad units should have a size array either directly or mapped so filter for that - const adUnitsWithSizes = adUnits.filter(unit => unit.sizes && unit.sizes.length); + const validAdUnits = adUnits.filter(unit => + (unit.sizes && unit.sizes.length) || + (unit.mediaTypes && unit.mediaTypes.native) + ); // in case config.bidders contains invalid bidders, we only process those we sent requests for - const requestedBidders = adUnitsWithSizes + const requestedBidders = validAdUnits .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(utils.uniques)) .reduce(utils.flatten) .filter(utils.uniques); @@ -745,7 +935,7 @@ export function PrebidServer() { queueSync(_s2sConfig.bidders, consent); } - const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes); + const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, validAdUnits); const requestJson = request && JSON.stringify(request); if (request && requestJson) { ajax( diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index b165741b49d..560cab91dca 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -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]; @@ -98,7 +71,7 @@ function formatSource(src) { function sendMessage(auctionId, bidWonId) { function formatBid(bid) { - return _pick(bid, [ + return utils.pick(bid, [ 'bidder', 'bidId', 'status', @@ -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', @@ -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, @@ -153,7 +126,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', @@ -191,7 +164,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 = { @@ -237,7 +210,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); @@ -251,7 +224,7 @@ export function parseBidResponse(bid) { 'dealId', 'status', 'mediaType', - 'dimensions', () => _pick(bid, [ + 'dimensions', () => utils.pick(bid, [ 'width', 'height' ]) @@ -327,7 +300,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' ]); @@ -340,7 +313,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 @@ -349,7 +322,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' @@ -380,7 +353,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } } }, - 'adUnit', () => _pick(bid, [ + 'adUnit', () => utils.pick(bid, [ 'adUnitCode', 'transactionId', 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), diff --git a/src/utils.js b/src/utils.js index a43151f89e4..6f592a8bcfe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1137,6 +1137,53 @@ export function convertCamelToUnderscore(value) { return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); } +/** + * Returns a new object with undefined properties removed from given object + * @param obj the object to clean + */ +export function cleanObj(obj) { + return Object.keys(obj).reduce((newObj, key) => { + if (typeof obj[key] !== 'undefined') { + newObj[key] = obj[key]; + } + return newObj; + }, {}) +} + +/** + * Create a new object with selected properties. Also allows property renaming and transform functions. + * @param obj the original object + * @param properties An array of desired properties + */ +export function pick(obj, properties) { + if (typeof obj !== 'object') { + return {}; + } + 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; + }, {}); +} + /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 8b077ca796a..0542385c5d5 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -33,6 +33,19 @@ const REQUEST = { 'mediaTypes': { 'banner': { 'sizes': [[ 300, 250 ], [ 300, 300 ]] + }, + 'native': { + 'title': { + 'required': true, + 'len': 800 + }, + 'image': { + 'required': true, + 'sizes': [989, 742], + }, + 'sponsoredBy': { + 'required': true + } } }, 'transactionId': '4ef956ad-fd83-406d-bd35-e4bb786ab86c', @@ -382,6 +395,93 @@ const RESPONSE_OPENRTB_VIDEO = { }, }; +const RESPONSE_OPENRTB_NATIVE = { + 'id': 'c7dcf14f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6451317310275562039', + 'impid': 'div-gpt-ad-1460505748561-0', + 'price': 10, + 'adm': { + 'ver': '1.2', + 'assets': [ + { + 'id': 1, + 'img': { + 'url': 'https://vcdn.adnxs.com/p/creative-image/f8/7f/0f/13/f87f0f13-230c-4f05-8087-db9216e393de.jpg', + 'w': 989, + 'h': 742, + 'ext': { + 'appnexus': { + 'prevent_crop': 0 + } + } + } + }, + { + 'id': 0, + 'title': { + 'text': 'This is a Prebid Native Creative' + } + }, + { + 'id': 2, + 'data': { + 'value': 'Prebid.org' + } + } + ], + 'link': { + 'url': 'https://lax1-ib.adnxs.com/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQGdce2vBWudAJZpFu1er1zA7ZzddAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALsAuhVqdgAAAAA./cpcpm=AAAAAAAAAAA=/bcr=AAAAAAAA8D8=/pp=${AUCTION_PRICE}/cnd=%213Q5HCQj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJTEFYMTo0MDc3QKcPSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAA/cca=OTMyNSNMQVgxOjQwNzc=/bn=84305/test=1/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html' + }, + 'eventtrackers': [ + { + 'event': 1, + 'method': 1, + 'url': 'https://lax1-ib.adnxs.com/it?an_audit=0&test=1&referrer=http%3A%2F%2Flocalhost%3A9999%2FintegrationExamples%2Fgpt%2Fdemo_native.html&e=wqT_3QKCCKACBAAAAwDWAAUBCLvO3ekFEOe47duW2NbzQBiltJba--rq6zAqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXjRkgWAAQGKAQNVU0SSAQEG8FKYAQGgAQGoAQGwAQC4AQLAAQPIAQLQAQnYAQDgAQHwAQCKAjt1ZignYScsIDI1Mjk4ODUsIDE1NjM5MTE5OTUpO3VmKCdyJywgOTc0OTQyMDQsIC4eAPQ0AZICnQIhb2pkaWlnajgtTHdLRUx6SnZpNFlBQ0NjOFZzd0FEZ0FRQVJJN1VoUTR0R25CbGdBWVAwQmFBQndBSGdBZ0FFQWlBRUFrQUVCbUFFQm9BRUJxQUVEc0FFQXVRSHpyV3FrQUFBa1FNRUI4NjFxcEFBQUpFREpBVVZpYmxDaFpRQkEyUUVBQUFBQUFBRHdQLUFCQVBVQkFBQUFBUGdCQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEX1BpOENyb0RDVXhCV0RFNk5EQTNOLUFEcHctUUJBQ1lCQUhCQkFBQUFBQUFBQUFBeVFRQUFBQUFBQUFBQU5nRUFBLi6aAoUBITNRNUhDUWo4LUx3S0VMeiUhJG5QRmJJQVFvQUQRvVhBa1FEb0pURUZZTVRvME1EYzNRS2NQUxFUDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQz0FwHYAgDgAq2YSOoCPmh0dHA6Ly9sb2NhbGhvc3Q6OTk5OS9pbnRlZ3JhdGlvbkV4YW1wbGVzL2dwdC9kZW1vX25hdGl2ZS5odG1sgAMAiAMBkAMAmAMUoAMBqgMAwAPgqAHIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBA0xNzMuMjQ0LjM2LjQwqATtoySyBAwIABAAGAAgADAAOAC4BADABADIBADSBA45MzI1I0xBWDE6NDA3N9oEAggB4AQA8AS8yb4uiAUBmAUAoAX___________8BqgUkZTU5YzNlYjYtNmRkNi00MmQ5LWExMWEtM2FhMTFjOTc5MGUwwAUAyQUAAAAAAADwP9IFCQkAaVh0ANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSQk8D_IBgDaBhYKEAkQGQEBwTTgBgzyBgIIAIAHAYgHAA..&s=11ababa390e9f7983de260493fc5b91ec5b1b3d4&pp=${AUCTION_PRICE}' + } + ] + }, + 'adid': '97494204', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://lax1-ib.adnxs.com/cr?id=97494204', + 'cid': '9325', + 'crid': '97494204', + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'prebid': { + 'targeting': { + 'hb_bidder': 'appnexus', + 'hb_pb': '10.00' + }, + 'type': 'native', + 'video': { + 'duration': 0, + 'primary_category': '' + } + }, + 'bidder': { + 'appnexus': { + 'brand_id': 555545, + 'auction_id': 4676806524825984103, + 'bidder_id': 2, + 'bid_ad_type': 3 + } + } + } + } + ], + 'seat': 'appnexus' + } + ] +}; + const RESPONSE_UNSUPPORTED_BIDDER = { 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', 'status': 'OK', @@ -641,7 +741,7 @@ describe('S2S Adapter', function () { }); }); - it('adds device and app objects to request for ORTB', function () { + it('adds device and app objects to request for OpenRTB', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' }); @@ -689,6 +789,54 @@ describe('S2S Adapter', function () { }); }); + it('adds native request for OpenRTB', function () { + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' + }); + + const _config = { + s2sConfig: s2sConfig + }; + + config.setConfig(_config); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(requests[0].requestBody); + + expect(requestBid.imp[0].native).to.deep.equal({ + request: JSON.stringify({ + 'context': 1, + 'plcmttype': 1, + 'eventtrackers': [{ + event: 1, + methods: [1] + }], + 'assets': [ + { + 'required': 1, + 'title': { + 'len': 800 + } + }, + { + 'required': 1, + 'img': { + 'type': 3, + 'w': 989, + 'h': 742 + } + }, + { + 'required': 1, + 'data': { + 'type': 1 + } + } + ] + }), + ver: '1.2' + }); + }); + it('adds site if app is not present', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' @@ -1404,6 +1552,33 @@ describe('S2S Adapter', function () { expect(response).to.have.property('vastUrl', 'https://prebid-cache.net/cache?uuid=a5ad3993'); }); + it('handles OpenRTB native responses', function () { + sinon.stub(utils, 'getBidRequest').returns({ + adUnitCode: 'div-gpt-ad-1460505748561-0', + bidder: 'appnexus', + bidId: '123' + }); + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: 'https://prebidserverurl/openrtb2/auction?querystring=param' + }); + config.setConfig({s2sConfig}); + + server.respondWith(JSON.stringify(RESPONSE_OPENRTB_NATIVE)); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.respond(); + + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('adm').deep.equal(RESPONSE_OPENRTB_NATIVE.seatbid[0].bid[0].adm); + expect(response).to.have.property('mediaType', 'native'); + expect(response).to.have.property('bidderCode', 'appnexus'); + expect(response).to.have.property('requestId', '123'); + expect(response).to.have.property('cpm', 10); + + utils.getBidRequest.restore(); + }); + it('should log warning for unsupported bidder', function () { server.respondWith(JSON.stringify(RESPONSE_UNSUPPORTED_BIDDER));