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 2 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
204 changes: 204 additions & 0 deletions modules/holidBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {
deepAccess,
deepSetValue,
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'
import { getRefererInfo } from '../src/refererDetection.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 = {
id: bid.auctionId,
imp: [getImp(bid)],
device: {
w: window.innerWidth,
h: window.innerHeight,
},
site: {
page: getRefererInfo().page,
ref: getRefererInfo().ref,
domain: getRefererInfo().domain,
},
regs: getRegs(bidderRequest),
}

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 getRegs(bidderRequest) {
const regs = {}

if (bidderRequest.gdprConsent) {
deepSetValue(
regs,
'ext.gdpr',
bidderRequest.gdprConsent.gdprApplies ? 1 : 0
)
deepSetValue(
regs,
'ext.gdprConsentString',
bidderRequest.gdprConsent.consentString || ''
)
}

if (bidderRequest.uspConsent) {
deepSetValue(regs, 'ext.us_privacy', bidderRequest.uspConsent)
}

return regs
Copy link
Collaborator

@patmmccann patmmccann Dec 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just merge the whole regs object? Or the whole ortb2 object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea, thank you! I have updated the PR with merging the whole ortb2 object

}

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: [email protected]
```

# 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',
},
},
],
},
]
```
147 changes: 147 additions & 0 deletions test/spec/modules/holidBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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]],
uspConsent: '1---',
gdprConsent: {
consentString: 'G4ll0p1ng_Un1c0rn5',
gdprApplies: true,
},
}

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 device', () => {
expect(payload.device.w).to.be.greaterThan(1)
expect(payload.device.h).to.be.greaterThan(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', () => {
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)
})
})