Skip to content

Commit

Permalink
Holid Bid Adapter: initial release (prebid#9371)
Browse files Browse the repository at this point in the history
* Holid bid adapter

* Adjust test to various device sizes

* Include first party data from ortb2 object

* Remove trailing spaces in test
  • Loading branch information
kdesput authored and JacobKlein26 committed Feb 8, 2023
1 parent 2a862fc commit 1c5d0fe
Show file tree
Hide file tree
Showing 3 changed files with 371 additions and 0 deletions.
170 changes: 170 additions & 0 deletions modules/holidBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
deepAccess,
getBidIdParameter,
isStr,
logMessage,
triggerPixel,
} from '../src/utils.js'
import * as events from '../src/events.js'
import CONSTANTS from '../src/constants.json'
import { BANNER } from '../src/mediaTypes.js'

import { registerBidder } from '../src/adapters/bidderFactory.js'

const BIDDER_CODE = 'holid'
const GVLID = 1177
const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction'
const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html'
const TIME_TO_LIVE = 300
let wurlMap = {}

events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler)

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

isBidRequestValid: function (bid) {
return !!bid.params.adUnitID
},

buildRequests: function (validBidRequests, _bidderRequest) {
return validBidRequests.map((bid) => {
const requestData = {
...bid.ortb2,
id: bid.auctionId,
imp: [getImp(bid)],
}

return {
method: 'POST',
url: ENDPOINT,
data: JSON.stringify(requestData),
bidId: bid.bidId,
}
})
},

interpretResponse: function (serverResponse, bidRequest) {
const bidResponses = []

if (!serverResponse.body.seatbid) {
return []
}

serverResponse.body.seatbid.map((response) => {
response.bid.map((bid) => {
const requestId = bidRequest.bidId
const auctionId = bidRequest.auctionId
const wurl = deepAccess(bid, 'ext.prebid.events.win')
const bidResponse = {
requestId,
cpm: bid.price,
width: bid.w,
height: bid.h,
ad: bid.adm,
creativeId: bid.crid,
currency: serverResponse.body.cur,
netRevenue: true,
ttl: TIME_TO_LIVE,
}

addWurl({ auctionId, requestId, wurl })

bidResponses.push(bidResponse)
})
})

return bidResponses
},

getUserSyncs(optionsType, serverResponse, gdprConsent, uspConsent) {
if (!serverResponse || serverResponse.length === 0) {
return []
}

const syncs = []

if (optionsType.iframeEnabled) {
const queryParams = []

queryParams.push('bidders=' + getBidders(serverResponse))
queryParams.push('gdpr=' + +gdprConsent.gdprApplies)
queryParams.push('gdpr_consent=' + gdprConsent.consentString)
queryParams.push('usp_consent=' + (uspConsent || ''))

let strQueryParams = queryParams.join('&')

if (strQueryParams.length > 0) {
strQueryParams = '?' + strQueryParams
}

syncs.push({
type: 'iframe',
url: COOKIE_SYNC_ENDPOINT + strQueryParams + '&type=iframe',
})

return syncs
}
},
}

function getImp(bid) {
const imp = {
ext: {
prebid: {
storedrequest: {
id: getBidIdParameter('adUnitID', bid.params),
},
},
},
}
const sizes =
bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes

if (deepAccess(bid, 'mediaTypes.banner')) {
imp.banner = {
format: sizes.map((size) => {
return { w: size[0], h: size[1] }
}),
}
}

return imp
}

function getBidders(serverResponse) {
const bidders = serverResponse
.map((res) => Object.keys(res.body.ext.responsetimemillis))
.flat(1)

return encodeURIComponent(JSON.stringify([...new Set(bidders)]))
}

function addWurl(auctionId, adId, wurl) {
if ([auctionId, adId].every(isStr)) {
wurlMap[`${auctionId}${adId}`] = wurl
}
}

function removeWurl(auctionId, adId) {
delete wurlMap[`${auctionId}${adId}`]
}

function getWurl(auctionId, adId) {
if ([auctionId, adId].every(isStr)) {
return wurlMap[`${auctionId}${adId}`]
}
}

function bidWonHandler(bid) {
const wurl = getWurl(bid.auctionId, bid.adId)
if (wurl) {
logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`)
triggerPixel(wurl)
removeWurl(bid.auctionId, bid.adId)
}
}

registerBidder(spec)
36 changes: 36 additions & 0 deletions modules/holidBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Overview

```
Module Name: Holid Bid Adapter
Module Type: Bidder Adapter
Maintainer: richard@holid.se
```

# Description

Currently module supports only banner mediaType.

# Test Parameters

## Sample Banner Ad Unit

```js
var adUnits = [
{
code: 'bannerAdUnit',
mediaTypes: {
banner: {
sizes: [[300, 250]],
},
},
bids: [
{
bidder: 'holid',
params: {
adUnitID: '12345',
},
},
],
},
]
```
165 changes: 165 additions & 0 deletions test/spec/modules/holidBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { expect } from 'chai'
import { spec } from 'modules/holidBidAdapter.js'

describe('holidBidAdapterTests', () => {
const bidRequestData = {
bidder: 'holid',
adUnitCode: 'test-div',
bidId: 'bid-id',
auctionId: 'test-id',
params: { adUnitID: '12345' },
mediaTypes: { banner: {} },
sizes: [[300, 250]],
ortb2: {
site: {
publisher: {
domain: 'https://foo.bar',
}
},
regs: {
gdpr: 1,
},
user: {
ext: {
consent: 'G4ll0p1ng_Un1c0rn5',
}
},
device: {
h: 410,
w: 1860,
}
}
}

describe('isBidRequestValid', () => {
const bid = JSON.parse(JSON.stringify(bidRequestData))

it('should return true', () => {
expect(spec.isBidRequestValid(bid)).to.equal(true)
})

it('should return false when required params are not passed', () => {
const bid = JSON.parse(JSON.stringify(bidRequestData))
delete bid.params.adUnitID

expect(spec.isBidRequestValid(bid)).to.equal(false)
})
})

describe('buildRequests', () => {
const bid = JSON.parse(JSON.stringify(bidRequestData))
const request = spec.buildRequests([bid], bid)
const payload = JSON.parse(request[0].data)

it('should include ext in imp', () => {
expect(payload.imp[0].ext).to.exist
expect(payload.imp[0].ext).to.deep.equal({
prebid: { storedrequest: { id: '12345' } },
})
})

it('should include banner format in imp', () => {
expect(payload.imp[0].banner).to.exist
expect(payload.imp[0].banner).to.deep.equal({
format: [{ w: 300, h: 250 }],
})
})

it('should include ortb2 first party data', () => {
expect(payload.device.w).to.equal(1860)
expect(payload.device.h).to.equal(410)
expect(payload.user.ext.consent).to.equal('G4ll0p1ng_Un1c0rn5')
expect(payload.regs.gdpr).to.equal(1)
})
})

describe('interpretResponse', () => {
const serverResponse = {
body: {
id: 'test-id',
cur: 'USD',
seatbid: [
{
bid: [
{
id: 'testbidid',
price: 0.4,
adm: 'test-ad',
adid: 789456,
crid: 1234,
w: 300,
h: 250,
},
],
},
],
},
}

const interpretedResponse = spec.interpretResponse(
serverResponse,
bidRequestData
)

it('should interpret response', () => {
expect(interpretedResponse[0].requestId).to.equal(bidRequestData.bidId)
expect(interpretedResponse[0].cpm).to.equal(
serverResponse.body.seatbid[0].bid[0].price
)
expect(interpretedResponse[0].ad).to.equal(
serverResponse.body.seatbid[0].bid[0].adm
)
expect(interpretedResponse[0].creativeId).to.equal(
serverResponse.body.seatbid[0].bid[0].crid
)
expect(interpretedResponse[0].width).to.equal(
serverResponse.body.seatbid[0].bid[0].w
)
expect(interpretedResponse[0].height).to.equal(
serverResponse.body.seatbid[0].bid[0].h
)
expect(interpretedResponse[0].currency).to.equal(serverResponse.body.cur)
})
})

describe('getUserSyncs', () => {
it('should return user sync', () => {
const optionsType = {
iframeEnabled: true,
pixelEnabled: true,
}
const serverResponse = [
{
body: {
ext: {
responsetimemillis: {
'test seat 1': 2,
'test seat 2': 1,
},
},
},
},
]
const gdprConsent = {
gdprApplies: 1,
consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig',
}
const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb'
const expectedUserSyncs = [
{
type: 'iframe',
url: 'https://null.holid.io/sync.html?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig&usp_consent=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe',
},
]

const userSyncs = spec.getUserSyncs(
optionsType,
serverResponse,
gdprConsent,
uspConsent
)

expect(userSyncs).to.deep.equal(expectedUserSyncs)
})
})
})

0 comments on commit 1c5d0fe

Please sign in to comment.