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

Bidmatic Bid Adapter: Initial Release #11690

Merged
merged 2 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
101 changes: 101 additions & 0 deletions modules/bidmaticBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { replaceAuctionPrice, isNumber, deepAccess } from '../src/utils.js';

export const END_POINT = 'https://adapter.bidmatic.io/ortb-client';
const BIDDER_CODE = 'bidmatic';
const DEFAULT_CURRENCY = 'USD';

export const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 290,
},
imp(buildImp, bidRequest, context) {
const imp = buildImp(bidRequest, context);
if (!imp.bidfloor) {
GeneGenie marked this conversation as resolved.
Show resolved Hide resolved
imp.bidfloor = bidRequest.params.bidfloor || 0;
imp.bidfloorcur = DEFAULT_CURRENCY;
}
imp.tagid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || bidRequest.adUnitCode;

return imp;
},
request(buildRequest, imps, bidderRequest, context) {
const request = buildRequest(imps, bidderRequest, context);
if (!request.cur) {
request.cur = [DEFAULT_CURRENCY];
}
return request;
},
bidResponse(buildBidResponse, bid, context) {
const { bidRequest } = context;

let resMediaType;
const reqMediaTypes = Object.keys(bidRequest.mediaTypes);
if (reqMediaTypes.length === 1) {
resMediaType = reqMediaTypes[0];
} else {
if (bid.adm.search(/^(<\?xml|<vast)/i) !== -1) {
resMediaType = VIDEO;
} else {
resMediaType = BANNER;
}
}

context.mediaType = resMediaType;

return buildBidResponse(bid, context);
}
});

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO],
gvlid: 1134,
isBidRequestValid: function (bid) {
return isNumber(deepAccess(bid, 'params.source'))
},

buildRequests: function (validBidRequests, bidderRequest) {
const requestsBySource = validBidRequests.reduce((acc, bidRequest) => {
acc[bidRequest.params.source] = acc[bidRequest.params.source] || [];
acc[bidRequest.params.source].push(bidRequest);
return acc;
}, {});

return Object.entries(requestsBySource).map(([source, bidRequests]) => {
const data = converter.toORTB({ bidRequests, bidderRequest });
const url = new URL(END_POINT);
url.searchParams.append('source', source);
return {
method: 'POST',
url: url.toString(),
data: data,
options: {
withCredentials: true,
}
};
});
},

interpretResponse: function (serverResponse, bidRequest) {
if (!serverResponse || !serverResponse.body) return [];
const parsedSeatbid = serverResponse.body.seatbid.map(seatbidItem => {
const parsedBid = seatbidItem.bid.map((bidItem) => ({
...bidItem,
adm: replaceAuctionPrice(bidItem.adm, bidItem.price),
nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price)
}));
return { ...seatbidItem, bid: parsedBid };
});
const responseBody = { ...serverResponse.body, seatbid: parsedSeatbid };
return converter.fromORTB({
response: responseBody,
request: bidRequest.data,
}).bids;
},

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

```
Module Name: Bidmatic Bid Adapter
Module Type: Bidder Adapter
Maintainer: mg@bidmatic.io
```

# Description

Adds access to Bidmatic SSP oRTB service.

# Sample Ad Unit: For Publishers
```
var adUnits = [{
code: 'bg-test-rectangle',
sizes: [[300, 250]],
bids: [{
bidder: 'bidmatic',
params: {
source: 886409,
bidfloor: 0.1
}
}]
}]
```
268 changes: 268 additions & 0 deletions test/spec/modules/bidmaticBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { expect } from 'chai';
import { END_POINT, spec } from 'modules/bidmaticBidAdapter.js';
import { deepClone, deepSetValue, mergeDeep } from '../../../src/utils';

const expectedImp = {
'id': '2eb89f0f062afe',
'banner': {
'topframe': 0,
'format': [
{
'w': 300,
'h': 250
},
{
'w': 300,
'h': 600
}
]
},
'bidfloor': 0,
'bidfloorcur': 'USD',
'tagid': 'div-gpt-ad-1460505748561-0'
}

describe('Bidmatic Bid Adapter', () => {
const GPID_RTB_EXT = {
'ortb2Imp': {
'ext': {
'gpid': 'gpId',
}
},
}
const FLOOR_RTB_EXT = {
'ortb2Imp': {
bidfloor: 1
},
}
const DEFAULT_BID_REQUEST = {
'id': '10bb57ee-712f-43e9-9769-b26d03df8a39',
'bidder': 'bidmatic',
'params': {
'source': 886409,
},
'mediaTypes': {
'banner': {
'sizes': [
[
300,
250
],
[
300,
600
]
]
}
},
'adUnitCode': 'div-gpt-ad-1460505748561-0',
'transactionId': '7d79850b-70aa-4c0f-af95-c1def0452825',
'sizes': [
[
300,
250
],
[
300,
600
]
],
'bidId': '2eb89f0f062afe',
'bidderRequestId': '1ae6c8e18f8462',
'auctionId': '1286637c-51bc-4fdd-8e35-2435ec11775a',
'ortb2': {}
};

describe('adapter interface', () => {
const bidRequest = deepClone(DEFAULT_BID_REQUEST);

it('should validate params', () => {
expect(spec.isBidRequestValid({
params: {
source: 1
}
})).to.equal(true, 'source param must be a number');

expect(spec.isBidRequestValid({
params: {
source: '1'
}
})).to.equal(false, 'source param must be a number');

expect(spec.isBidRequestValid({})).to.equal(false, 'source param must be a number');
});

it('should build hb request', () => {
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});

expect(ortbRequest.data.imp[0]).to.deep.equal(expectedImp);
expect(ortbRequest.data.cur).to.deep.equal(['USD']);
});

it('should request with source in url', () => {
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});
expect(ortbRequest.url).to.equal(`${END_POINT}?source=886409`);
});

it('should split http reqs by sources', () => {
const bidRequest2 = mergeDeep(deepClone(DEFAULT_BID_REQUEST), {
params: {
source: 1111
}
});
const [ortbRequest1, ortbRequest2] = spec.buildRequests([bidRequest2, bidRequest, bidRequest2], {
bids: [bidRequest2, bidRequest, bidRequest2]
});
expect(ortbRequest1.url).to.equal(`${END_POINT}?source=1111`);
expect(ortbRequest1.data.imp.length).to.eq(2)
expect(ortbRequest2.url).to.equal(`${END_POINT}?source=886409`);
expect(ortbRequest2.data.imp.length).to.eq(1)
});

it('should grab bid floor info', () => {
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});

expect(ortbRequest.data.imp[0].bidfloor).eq(0)
expect(ortbRequest.data.imp[0].bidfloorcur).eq('USD')
});

it('should grab bid floor info from exts', () => {
const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), FLOOR_RTB_EXT);
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});

expect(ortbRequest.data.imp[0].bidfloor).eq(1)
});

it('should grab bid floor info from params', () => {
const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), {
params: {
bidfloor: 2
}
});
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});

expect(ortbRequest.data.imp[0].bidfloor).eq(2)
});

it('should set gpid as tagid', () => {
const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), GPID_RTB_EXT);
const [ortbRequest] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
});

expect(ortbRequest.data.imp[0].tagid).eq(GPID_RTB_EXT.ortb2Imp.ext.gpid)
});
})

describe('response interpreter', () => {
const SERVER_RESPONSE = {
'body': {
'id': '10bb57ee-712f-43e9-9769-b26d03df8a39',
'seatbid': [
{
'bid': [
{
'id': 'c5BsBD5QHHgx4aS8',
'impid': '2eb89f0f062afe',
'price': 1,
'adid': 'BDhclfXLcGzRMeV',
'adm': '123',
'adomain': [
'https://test.com'
],
'crid': 'display_300x250',
'w': 300,
'h': 250,
}
],
'seat': '1'
}
],
'cur': 'USD'
},
'headers': {}
}

it('should return empty results', () => {
const [req] = spec.buildRequests([deepClone(DEFAULT_BID_REQUEST)], {
bids: [deepClone(DEFAULT_BID_REQUEST)]
})
const result = spec.interpretResponse(null, {
data: req.data
})

expect(result.length).to.eq(0);
});
it('should detect media type based on adm', () => {
const [req] = spec.buildRequests([deepClone(DEFAULT_BID_REQUEST)], {
bids: [deepClone(DEFAULT_BID_REQUEST)]
})
const result = spec.interpretResponse(SERVER_RESPONSE, {
data: req.data
})

expect(result.length).to.eq(1);
expect(result[0].mediaType).to.eq('banner')
});
it('should detect video adm', () => {
const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), {
mediaTypes: {
banner: {
sizes: [
[300, 250]
]
},
video: {
playerSize: [640, 480]
}
}
})
const bannerResponse = deepClone(SERVER_RESPONSE);
const [ortbReq] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
})
deepSetValue(bannerResponse, 'body.seatbid.0.bid.0.adm', '<vast></vast>');
const result = spec.interpretResponse(bannerResponse, {
data: ortbReq.data
})

expect(result.length).to.eq(1);
expect(result[0].mediaType).to.eq('video')
});

it('should detect banner adm', () => {
const bidRequest = mergeDeep(deepClone(DEFAULT_BID_REQUEST), {
mediaTypes: {
banner: {
sizes: [
[300, 250]
]
},
video: {
playerSize: [640, 480]
}
}
})
const bannerResponse = deepClone(SERVER_RESPONSE);
const [ortbReq] = spec.buildRequests([bidRequest], {
bids: [bidRequest]
})
const result = spec.interpretResponse(bannerResponse, {
data: ortbReq.data
})

expect(result.length).to.eq(1);
expect(result[0].mediaType).to.eq('banner')
});
})
})