Skip to content

Commit

Permalink
Nativo Bid Adapter: support native and video (prebid#12134)
Browse files Browse the repository at this point in the history
* Initial nativoBidAdapter document creation (js, md and spec)

* Fulling working prebid using nativoBidAdapter. Support for GDPR and CCPA in user syncs.

* Added defult size settings based on the largest ad unit. Added response body validation. Added consent to request url qs params.

* Changed bidder endpoint url

* Changed double quotes to single quotes.

* Reverted package-json.lock to remove modifications from PR

* Added optional bidder param 'url' so the ad server can force- match an existing placement

* Lint fix. Added space after if.

* Added new QS param to send various adUnit data to adapter endpopint

* Updated unit test for new QS param

* Added qs param to keep track of ad unit refreshes

* Updated bidMap key default value

* Updated refresh increment logic

* Refactored spread operator for IE11 support

* Updated isBidRequestValid check

* Refactored Object.enties to use Object.keys to fix CircleCI testing errors

* Updated bid mapping key creation to prioritize ad unit code over placementId

* Added filtering by ad, advertiser and campaign.

* Merged master

* Added more robust bidDataMap with multiple key access

* Deduped filer values

* Rolled back package.json

* Duped upstream/master's package.lock file ... not sure how it got changed in the first place

* Small refactor of filterData length check. Removed comparison with 0 since a length value of 0 is already falsy.

* Added bid sizes to request

* Fixed function name in spec. Added unit tests.

* Added priceFloor module support

* Added protection agains empty url parameter

* Changed ntv_url QS param to use referrer.location instead of referrer.page

* Removed testing 'only' flag

* Added ntv_url QS param value validation

* Added userId support

* Added unit tests, refactored for bugs

* Wrapped ajax in try/catch

* Added more unit testing

* Updated eid check for duplicate values. Removed error logging as we no longer need it.

* Removed spec test .only. Fixed unit tests that were breaking.

* Added Prebid version to nativo exchange request

* Removed unused bidder methods

* Added OpenRTB payload response. Changes requerst type to POST.

* Removed debug log

* Added/fixed tests

* Handle video mediaType

* Add built renderer files

* Fix no-inner-declarations linting error

* Handle native requests

* Add examples in Nativo readme

* Add 'mediaType' property to tests for compatibility with adapter code

* Remove data URI from VAST XML

* Fix 'no-unused-expressions' lint error

* Fix lint error 'curcly'

* Remove bidder name validation in 'isBidRequestValid' (#4)

* Add GPP consent string to req (#5)

---------

Co-authored-by: Josh <jfledd@gmail.com>
Co-authored-by: Joshua Fledderjohn <jfledderjohn@nativo.com>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 1205401 commit b60d732
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 58 deletions.
110 changes: 79 additions & 31 deletions modules/nativoBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { deepAccess, isEmpty } from '../src/utils.js'
import { registerBidder } from '../src/adapters/bidderFactory.js'
import { BANNER } from '../src/mediaTypes.js'
import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'
import { getGlobal } from '../src/prebidGlobal.js'
import { ortbConverter } from '../libraries/ortbConverter/converter.js'

const converter = ortbConverter({
context: {
// `netRevenue` and `ttl` are required properties of bid responses - provide a default for them
netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false
ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp)
ttl: 30, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp)
},
imp(buildImp, bidRequest, context) {
const imp = buildImp(bidRequest, context);
const imp = buildImp(bidRequest, context)
imp.tagid = bidRequest.adUnitCode
return imp;
}
});
if (imp.ext) imp.ext.placementId = bidRequest.params.placementId

return imp
},
})

const BIDDER_CODE = 'nativo'
const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid'
Expand All @@ -24,12 +26,22 @@ const GVLID = 263

const TIME_TO_LIVE = 360

const SUPPORTED_AD_TYPES = [BANNER]
const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE]
const FLOOR_PRICE_CURRENCY = 'USD'
const PRICE_FLOOR_WILDCARD = '*'

const localPbjsRef = getGlobal()

function getMediaType(accessObj) {
if (deepAccess(accessObj, 'mediaTypes.video')) {
return VIDEO
} else if (deepAccess(accessObj, 'mediaTypes.native')) {
return NATIVE
} else {
return BANNER
}
}

/**
* Keep track of bid data by keys
* @returns {Object} - Map of bid data that can be referenced by multiple keys
Expand Down Expand Up @@ -122,8 +134,7 @@ export const spec = {
*/
isBidRequestValid: function (bid) {
// We don't need any specific parameters to make a bid request
// If not parameters are supplied just verify it's the correct bidder code
if (!bid.params) return bid.bidder === BIDDER_CODE
if (!bid.params) return true

// Check if any supplied parameters are invalid
const hasInvalidParameters = Object.keys(bid.params).some((key) => {
Expand All @@ -150,7 +161,10 @@ export const spec = {
*/
buildRequests: function (validBidRequests, bidderRequest) {
// Get OpenRTB Data
const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest})
const openRTBData = converter.toORTB({
bidRequests: validBidRequests,
bidderRequest,
})
const openRTBDataString = JSON.stringify(openRTBData)

const requestData = new RequestData()
Expand Down Expand Up @@ -201,7 +215,8 @@ export const spec = {
let params = [
// Prebid version
{
key: 'ntv_pbv', value: localPbjsRef.version
key: 'ntv_pbv',
value: localPbjsRef.version,
},
// Prebid request id
{ key: 'ntv_pb_rid', value: bidderRequest.bidderRequestId },
Expand Down Expand Up @@ -278,19 +293,31 @@ export const spec = {
})
}

// Add GPP params
if (bidderRequest.gppConsent) {
params.unshift({
key: 'ntv_gpp_consent',
value: bidderRequest.gppConsent.gppString,
})
}

// Add USP params
if (bidderRequest.uspConsent) {
// Put on the beginning of the qs param array
params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent })
}

const qsParamStrings = [requestData.getRequestDataQueryString(), arrayToQS(params)]
const qsParamStrings = [
requestData.getRequestDataQueryString(),
arrayToQS(params),
]
const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings)

let serverRequest = {
method: 'POST',
url: requestUrl,
data: openRTBDataString,
bidderRequest: bidderRequest,
}

return serverRequest
Expand Down Expand Up @@ -320,9 +347,10 @@ export const spec = {

// Step through and grab pertinent data
let bidResponse, adUnit
seatbids.forEach((seatbid) => {
seatbids.forEach((seatbid, i) => {
seatbid.bid.forEach((bid) => {
adUnit = this.getAdUnitData(body.id, bid)

bidResponse = {
requestId: adUnit.bidId,
cpm: bid.price,
Expand All @@ -337,10 +365,18 @@ export const spec = {
meta: {
advertiserDomains: bid.adomain,
},
mediaType: getMediaType(request.bidderRequest.bids[i]),
}

if (bid.ext) extData[bid.id] = bid.ext

if (bidResponse.mediaType === VIDEO) {
bidResponse.vastUrl = bid.adm
}
if (bidResponse.mediaType === NATIVE) {
bidResponse.native = {
ortb: JSON.parse(bidResponse.ad),
}
}
bidResponses.push(bidResponse)
})
})
Expand Down Expand Up @@ -414,23 +450,27 @@ export const spec = {
typeof response.body === 'string'
? JSON.parse(response.body)
: response.body
} catch (err) { return }
} catch (err) {
return
}

// Make sure we have valid content
if (!body || !body.seatbid || body.seatbid.length === 0) return

body.seatbid.forEach((seatbid) => {
// Grab the syncs for each seatbid
seatbid.syncUrls.forEach((sync) => {
if (types[sync.type]) {
if (sync.url.trim() !== '') {
syncs.push({
type: sync.type,
url: sync.url.replace('{GDPR_params}', params),
})
if (seatbid.syncUrls) {
seatbid.syncUrls.forEach((sync) => {
if (types[sync.type]) {
if (sync.url.trim() !== '') {
syncs.push({
type: sync.type,
url: sync.url.replace('{GDPR_params}', params),
})
}
}
}
})
})
}
})
})

Expand Down Expand Up @@ -491,7 +531,9 @@ export class RequestData {
getRequestDataQueryString() {
if (this.bidRequestDataSources.length == 0) return

const queryParams = this.bidRequestDataSources.map(dataSource => dataSource.getRequestQueryString()).filter(queryString => queryString !== '')
const queryParams = this.bidRequestDataSources
.map((dataSource) => dataSource.getRequestQueryString())
.filter((queryString) => queryString !== '')
return queryParams.join('&')
}
}
Expand All @@ -500,8 +542,10 @@ export class BidRequestDataSource {
constructor() {
this.type = 'BidRequestDataSource'
}
processBidRequestData(bidRequest, bidderRequest) { }
getRequestQueryString() { return '' }
processBidRequestData(bidRequest, bidderRequest) {}
getRequestQueryString() {
return ''
}
}

export class UserEIDs extends BidRequestDataSource {
Expand Down Expand Up @@ -540,7 +584,7 @@ QueryStringParam.prototype.toString = function () {
export function encodeToBase64(value) {
try {
return btoa(JSON.stringify(value))
} catch (err) { }
} catch (err) {}
}

export function parseFloorPriceData(bidRequest) {
Expand Down Expand Up @@ -708,9 +752,13 @@ function getLargestSize(sizes, method = area) {
* Build the final request url
*/
export function buildRequestUrl(baseUrl, qsParamStringArray = []) {
if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) return baseUrl
if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) {
return baseUrl
}

const nonEmptyQSParamStrings = qsParamStringArray.filter(qsParamString => qsParamString.trim() !== '')
const nonEmptyQSParamStrings = qsParamStringArray.filter(
(qsParamString) => qsParamString.trim() !== ''
)

if (nonEmptyQSParamStrings.length === 0) return baseUrl

Expand Down Expand Up @@ -752,7 +800,7 @@ export function getPageUrlFromBidRequest(bidRequest) {
try {
const url = new URL(paramPageUrl)
return url.href
} catch (err) { }
} catch (err) {}
}

export function hasProtocol(url) {
Expand Down
101 changes: 84 additions & 17 deletions modules/nativoBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,91 @@ gulp serve --modules=nativoBidAdapter

# Test Parameters

## Banner

```js
var adUnits = [
{
code: 'div-gpt-ad-1460505748561-0',
mediaTypes: {
banner: {
sizes: [
[300, 250],
[300, 600],
],
},
},
// Replace this object to test a new Adapter!
bids: [
{
bidder: 'nativo',
params: {
url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html',
},
},
],
},
]
```

## Video

```js
var adUnits = [
{
code: 'div-gpt-ad-1460505748561-0',
mediaTypes: {
banner: {
sizes: [[300, 250], [300,600]],
}
},
// Replace this object to test a new Adapter!
bids: [{
bidder: 'nativo',
params: {
url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html'
}
}]
}
];
{
code: 'ntvPlaceholder-1',
mediaTypes: {
video: {
mimes: ['video/mp4'],
protocols: [2, 3, 5, 6],
playbackmethod: [1, 2],
skip: 1,
skipafter: 5,
},
},
video: {
divId: 'player',
},
bids: [
{
bidder: 'nativo',
params: {
url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html',
},
},
],
},
]
```

## Native

```js
var adUnits = [
{
code: '/416881364/prebid-native-test-unit',
sizes: [[300, 250]],
mediaTypes: {
native: {
title: {
required: true,
},
image: {
required: true,
},
sponsoredBy: {
required: true,
},
},
},
bids: [
{
bidder: 'nativo',
params: {
url: 'https://test-sites.internal.nativo.net/testing/prebid_adpater.html',
},
},
],
},
]
```
Loading

0 comments on commit b60d732

Please sign in to comment.