diff --git a/modules/adgridBidAdapter.js b/modules/adgridBidAdapter.js new file mode 100644 index 00000000000..17156280c0d --- /dev/null +++ b/modules/adgridBidAdapter.js @@ -0,0 +1,175 @@ +import { _each, isEmpty, deepAccess } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER = Object.freeze({ + CODE: 'adgrid', + HOST: 'https://api-prebid.adgrid.io', + REQUEST_METHOD: 'POST', + REQUEST_ENDPOINT: '/api/v1/auction', + SUPPORTED_MEDIA_TYPES: [BANNER], +}); + +const CURRENCY = Object.freeze({ + KEY: 'currency', + US_DOLLAR: 'USD', +}); + +function isBidRequestValid(bid) { + if (!bid || !bid.params) { + return false; + } + + return !!bid.params.domainId; +} + +/** + * Return some extra params + */ +function getAudience(validBidRequests, bidderRequest) { + const params = { + domain: deepAccess(bidderRequest, 'refererInfo.page') + }; + + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + params.gdpr = 1; + params.gdprConsent = deepAccess(bidderRequest, 'gdprConsent.consentString'); + } + + if (deepAccess(bidderRequest, 'uspConsent')) { + params.usp = deepAccess(bidderRequest, 'uspConsent'); + } + + if (deepAccess(validBidRequests[0], 'schain')) { + params.schain = deepAccess(validBidRequests[0], 'schain'); + } + + if (deepAccess(validBidRequests[0], 'userId')) { + params.userIds = deepAccess(validBidRequests[0], 'userId'); + } + + if (deepAccess(validBidRequests[0], 'userIdAsEids')) { + params.userEids = deepAccess(validBidRequests[0], 'userIdAsEids'); + } + + if (bidderRequest.gppConsent) { + params.gpp = bidderRequest.gppConsent.gppString; + params.gppSid = bidderRequest.gppConsent.applicableSections?.toString(); + } else if (bidderRequest.ortb2?.regs?.gpp) { + params.gpp = bidderRequest.ortb2.regs.gpp; + params.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + return params; +} + +function buildRequests(validBidRequests, bidderRequest) { + const currencyObj = config.getConfig(CURRENCY.KEY); + const currency = (currencyObj && currencyObj.adServerCurrency) ? currencyObj.adServerCurrency : 'USD'; + const bids = []; + + _each(validBidRequests, bid => { + bids.push(getBidData(bid)) + }); + + const bidsParams = Object.assign({}, { + url: window.location.href, + timeout: bidderRequest.timeout, + ts: new Date().getTime(), + device: { + size: [ + window.screen.width, + window.screen.height + ] + }, + bids + }); + + // Add currency if not USD + if (currency != null && currency != CURRENCY.US_DOLLAR) { + bidsParams.cur = currency; + } + + bidsParams.audience = getAudience(validBidRequests, bidderRequest); + + // Passing geo location data if found in prebid config + bidsParams.geodata = config.getConfig('adgGeodata') || {}; + + return Object.assign({}, bidderRequest, { + method: BIDDER.REQUEST_METHOD, + url: `${BIDDER.HOST}${BIDDER.REQUEST_ENDPOINT}`, + data: bidsParams, + currency: currency, + options: { + withCredentials: false, + contentType: 'application/json' + } + }); +} + +function interpretResponse(response, bidRequest) { + let bids = response.body; + const bidResponses = []; + + if (isEmpty(bids)) { + return bidResponses; + } + + if (typeof bids !== 'object') { + return bidResponses; + } + + bids = bids.bids; + + bids.forEach((adUnit) => { + const bidResponse = { + requestId: adUnit.bidId, + cpm: Number(adUnit.cpm), + width: adUnit.width, + height: adUnit.height, + ttl: 300, + creativeId: adUnit.creativeId, + netRevenue: true, + currency: adUnit.currency || bidRequest.currency, + mediaType: adUnit.mediaType, + ad: adUnit.ad, + }; + + bidResponses.push(bidResponse); + }); + + return bidResponses; +} + +function getBidData(bid) { + const bidData = { + requestId: bid.bidId, + tid: bid.ortb2Imp?.ext?.tid, + deviceW: bid.ortb2?.device?.w, + deviceH: bid.ortb2?.device?.h, + deviceUa: bid.ortb2?.device?.ua, + domain: bid.ortb2?.site?.publisher?.domain, + domainId: bid.params.domainId, + code: bid.adUnitCode + }; + + if (bid.mediaTypes != null) { + if (bid.mediaTypes.banner != null) { + bidData.mediaType = 'banner'; + bidData.sizes = bid.mediaTypes.banner.sizes; + } + } + + return bidData; +} + +export const spec = { + code: BIDDER.CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + supportedMediaTypes: BIDDER.SUPPORTED_MEDIA_TYPES +}; + +registerBidder(spec); diff --git a/modules/adgridBidAdapter.md b/modules/adgridBidAdapter.md new file mode 100644 index 00000000000..94eda01e895 --- /dev/null +++ b/modules/adgridBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +**Module Name**: AdGrid Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: support@adgrid.io + +# Description + +The AdGrid Bidding Adapter requires setup and approval before beginning. Please reach out to for more details. + +# Test Parameters + +```javascript +var adUnits = [ + // Banner adUnit + { + code: 'test-div-1', + mediaTypes:{ + banner:{ + sizes: [[300, 250]] + } + } + bids: [{ + bidder: 'adgrid', + params: { + domainId: 12345 + } + }] + }, + { + code: 'test-div-2', + mediaTypes:{ + banner:{ + sizes: [[728, 90], [320, 50]] + } + } + bids: [{ + bidder: 'adgrid', + params: { + domainId: 67890 + } + }] + } +]; +``` diff --git a/test/spec/modules/adgridBidAdapter_spec.js b/test/spec/modules/adgridBidAdapter_spec.js new file mode 100644 index 00000000000..f7da18403fd --- /dev/null +++ b/test/spec/modules/adgridBidAdapter_spec.js @@ -0,0 +1,100 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adgridBidAdapter.js' + +const globalConfig = { + method: 'POST', + endPoint: 'https://api-prebid.adgrid.io/api/v1/auction' +}; + +describe('AdGrid Bid Adapter', function () { + const bannerRequest = [{ + bidId: 123456, + auctionId: 98765, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + params: { + domainId: 12345 + } + }]; + + describe('isBidRequestValid', function () { + it('Should return true when domainId exist inside params object', function () { + const isBidValid = spec.isBidRequestValid(bannerRequest[0]); + expect(isBidValid).to.be.true; + }); + + it('Should return false when domainId is not exist inside params object', function () { + const isBidNotValid = spec.isBidRequestValid(null); + expect(isBidNotValid).to.be.false; + }); + }); + + describe('buildRequests', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = request.data; + const apiURL = request.url; + const method = request.method; + + it('Test the request is not empty', function () { + expect(request).to.not.be.empty; + }); + + it('Test the request payload is not empty', function () { + expect(payload).to.not.be.empty; + }); + + it('Test the API End Point', function () { + expect(apiURL).to.equal(globalConfig.endPoint); + }); + + it('Test the API Method', function () { + expect(method).to.equal(globalConfig.method); + }); + }); + + describe('interpretResponse', function () { + const responseObj = { + bids: [ + { + bidId: '4b99f3428651c1', + cpm: 7.7, + ad: '
Ad Content
', + creativeId: '9004', + currency: 'USD', + mediaType: 'banner', + width: 320, + height: 50, + domainId: '2002', + marketplaceId: '703', + devices: 'desktop' + } + ] + }; + + it('Test the interpretResponse function', function () { + const receivedBid = responseObj.bids[0]; + const response = {}; + response.body = responseObj; + + const bidRequest = {}; + bidRequest.currency = 'USD'; + + const bidResponse = spec.interpretResponse(response, bidRequest); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.requestId).to.equal(receivedBid.bidId); + expect(bid.ad).to.equal(receivedBid.ad); + expect(bid.cpm).to.equal(receivedBid.cpm); + expect(bid.mediaType).to.equal(receivedBid.mediaType); + expect(bid.creativeId).to.equal(receivedBid.creativeId); + expect(bid.width).to.equal(receivedBid.width); + expect(bid.height).to.equal(receivedBid.height); + expect(bid.currency).to.equal(receivedBid.currency); + }); + }); +});