Skip to content

Commit

Permalink
Nativo Bid Adapter: Adding UserId support (#9767)
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
  • Loading branch information
jsfledd authored Apr 17, 2023
1 parent ec76c84 commit 0d1af3b
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 21 deletions.
131 changes: 110 additions & 21 deletions modules/nativoBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deepAccess, isEmpty } from '../src/utils.js'
import { registerBidder } from '../src/adapters/bidderFactory.js'
import { BANNER } from '../src/mediaTypes.js'
import { getGlobal } from '../src/prebidGlobal.js'
// import { config } from 'src/config'

const BIDDER_CODE = 'nativo'
Expand All @@ -14,6 +15,8 @@ const SUPPORTED_AD_TYPES = [BANNER]
const FLOOR_PRICE_CURRENCY = 'USD'
const PRICE_FLOOR_WILDCARD = '*'

const localPbjsRef = getGlobal()

/**
* Keep track of bid data by keys
* @returns {Object} - Map of bid data that can be referenced by multiple keys
Expand Down Expand Up @@ -133,6 +136,9 @@ export const spec = {
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: function (validBidRequests, bidderRequest) {
const requestData = new RequestData()
requestData.addBidRequestDataSource(new UserEIDs())

// Parse values from bid requests
const placementIds = new Set()
const bidDataMap = BidDataMap()
Expand Down Expand Up @@ -166,6 +172,8 @@ export const spec = {
if (bidRequestFloorPriceData) {
floorPriceData[bidRequest.adUnitCode] = bidRequestFloorPriceData
}

requestData.processBidRequestData(bidRequest, bidderRequest)
})
bidRequestMap[bidderRequest.bidderRequestId] = bidDataMap

Expand All @@ -174,6 +182,10 @@ export const spec = {

// Build basic required QS Params
let params = [
// Prebid version
{
key: 'ntv_pbv', value: localPbjsRef.version
},
// Prebid request id
{ key: 'ntv_pb_rid', value: bidderRequest.bidderRequestId },
// Ad unit data
Expand Down Expand Up @@ -255,9 +267,12 @@ export const spec = {
params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent })
}

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

let serverRequest = {
method: 'GET',
url: BIDDER_ENDPOINT + arrayToQS(params),
url: requestUrl
}

return serverRequest
Expand Down Expand Up @@ -404,13 +419,6 @@ export const spec = {
return syncs
},

/**
* Will be called when an adpater timed out for an auction.
* Adapter can fire a ajax or pixel call to register a timeout at thier end.
* @param {Object} timeoutData - Timeout specific data
*/
onTimeout: function (timeoutData) {},

/**
* Will be called when a bid from the adapter won the auction.
* @param {Object} bid - The bid that won the auction
Expand All @@ -425,12 +433,6 @@ export const spec = {
appendFilterData(campaignsToFilter, ext.campaignsToFilter)
},

/**
* Will be called when the adserver targeting has been set for a bid from the adapter.
* @param {Object} bidder - The bid of which the targeting has been set
*/
onSetTargeting: function (bid) {},

/**
* Maps Prebid's bidId to Nativo's placementId values per unique bidderRequestId
* @param {String} bidderRequestId - The unique ID value associated with the bidderRequest
Expand All @@ -451,6 +453,78 @@ export const spec = {
registerBidder(spec)

// Utils
export class RequestData {
constructor() {
this.bidRequestDataSources = []
}

addBidRequestDataSource(bidRequestDataSource) {
if (!(bidRequestDataSource instanceof BidRequestDataSource)) return

this.bidRequestDataSources.push(bidRequestDataSource)
}

processBidRequestData(bidRequest, bidderRequest) {
for (let bidRequestDataSource of this.bidRequestDataSources) {
bidRequestDataSource.processBidRequestData(bidRequest, bidderRequest)
}
}

getRequestDataQueryString() {
if (this.bidRequestDataSources.length == 0) return

const queryParams = this.bidRequestDataSources.map(dataSource => dataSource.getRequestQueryString()).filter(queryString => queryString !== '')
return queryParams.join('&')
}
}

export class BidRequestDataSource {
constructor() {
this.type = 'BidRequestDataSource'
}
processBidRequestData(bidRequest, bidderRequest) { }
getRequestQueryString() { return '' }
}

export class UserEIDs extends BidRequestDataSource {
constructor() {
super()
this.type = 'UserEIDs'
this.qsParam = new QueryStringParam('ntv_pb_eid')
this.eids = []
}

processBidRequestData(bidRequest, bidderRequest) {
if (bidRequest.userIdAsEids === undefined || this.eids.length > 0) return
this.eids = bidRequest.userIdAsEids
}

getRequestQueryString() {
if (this.eids.length === 0) return ''

const encodedValueArray = encodeToBase64(this.eids)
this.qsParam.value = encodedValueArray
return this.qsParam.toString()
}
}

export class QueryStringParam {
constructor(key, value) {
this.key = key
this.value = value
}
}

QueryStringParam.prototype.toString = function () {
return `${this.key}=${this.value}`
}

export function encodeToBase64(value) {
try {
return btoa(JSON.stringify(value))
} catch (err) { }
}

export function parseFloorPriceData(bidRequest) {
if (typeof bidRequest.getFloor !== 'function') return

Expand Down Expand Up @@ -589,12 +663,9 @@ function appendQSParamString(str, key, value) {
* @returns
*/
function arrayToQS(arr) {
return (
'?' +
arr.reduce((value, obj) => {
return appendQSParamString(value, obj.key, obj.value)
}, '')
)
return arr.reduce((value, obj) => {
return appendQSParamString(value, obj.key, obj.value)
}, '')
}

/**
Expand All @@ -615,6 +686,24 @@ function getLargestSize(sizes, method = area) {
})
}

/**
* Build the final request url
*/
export function buildRequestUrl(baseUrl, qsParamStringArray = []) {
if (qsParamStringArray.length === 0 || !Array.isArray(qsParamStringArray)) return baseUrl

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

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

let requestUrl = `${baseUrl}?${nonEmptyQSParamStrings[0]}`
for (let i = 1; i < nonEmptyQSParamStrings.length; i++) {
requestUrl += `&${nonEmptyQSParamStrings[i]}`
}

return requestUrl
}

/**
* Calculate the area
* @param {Array} size - [width, height]
Expand Down Expand Up @@ -645,7 +734,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: 101 additions & 0 deletions test/spec/modules/nativoBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
getPageUrlFromBidRequest,
hasProtocol,
addProtocol,
BidRequestDataSource,
RequestData,
UserEIDs,
buildRequestUrl,
} from '../../../modules/nativoBidAdapter'

describe('bidDataMap', function () {
Expand Down Expand Up @@ -120,6 +124,7 @@ describe('nativoBidAdapterTests', function () {
expect(request.url).to.be.a('string')

expect(request.url).to.include('?')
expect(request.url).to.include('ntv_pbv')
expect(request.url).to.include('ntv_ptd')
expect(request.url).to.include('ntv_pb_rid')
expect(request.url).to.include('ntv_ppc')
Expand Down Expand Up @@ -731,3 +736,99 @@ describe('getPageUrlFromBidRequest', () => {
expect(url).not.to.be.undefined
})
})

describe('RequestData', () => {
describe('addBidRequestDataSource', () => {
it('Adds a BidRequestDataSource', () => {
const requestData = new RequestData()
const testBidRequestDataSource = new BidRequestDataSource()

requestData.addBidRequestDataSource(testBidRequestDataSource)

expect(requestData.bidRequestDataSources.length == 1)
})

it("Doeasn't add a non BidRequestDataSource", () => {
const requestData = new RequestData()

requestData.addBidRequestDataSource({})
requestData.addBidRequestDataSource('test')
requestData.addBidRequestDataSource(1)
requestData.addBidRequestDataSource(true)

expect(requestData.bidRequestDataSources.length == 0)
})
})

describe('getRequestDataString', () => {
it("Doesn't append empty query strings", () => {
const requestData = new RequestData()
const testBidRequestDataSource = new BidRequestDataSource()

requestData.addBidRequestDataSource(testBidRequestDataSource)

let qs = requestData.getRequestDataQueryString()
expect(qs).to.be.empty

testBidRequestDataSource.getRequestQueryString = () => {
return 'ntv_test=true'
}
qs = requestData.getRequestDataQueryString()
expect(qs).to.be.equal('ntv_test=true')
})
})
})

describe('UserEIDs', () => {
const userEids = new UserEIDs()
const eids = [{ 'testId': 1111 }]

describe('processBidRequestData', () => {
it('Processes bid request without eids', () => {
userEids.processBidRequestData({})

expect(userEids.eids).to.be.empty
})

it('Processed bid request with eids', () => {
userEids.processBidRequestData({ userIdAsEids: eids })

expect(userEids.eids).to.not.be.empty
})
})

describe('getRequestQueryString', () => {
it('Correctly prints out QS param string', () => {
const qs = userEids.getRequestQueryString()
const value = qs.slice(11)

expect(qs).to.include('ntv_pb_eid=')
try {
expect(JSON.parse(value)).to.be.equal(eids)
} catch (err) { }
})
})
})

describe('buildRequestUrl', () => {
const baseUrl = 'https://www.testExchange.com'
it('Returns baseUrl if no QS strings passed', () => {
const url = buildRequestUrl(baseUrl)
expect(url).to.be.equal(baseUrl)
})

it('Returns baseUrl if empty QS strings passed', () => {
const url = buildRequestUrl(baseUrl, ['', '', ''])
expect(url).to.be.equal(baseUrl)
})

it('Returns baseUrl + QS params if QS strings passed', () => {
const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', 'ntv_foo=bar'])
expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`)
})

it('Returns baseUrl + QS params if mixed QS strings passed', () => {
const url = buildRequestUrl(baseUrl, ['ntv_ptd=123456&ntv_test=true', '', '', 'ntv_foo=bar'])
expect(url).to.be.equal(`${baseUrl}?ntv_ptd=123456&ntv_test=true&ntv_foo=bar`)
})
})

0 comments on commit 0d1af3b

Please sign in to comment.