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

RelevateHealth Bid Adapter : Initial release #11640

Merged
merged 7 commits into from
Jun 6, 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
151 changes: 151 additions & 0 deletions modules/relevatehealthBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER } from '../src/mediaTypes.js';
import { deepAccess, generateUUID, isArray, logError } from '../src/utils.js';

const BIDDER_CODE = 'relevatehealth';

const ENDPOINT_URL = 'https://rtb.relevate.health/prebid/relevate';

function buildRequests(bidRequests, bidderRequest) {
const requests = [];
// Loop through each bid request
bidRequests.forEach(bid => {
// Construct the bid request object
const request = {
id: generateUUID(),
placementId: bid.params.placementId,
imp: [{
id: bid.bidId,
banner: getBanner(bid),
bidfloor: bid.params.bid_floor
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please also look to the getFloor function in the request

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created a function getFloor() to get bid_floor.

}],
site: getSite(bidderRequest),
user: buildUser(bid)
};

// Get uspConsent from bidderRequest
if (bidderRequest && bidderRequest.uspConsent) {
request.us_privacy = bidderRequest.uspConsent;
}
// Get GPP Consent from bidderRequest
if (bidderRequest?.gppConsent?.gppString) {
request.gpp = bidderRequest.gppConsent.gppString;
request.gpp_sid = bidderRequest.gppConsent.applicableSections;
} else if (bidderRequest?.ortb2?.regs?.gpp) {
request.gpp = bidderRequest.ortb2.regs.gpp;
request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid;
}

// Get coppa compliance from bidderRequest
if (bidderRequest?.ortb2?.regs?.coppa) {
request.coppa = 1;
}
// Push the constructed bid request to the requests array
requests.push(request);
});
// Return the array of bid requests
return {
method: 'POST',
url: ENDPOINT_URL,
data: JSON.stringify(requests),
options: {
contentType: 'application/json',
}
};
}

// Format the response as per the standards
function interpretResponse(bidResponse, bidRequest) {
let resp = [];
if (bidResponse && bidResponse.body) {
try {
let bids = bidResponse.body.seatbid && bidResponse.body.seatbid[0] ? bidResponse.body.seatbid[0].bid : [];
if (bids) {
bids.forEach(bidObj => {
let newBid = formatResponse(bidObj);
newBid.mediaType = BANNER;
resp.push(newBid);
});
}
} catch (err) {
logError(err);
}
}
return resp;
}

// Function to check if Bid is valid
function isBidRequestValid(bid) {
return !!(bid.params.placementId && bid.params.user_id);
Copy link
Contributor

Choose a reason for hiding this comment

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

Good chance the answer is yes, but did you intend to have placementId be camel case while user_id uses underscores?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To make all consistent, changed placementId to placement_id. Also changed in Docs

}

// Function to get banner details
function getBanner(bid) {
if (deepAccess(bid, 'mediaTypes.banner')) {
// Fetch width and height from MediaTypes object, if not provided in bid params
if (deepAccess(bid, 'mediaTypes.banner.sizes') && !bid.params.height && !bid.params.width) {
let sizes = deepAccess(bid, 'mediaTypes.banner.sizes');
if (isArray(sizes) && sizes.length > 0) {
return {
h: sizes[0][1],
w: sizes[0][0]
}
}
} else {
return {
h: bid.params.height,
w: bid.params.width
}
}
}
}

// Function to get site details
function getSite(bidderRequest) {
let site = {};
if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page) {
site.name = bidderRequest.refererInfo.domain;
} else {
site.name = '';
}
return site;
}

function formatResponse(bid) {
return {
requestId: bid && bid.impid ? bid.impid : undefined,
cpm: bid && bid.price ? bid.price : 0.0,
width: bid && bid.w ? bid.w : 0,
height: bid && bid.h ? bid.h : 0,
ad: bid && bid.adm ? bid.adm : '',
meta: {
advertiserDomains: bid && bid.adomain ? bid.adomain : []
},
creativeId: bid && bid.crid ? bid.crid : undefined,
netRevenue: false,
currency: bid && bid.cur ? bid.cur : 'USD',
ttl: 300,
dealId: bid && bid.dealId ? bid.dealId : undefined
}
}

function buildUser(bid) {
if (bid && bid.params) {
return {
id: bid.params.user_id && typeof bid.params.user_id == 'string' ? bid.params.user_id : '',
buyeruid: localStorage.getItem('adx_profile_guid') ? localStorage.getItem('adx_profile_guid') : '',
keywords: bid.params.keywords && typeof bid.params.keywords == 'string' ? bid.params.keywords : '',
customdata: bid.params.customdata && typeof bid.params.customdata == 'string' ? bid.params.customdata : ''
}
}
}

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: BANNER,
isBidRequestValid,
buildRequests,
interpretResponse
}

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

```
Module Name: relevatehealth Bidder Adapter
Module Type: Bidder Adapter
Maintainer: marketingops@relevatehealth.com
```

# Description

relevatehealth currently supports the BANNER type ads through prebid js

Module that connects to relevatehealth's demand sources.

# Banner Test Request
```
var adUnits = [
{
code: 'banner-ad',
mediaTypes: {
banner: {
sizes: [[160, 600]],
}
}
bids: [
{
bidder: 'relevatehealth',
params: {
placementId: 110011, // Required parameter
user_id: '1111111' // Required parameter
width: 160, // Optional parameter
height: 600, // Optional parameter
domain: '', // Optional parameter
bid_floor: 0.5 // Optional parameter
}
}
]
}
];
```
204 changes: 204 additions & 0 deletions test/spec/modules/relevatehealthBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { expect } from 'chai';
import { spec } from '../../../modules/relevatehealthBidAdapter.js';
import * as utils from '../../../src/utils.js';

describe('relevatehealth adapter', function () {
let request;
let bannerResponse, invalidResponse;

beforeEach(function () {
request = [
{
bidder: 'relevatehealth',
mediaTypes: {
banner: {
sizes: [[160, 600]]
}
},
params: {
placementId: 110011,
user_id: '11211',
width: 160,
height: 600,
domain: '',
bid_floor: 0.5
}
}
];
bannerResponse = {
'body': {
'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54',
'seatbid': [{
'bid': [{
'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8',
'impid': '285b9c53b2c662',
'price': 20.68,
'adid': '3389',
'adm': "<a href=\"https://r.relevate.health/adx-rtb-m/servlet/WebF_AdManager.AdLinkManager?qs=H4sIAAAAAAAAAx2S2Q1FIQhEW2ITsBwW6b+Ex32J8cMAM2fQGDvalAl1atTnsOaZRLWjokdsKrG8J5i7Mg+1S1tAZ3gH1/SxuACgSLd3Gjid77Ei77uXH1rrFUZLH3KYaopzyPCpQAkqigyfg3QecCaeG/fN4FXT92A6coszhUuer1QfYNJjXmkFStJ6TngOTdsVKUi20+yyxolDMEb6ZIlP6N6OOJ3v1dxHdKtUvmlg+XI1kQAuTDqIrPzNk7RxQKyw8HIZI7560o1LVCM3yDt5czy5YlfdOAy3VewlaEVVXgRA3HOR1c+7HKG0cQC+OBJxuo8P8nJsYqK9Ipk6UY9uagagmJQ+bm6YtX4CSdPs8cdcu7buus/rHiOthp2JRqJQtjQk6xZxe0r4Bg8WnGwGM8m7G8Abs0j5/mly31F0vpshYybsigk3IGU/vpDtT/2mAdV2KjI0l1VnC3+lcF7cyc5FNjXjN+Uop7UFupaj6OlIcVG+Fq16ybcr1l1HZGEUerx9SteCLRV/4Kg53fQE08jpvSu0qa8Dr+K8qz1QTrg+c3+2m5wLHjJhYjGkRpSKtjkIgXXIru6Q+/qDBMMVI/0BzbAHIiADAAA=\" target=\"_blank\"><img src=\"https://cdn.relevate.health/2_310042_1.png\" height=\"600\" width=\"160\"></img></a><img width=\"1px\" height=\"1px\" style=\"display:none;\" src=\"http://rtb.relevate.health:9001/beacon?uid=7a89e67afcc50dd00df1f36b1e113f9e&cc=410014&fccap=3&nid=2\"></img><script async src='https://i.relevate.health/adx-rtb-m/servlet/WebF_AdManager.ImpTracker?qs=&price=${AUCTION_PRICE}%26id%3D110011%2C12517%2C410014%2C310042%2C210009%2C6%2C2%2C12518%2C2%2C12518%2C1%26cb%3D1717164048%26ap%3D1.88000%26mf%3D0.06000%26ai%3D%2C-1%2C-1%2C-1%26ag%3D%5Badx_guid%5D%2Cb52c3caf-261b-45b1-8f6a-12507b95c335%2C123456%2Cb52c3caf-261b-45b1-8f6a-12507b95c335%2C12518_%26as%3D-1%2C-1%26mm%3D-1%2C-1%26ua%3DUnKnown%26ref%3D'></script><script async src='https://i.relevate.health/adx-rtb-m/servlet/WebF_AdManager.ImpCounter?qs=&price=${AUCTION_PRICE}%26id%3D110011%2C12517%2C410014%2C310042%2C210009%2C6%2C2%2C12518%2C2%2C12518%2C1%26cb%3D1717164048%26ap%3D1.88000%26mf%3D0.06000%26ai%3D%2C-1%2C-1%2C-1%26ag%3D%5Badx_guid%5D%2Cb52c3caf-261b-45b1-8f6a-12507b95c335%2C123456%2Cb52c3caf-261b-45b1-8f6a-12507b95c335%2C12518_%26as%3D-1%2C-1%26mm%3D-1%2C-1%26ua%3DUnKnown%26ref%3D'></script>",
'adomain': ['google.com'],
'iurl': 'https://rtb.relevate.health/prebid/relevate',
'cid': '1431/3389',
'crid': '3389',
'w': 160,
'h': 600,
'cat': ['IAB1-15']
}],
'seat': '00001',
'group': 0
}],
'cur': 'USD',
'bidid': 'BIDDER_1276'
}
};
invalidResponse = {
'body': {
'id': 'a48b79e7-7104-4213-99f3-55f3234f2e54',
'seatbid': [{
'bid': [{
'id': '3d7dd6dc-7665-4cdc-96a4-5ea192df32b8',
'impid': '285b9c53b2c662',
'price': 20.68,
'adid': '3389',
'adm': 'invalid response',
'adomain': ['google.com'],
'iurl': 'https://rtb.relevate.health/prebid/relevate',
'cid': '1431/3389',
'crid': '3389',
'w': 160,
'h': 600,
'cat': ['IAB1-15']
}],
'seat': '00001',
'group': 0
}],
'cur': 'USD',
'bidid': 'BIDDER_1276'
}
};
});

describe('validations', function () {
it('isBidValid : placementId and user_id are passed', function () {
let bid = {
bidder: 'relevatehealth',
params: {
placementId: 110011,
user_id: '11211'
}
},
isValid = spec.isBidRequestValid(bid);
expect(isValid).to.equals(true);
});
it('isBidValid : placementId and user_id are not passed', function () {
Copy link
Contributor

Choose a reason for hiding this comment

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

You should check if it fails validation when either user_id or placementId are missing. There should be multiple tests for multiple conditions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added the validations as asked.

let bid = {
bidder: 'relevatehealth',
params: {
width: 160,
height: 600,
domain: '',
bid_floor: 0.5,
user_id: ''
}
},
isValid = spec.isBidRequestValid(bid);
expect(isValid).to.equals(false);
});
});
describe('Validate Request', function () {
it('Immutable bid request validate', function () {
let _Request = utils.deepClone(request),
bidRequest = spec.buildRequests(request);
expect(request).to.deep.equal(_Request);
});
it('Validate bidder connection', function () {
let _Request = spec.buildRequests(request);
expect(_Request.url).to.equal('https://rtb.relevate.health/prebid/relevate');
expect(_Request.method).to.equal('POST');
expect(_Request.options.contentType).to.equal('application/json');
});
it('Validate bid request : Impression', function () {
let _Request = spec.buildRequests(request);
let data = JSON.parse(_Request.data);
// expect(data.at).to.equal(1); // auction type
expect(data[0].imp[0].id).to.equal(request[0].bidId);
expect(data[0].placementId).to.equal(110011);
});
it('Validate bid request : ad size', function () {
let _Request = spec.buildRequests(request);
let data = JSON.parse(_Request.data);
expect(data[0].imp[0].banner).to.be.a('object');
expect(data[0].imp[0].banner.w).to.equal(160);
expect(data[0].imp[0].banner.h).to.equal(600);
});
it('Validate bid request : user object', function () {
let _Request = spec.buildRequests(request);
let data = JSON.parse(_Request.data);
expect(data[0].user).to.be.a('object');
expect(data[0].user.id).to.be.a('string');
});
it('Validate bid request : CCPA Check', function () {
let bidRequest = {
uspConsent: '1NYN'
};
let _Request = spec.buildRequests(request, bidRequest);
let data = JSON.parse(_Request.data);
expect(data[0].us_privacy).to.equal('1NYN');
// let _bidRequest = {};
// let _Request1 = spec.buildRequests(request, _bidRequest);
// let data1 = JSON.parse(_Request1.data);
// expect(data1.regs).to.equal(undefined);
});
});
describe('Validate response ', function () {
it('Validate bid response : valid bid response', function () {
let bRequest = spec.buildRequests(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong, but it doesn't seem like setting bRequest and data is actually doing anything in this test. Remove these two lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right. Removed the lines.

let data = JSON.parse(bRequest.data);
let bResponse = spec.interpretResponse(bannerResponse, request);
expect(bResponse).to.be.an('array').with.length.above(0);
expect(bResponse[0].requestId).to.equal(bannerResponse.body.seatbid[0].bid[0].impid);
expect(bResponse[0].width).to.equal(bannerResponse.body.seatbid[0].bid[0].w);
expect(bResponse[0].height).to.equal(bannerResponse.body.seatbid[0].bid[0].h);
expect(bResponse[0].currency).to.equal('USD');
expect(bResponse[0].netRevenue).to.equal(false);
expect(bResponse[0].mediaType).to.equal('banner');
expect(bResponse[0].meta.advertiserDomains).to.deep.equal(['google.com']);
expect(bResponse[0].ttl).to.equal(300);
expect(bResponse[0].creativeId).to.equal(bannerResponse.body.seatbid[0].bid[0].crid);
expect(bResponse[0].dealId).to.equal(bannerResponse.body.seatbid[0].bid[0].dealId);
});
it('Invalid bid response check ', function () {
let bRequest = spec.buildRequests(request);
let response = spec.interpretResponse(invalidResponse, bRequest);
expect(response[0].ad).to.equal('invalid response');
});
});
describe('GPP and coppa', function () {
it('Request params check with GPP Consent', function () {
let bidderReq = { gppConsent: { gppString: 'gpp-string-test', applicableSections: [5] } };
let _Request = spec.buildRequests(request, bidderReq);
let data = JSON.parse(_Request.data);
expect(data[0].gpp).to.equal('gpp-string-test');
expect(data[0].gpp_sid[0]).to.equal(5);
});
it('Request params check with GPP Consent read from ortb2', function () {
let bidderReq = {
ortb2: {
regs: {
gpp: 'gpp-test-string',
gpp_sid: [5]
}
}
};
let _Request = spec.buildRequests(request, bidderReq);
let data = JSON.parse(_Request.data);
expect(data[0].gpp).to.equal('gpp-test-string');
expect(data[0].gpp_sid[0]).to.equal(5);
});
it(' Bid request should have coppa flag if its true', () => {
let bidderReq = { ortb2: { regs: { coppa: 1 } } };
let _Request = spec.buildRequests(request, bidderReq);
let data = JSON.parse(_Request.data);
expect(data[0].coppa).to.equal(1);
});
});
});