diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js new file mode 100644 index 000000000000..8e5e221a9d4f --- /dev/null +++ b/modules/sortableBidAdapter.js @@ -0,0 +1,149 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import { BANNER } from 'src/mediaTypes'; +import { REPO_AND_VERSION } from 'src/constants'; + +const BIDDER_CODE = 'sortable'; +const SERVER_URL = 'c.deployads.com'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + const sortableConfig = config.getConfig('sortable'); + const haveSiteId = (sortableConfig && !!sortableConfig.siteId) || bid.params.siteId; + return !!(bid.params.tagId && haveSiteId && bid.sizes && + bid.sizes.every(sizeArr => sizeArr.length == 2 && sizeArr.every(Number.isInteger))); + }, + + buildRequests: function(validBidReqs, bidderRequest) { + const sortableConfig = config.getConfig('sortable') || {}; + const globalSiteId = sortableConfig.siteId; + let loc = utils.getTopWindowLocation(); + + const sortableImps = utils._map(validBidReqs, bid => { + let rv = { + id: bid.bidId, + tagid: bid.params.tagId, + banner: { + format: utils._map(bid.sizes, ([width, height]) => ({w: width, h: height})) + }, + ext: {} + }; + if (bid.params.floor) { + rv.bidfloor = bid.params.floor; + } + if (bid.params.keywords) { + let keywords = utils._map(bid.params.keywords, (foo, bar) => ({name: bar, value: foo})); + rv.ext.keywords = keywords; + } + if (bid.params.bidderParams) { + utils._each(bid.params.bidderParams, (params, partner) => { + rv.ext[partner] = params; + }); + } + return rv; + }); + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const sortableBidReq = { + id: utils.getUniqueIdentifierStr(), + imp: sortableImps, + site: { + domain: loc.hostname, + page: loc.href, + ref: utils.getTopWindowReferrer(), + publisher: { + id: globalSiteId || validBidReqs[0].params.siteId, + }, + device: { + w: screen.width, + h: screen.height + }, + }, + }; + if (gdprConsent) { + sortableBidReq.user = { + ext: { + consent: gdprConsent.consentString + } + }; + sortableBidReq.regs = { + ext: { + gdpr: gdprConsent.gdprApplies ? 1 : 0 + } + }; + } + + return { + method: 'POST', + url: `//${SERVER_URL}/openrtb2/auction?src=${REPO_AND_VERSION}&host=${loc.host}`, + data: JSON.stringify(sortableBidReq), + options: {contentType: 'text/plain'} + }; + }, + + interpretResponse: function(serverResponse) { + const { body: {id, seatbid} } = serverResponse; + const sortableBids = []; + if (id && seatbid) { + utils._each(seatbid, seatbid => { + utils._each(seatbid.bid, bid => { + const bidObj = { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.id, + dealId: bid.dealid || null, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ttl: 60 + }; + if (bid.adm && bid.nurl) { + bidObj.ad = bid.adm; + bidObj.ad += utils.createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + bidObj.ad = bid.adm; + } else if (bid.nurl) { + bidObj.adUrl = bid.nurl; + } + sortableBids.push(bidObj); + }); + }); + } + return sortableBids; + }, + + getUserSyncs: (syncOptions, responses, gdprConsent) => { + const sortableConfig = config.getConfig('sortable'); + if (syncOptions.iframeEnabled && sortableConfig && !!sortableConfig.siteId) { + let syncUrl = `//${SERVER_URL}/sync?f=html&s=${sortableConfig.siteId}&u=${encodeURIComponent(utils.getTopWindowLocation())}`; + + if (gdprConsent) { + syncurl += '&g=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&cs=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + return [{ + type: 'iframe', + url: syncUrl + }]; + } + }, + + onTimeout(details) { + fetch(`//${SERVER_URL}/prebid/timeout`, { + method: 'POST', + body: JSON.stringify(details), + mode: 'no-cors', + headers: new Headers({ + 'Content-Type': 'text/plain' + }) + }); + } +}; + +registerBidder(spec); diff --git a/modules/sortableBidAdapter.md b/modules/sortableBidAdapter.md new file mode 100644 index 000000000000..027d6390e87c --- /dev/null +++ b/modules/sortableBidAdapter.md @@ -0,0 +1,56 @@ +# Overview + +``` +Module Name: Sortable Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@sortable.com +``` + +# Description + +Sortable's adapter integration to the Prebid library. Posts plain-text JSON to the /openrtb2/auction endpoint. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-pb-leaderboard', + sizes: [[728, 90]], + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-leaderboard', + siteId: 'prebid.example.com', + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + } + } + }] + }, { + code: 'test-pb-banner', + sizes: [[300, 250]], + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-banner', + siteId: 'prebid.example.com' + } + }] + }, { + code: 'test-pb-sidebar', + size: [[160, 600]], + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-sidebar', + siteId: 'prebid.example.com', + 'keywords': { + 'keyA': 'valA' + } + } + }] + } +] +``` diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js new file mode 100644 index 000000000000..9f351e9764c6 --- /dev/null +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -0,0 +1,194 @@ +import { expect } from 'chai'; +import { spec } from 'modules/sortableBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { REPO_AND_VERSION } from 'src/constants'; +import * as utils from 'src/utils'; + +const ENDPOINT = `//c.deployads.com/openrtb2/auction?src=${REPO_AND_VERSION}&host=${utils.getTopWindowLocation().host}`; + +describe('sortableBidAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', () => { + function makeBid() { + return { + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when tagId not passed correctly', () => { + let bid = makeBid(); + delete bid.params.tagId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes not passed correctly', () => { + let bid = makeBid(); + delete bid.sizes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when sizes are wrong length', () => { + let bid = makeBid(); + bid.sizes = [[300]]; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', () => { + let bid = makeBid(); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const bidRequests = [{ + 'bidder': 'sortable', + 'params': { + 'tagId': '403370', + 'siteId': 'example.com', + 'floor': 0.21, + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + } + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + + const request = spec.buildRequests(bidRequests); + const requestBody = JSON.parse(request.data); + + it('sends bid request to our endpoint via POST', () => { + expect(request.method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', () => { + expect(request.url).to.equal(ENDPOINT); + }); + + it('sends screen dimensions', () => { + expect(requestBody.site.device.w).to.equal(screen.width); + expect(requestBody.site.device.h).to.equal(screen.height); + }); + + it('includes the ad size in the bid request', () => { + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + }); + + it('includes the params in the bid request', () => { + expect(requestBody.imp[0].ext.keywords).to.deep.equal([ + {'name': 'key1', 'value': 'val1'}, + {'name': 'key2', 'value': 'val2'} + ]); + expect(requestBody.site.publisher.id).to.equal('example.com'); + expect(requestBody.imp[0].tagid).to.equal('403370'); + expect(requestBody.imp[0].bidfloor).to.equal(0.21); + }); + }); + + describe('interpretResponse', () => { + function makeResponse() { + return { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6vmb3isptf', + 'impid': '322add653672f68', + 'price': 1.22, + 'adm': '', + 'attr': [5], + 'h': 90, + 'nurl': 'http://nurl', + 'w': 728 + } + ], + 'seat': 'MOCK' + } + ], + 'bidid': '5e5c23a5ba71e78' + } + }; + } + + const expectedBid = { + 'requestId': '322add653672f68', + 'cpm': 1.22, + 'width': 728, + 'height': 90, + 'creativeId': '6vmb3isptf', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ttl': 60, + 'ad': '
' + }; + + it('should get the correct bid response', () => { + let result = spec.interpretResponse(makeResponse()); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should handle a missing nurl', () => { + let noNurlResponse = makeResponse(); + delete noNurlResponse.body.seatbid[0].bid[0].nurl; + let noNurlResult = Object.assign({}, expectedBid); + noNurlResult.ad = ''; + let result = spec.interpretResponse(noNurlResponse); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noNurlResult); + }); + + it('should handle a missing adm', () => { + let noAdmResponse = makeResponse(); + delete noAdmResponse.body.seatbid[0].bid[0].adm; + let noAdmResult = Object.assign({}, expectedBid); + delete noAdmResult.ad; + noAdmResult.adUrl = 'http://nurl'; + let result = spec.interpretResponse(noAdmResponse); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noAdmResult); + }); + + it('handles empty bid response', () => { + let response = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [] + } + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); +});