Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Holid Bid Adapter: initial release #9371

Merged
merged 5 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
})
})