Skip to content

Commit

Permalink
Merge pull request #6 from AdRoll/native_support
Browse files Browse the repository at this point in the history
Add native support
  • Loading branch information
abijr authored Jun 9, 2020
2 parents b404d20 + 69746bb commit 96f6002
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 11 deletions.
159 changes: 149 additions & 10 deletions modules/nextrollBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import * as utils from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER } from '../src/mediaTypes.js';
import { BANNER, NATIVE } from '../src/mediaTypes.js';

import find from 'core-js-pure/features/array/find.js';

const BIDDER_CODE = 'nextroll';
const BIDDER_ENDPOINT = 'https://d.adroll.com/bid/prebid/';
const ADAPTER_VERSION = 4;
const ADAPTER_VERSION = 5;

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER],
supportedMediaTypes: [BANNER, NATIVE],

/**
* Determines whether or not the given bid request is valid.
Expand Down Expand Up @@ -43,9 +43,8 @@ export const spec = {
imp: {
id: bidRequest.bidId,
bidfloor: utils.getBidIdParameter('bidfloor', bidRequest.params),
banner: {
format: _getSizes(bidRequest)
},
banner: _getBanner(bidRequest),
native: _getNative(utils.deepAccess(bidRequest, 'mediaTypes.native')),
ext: {
zone: {
id: utils.getBidIdParameter('zoneId', bidRequest.params)
Expand Down Expand Up @@ -82,6 +81,98 @@ export const spec = {
}
}

function _getBanner(bidRequest) {
let sizes = _getSizes(bidRequest)
if (sizes === undefined) return undefined
return {format: sizes}
}

function _getNative(mediaTypeNative) {
if (mediaTypeNative === undefined) return undefined
let assets = _getNativeAssets(mediaTypeNative)
if (assets === undefined || assets.length == 0) return undefined
return {
request: {
native: {
assets: assets
}
}
}
}

/*
id: Unique numeric id for the asset
kind: OpenRTB kind of asset. Supported: title, img and data.
key: Name of property that comes in the mediaType.native object.
type: OpenRTB type for that spefic kind of asset.
required: Overrides the asset required field configured, only overrides when is true.
*/
const NATIVE_ASSET_MAP = [
{id: 1, kind: 'title', key: 'title', required: true},
{id: 2, kind: 'img', key: 'image', type: 3, required: true},
{id: 3, kind: 'img', key: 'icon', type: 1},
{id: 4, kind: 'img', key: 'logo', type: 2},
{id: 5, kind: 'data', key: 'sponsoredBy', type: 1},
{id: 6, kind: 'data', key: 'body', type: 2}
]

const ASSET_KIND_MAP = {
title: _getTitleAsset,
img: _getImageAsset,
data: _getDataAsset,
}

function _getAsset(mediaTypeNative, assetMap) {
let asset = mediaTypeNative[assetMap.key]
if (asset === undefined) return undefined
let assetFunc = ASSET_KIND_MAP[assetMap.kind]
return {
id: assetMap.id,
required: (assetMap.required || !!asset.required) ? 1 : 0,
[assetMap.kind]: assetFunc(asset, assetMap)
}
}

function _getTitleAsset(title, _assetMap) {
return {len: title.len || 0}
}

function _getMinAspectRatio(aspectRatio, property) {
if (!utils.isPlainObject(aspectRatio)) return 1

let ratio = aspectRatio['ratio_' + property]
let min = aspectRatio['min_' + property]

if (utils.isNumber(ratio)) return ratio
if (utils.isNumber(min)) return min

return 1
}

function _getImageAsset(image, assetMap) {
let sizes = image.sizes
let aspectRatio = image.aspect_ratios ? image.aspect_ratios[0] : undefined

return {
type: assetMap.type,
w: (sizes ? sizes[0] : undefined),
h: (sizes ? sizes[1] : undefined),
wmin: _getMinAspectRatio(aspectRatio, 'width'),
hmin: _getMinAspectRatio(aspectRatio, 'height'),
}
}

function _getDataAsset(data, assetMap) {
return {
type: assetMap.type,
len: data.len || 0
}
}

function _getNativeAssets(mediaTypeNative) {
return NATIVE_ASSET_MAP.map(assetMap => _getAsset(mediaTypeNative, assetMap)).filter(asset => asset !== undefined)
}

function _getUser(requests) {
let id = utils.deepAccess(requests, '0.userId.nextroll');
if (id === undefined) {
Expand All @@ -99,8 +190,7 @@ function _getUser(requests) {
}

function _buildResponse(bidResponse, bid) {
const adm = utils.replaceAuctionPrice(bid.adm, bid.price);
return {
let response = {
requestId: bidResponse.id,
cpm: bid.price,
width: bid.w,
Expand All @@ -109,8 +199,53 @@ function _buildResponse(bidResponse, bid) {
dealId: bidResponse.dealId,
currency: 'USD',
netRevenue: true,
ttl: 300,
ad: adm
ttl: 300
}
if (utils.isStr(bid.adm)) {
response.mediaType = BANNER
response.ad = utils.replaceAuctionPrice(bid.adm, bid.price)
} else {
response.mediaType = NATIVE
response.native = _getNativeResponse(bid.adm, bid.price)
}
return response
}

const privacyLink = 'https://info.evidon.com/pub_info/573';
const privacyIcon = 'https://c.betrad.com/pub/icon1.png';

function _getNativeResponse(adm, price) {
let baseResponse = {
clickTrackers: (adm.link && adm.link.clicktrackers) || [],
jstracker: adm.jstracker || [],
clickUrl: utils.replaceAuctionPrice(adm.link.url, price),
impressionTrackers: adm.imptrackers.map(impTracker => utils.replaceAuctionPrice(impTracker, price)),
privacyLink: privacyLink,
privacyIcon: privacyIcon
}
return adm.assets.reduce((accResponse, asset) => {
let assetMaps = NATIVE_ASSET_MAP.filter(assetMap => assetMap.id === asset.id && asset[assetMap.kind] !== undefined)
if (assetMaps.length === 0) return accResponse
let assetMap = assetMaps[0]
accResponse[assetMap.key] = _getAssetResponse(asset, assetMap)
return accResponse
}, baseResponse)
}

function _getAssetResponse(asset, assetMap) {
switch (assetMap.kind) {
case 'title':
return asset.title.text

case 'img':
return {
url: asset.img.url,
width: asset.img.w,
height: asset.img.h
}

case 'data':
return asset.data.value
}
}

Expand All @@ -131,6 +266,9 @@ function _getSeller(bidRequest) {
}

function _getSizes(bidRequest) {
if (!utils.isArray(bidRequest.sizes)) {
return undefined
}
return bidRequest.sizes.filter(_isValidSize).map(size => {
return {
w: size[0],
Expand Down Expand Up @@ -191,6 +329,7 @@ function _getOsVersion(userAgent) {
}

export function hasCCPAConsent(bidderRequest) {
if (bidderRequest === undefined) return true;
if (typeof bidderRequest.uspConsent !== 'string') {
return true;
}
Expand Down
29 changes: 28 additions & 1 deletion modules/nextrollBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ Maintainer: prebid@nextroll.com
# Description

Module that connects to NextRoll's bidders.
The NextRoll bid adapter supports Banner format only.
The NextRoll bid adapter supports banner and native format.

# Test Parameters

## Banner Example
``` javascript
var adUnits = [
{
Expand Down Expand Up @@ -47,4 +49,29 @@ var adUnits = [
}]
}
]
```

## Native Example
```javascript
var adUnits = [
{
code: 'div-1',
mediaTypes: {
native: {
title: { required: true, len: 80 },
image: { required: true, sizes: [728, 90] },
sponsoredBy: { required: false, len: 20 }
}
},
bids: [{
bidder: 'nextroll',
params: {
bidfloor: 1,
zoneId: "13144370",
publisherId: "publisherId",
sellerId: "sellerId",
}
}]
}
];
```
117 changes: 117 additions & 0 deletions test/spec/modules/nextrollBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,44 @@ describe('nextrollBidAdapter', function() {
let bidWithoutValidId = { id: '' };
let bidWithoutId = { params: { zoneId: 'zone1' } };

describe('nativeBidRequest', () => {
it('validates native spec', () => {
let nativeAdUnit = [{
bidder: 'nextroll',
adUnitCode: 'adunit-code',
bidId: 'bid_id',
mediaTypes: {
native: {
title: {required: true, len: 80},
image: {required: true, sizes: [728, 90]},
sponsoredBy: {required: false, len: 20},
clickUrl: {required: true},
body: {required: true, len: 25},
icon: {required: true, sizes: [50, 50], aspect_ratios: [{ratio_height: 3, ratio_width: 4}]},
someRandomAsset: {required: false, len: 100} // This should be ignored
}
},
params: {
bidfloor: 1,
zoneId: 'zone1',
publisherId: 'publisher_id'
}
}];

let request = spec.buildRequests(nativeAdUnit)
let assets = request[0].data.imp.native.request.native.assets

let excptedAssets = [
{id: 1, required: 1, title: {len: 80}},
{id: 2, required: 1, img: {w: 728, h: 90, wmin: 1, hmin: 1, type: 3}},
{id: 3, required: 1, img: {w: 50, h: 50, wmin: 4, hmin: 3, type: 1}},
{id: 5, required: 0, data: {len: 20, type: 1}},
{id: 6, required: 1, data: {len: 25, type: 2}}
]
expect(assets).to.be.deep.equal(excptedAssets)
})
})

describe('isBidRequestValid', function() {
it('validates the bids correctly when the bid has an id', function() {
expect(spec.isBidRequestValid(validBid)).to.be.true;
Expand Down Expand Up @@ -142,6 +180,85 @@ describe('nextrollBidAdapter', function() {
});
});

describe('interpret native response', () => {
let clickUrl = 'https://clickurl.com/with/some/path'
let titleText = 'Some title'
let imgW = 300
let imgH = 250
let imgUrl = 'https://clickurl.com/img.png'
let brandText = 'Some Brand'
let impUrl = 'https://clickurl.com/imptracker'

let responseBody = {
body: {
id: 'bidresponse_id',
seatbid: [{
bid: [{
price: 1.2,
crid: 'crid1',
adm: {
link: {url: clickUrl},
assets: [
{id: 1, title: {text: titleText}},
{id: 2, img: {w: imgW, h: imgH, url: imgUrl}},
{id: 5, data: {value: brandText}}
],
imptrackers: [impUrl]
}
}]
}]
}
};

it('Should interpret response', () => {
let response = spec.interpretResponse(utils.deepClone(responseBody))
let expectedResponse = {
clickUrl: clickUrl,
impressionTrackers: [impUrl],
privacyLink: 'https://info.evidon.com/pub_info/573',
privacyIcon: 'https://c.betrad.com/pub/icon1.png',
title: titleText,
image: {url: imgUrl, width: imgW, height: imgH},
sponsoredBy: brandText,
clickTrackers: [],
jstracker: []
}

expect(response[0].native).to.be.deep.equal(expectedResponse)
})

it('Should interpret all assets', () => {
let allAssetsResponse = utils.deepClone(responseBody)
let iconUrl = imgUrl + '?icon=true', iconW = 10, iconH = 15
let logoUrl = imgUrl + '?logo=true', logoW = 20, logoH = 25
let bodyText = 'Some body text'

allAssetsResponse.body.seatbid[0].bid[0].adm.assets.push(...[
{id: 3, img: {w: iconW, h: iconH, url: iconUrl}},
{id: 4, img: {w: logoW, h: logoH, url: logoUrl}},
{id: 6, data: {value: bodyText}}
])

let response = spec.interpretResponse(allAssetsResponse)
let expectedResponse = {
clickUrl: clickUrl,
impressionTrackers: [impUrl],
jstracker: [],
clickTrackers: [],
privacyLink: 'https://info.evidon.com/pub_info/573',
privacyIcon: 'https://c.betrad.com/pub/icon1.png',
title: titleText,
image: {url: imgUrl, width: imgW, height: imgH},
icon: {url: iconUrl, width: iconW, height: iconH},
logo: {url: logoUrl, width: logoW, height: logoH},
body: bodyText,
sponsoredBy: brandText
}

expect(response[0].native).to.be.deep.equal(expectedResponse)
})
})

describe('hasCCPAConsent', function() {
function ccpaRequest(consentString) {
return {
Expand Down

0 comments on commit 96f6002

Please sign in to comment.