From f8b4d3b7f2fbd15f1786088ca308d4a81eab1006 Mon Sep 17 00:00:00 2001 From: Shannon Broekhoven Date: Fri, 6 Jul 2018 11:16:19 -0400 Subject: [PATCH 1/5] Add Sortable bid adapter - [x] New bidder adapter This change adds the Sortable adapter to Prebid.js. --- modules/sortableBidAdapter.js | 143 ++++++++++++++ modules/sortableBidAdapter.md | 56 ++++++ test/spec/modules/sortableBidAdapter_spec.js | 192 +++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 modules/sortableBidAdapter.js create mode 100644 modules/sortableBidAdapter.md create mode 100644 test/spec/modules/sortableBidAdapter_spec.js diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js new file mode 100644 index 00000000000..e2b4cac89f9 --- /dev/null +++ b/modules/sortableBidAdapter.js @@ -0,0 +1,143 @@ +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'; +const SORTABLE_ID = config.getConfig('sortableId'); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + const haveSiteId = !!config.getConfig('sortableId'); + return !!(bid.params.tagId && (haveSiteId || bid.params.siteId) && bid.sizes && + bid.sizes.every(sizeArr => sizeArr.length == 2 && sizeArr.every(Number.isInteger))); + }, + + buildRequests: function(validBidReqs, bidderRequest) { + 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.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.host, + page: loc.href, + ref: utils.getTopWindowReferrer(), + publisher: { + id: SORTABLE_ID || 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) => { + let syncUrl = `//${SERVER_URL}/sync?f=html&u=${encodeURIComponent(utils.getTopWindowLocation())}`; + + if (gdprConsent) { + syncurl += '&g=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&cs=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + if (syncOptions.iframeEnabled && SORTABLE_ID) { + 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 00000000000..6198ff476df --- /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: 1, + 'keywords': { + 'key1': 'val1', + 'key2': 'val2' + } + } + }] + }, { + code: 'test-pb-banner', + sizes: [[300, 250]], + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-banner', + siteId: 1 + } + }] + }, { + code: 'test-pb-sidebar', + size: [[160, 600]], + bids: [{ + bidder: 'sortable', + params: { + tagId: 'test-pb-sidebar', + siteId: 1, + 'keywords': { + 'keyA': 'valA' + } + } + }] + } +] +``` diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js new file mode 100644 index 00000000000..f9ad113ebc8 --- /dev/null +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -0,0 +1,192 @@ +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': 1, + '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': 1, + '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(800); + expect(requestBody.site.device.h).to.equal(600); + }); + + 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(1); + expect(requestBody.imp[0].tagid).to.equal('403370'); + }); + }); + + 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); + }); + }); +}); From f5ca7cffc51f83f630c649fe31a2980dbbec0b83 Mon Sep 17 00:00:00 2001 From: Bryan Gahagan Date: Mon, 9 Jul 2018 19:37:18 -0400 Subject: [PATCH 2/5] move global sortable config into its own object --- modules/sortableBidAdapter.js | 21 ++++++++++---------- modules/sortableBidAdapter.md | 6 +++--- test/spec/modules/sortableBidAdapter_spec.js | 10 +++++----- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index e2b4cac89f9..e6eddacdf2e 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -6,15 +6,15 @@ import { REPO_AND_VERSION } from 'src/constants'; const BIDDER_CODE = 'sortable'; const SERVER_URL = 'c.deployads.com'; -const SORTABLE_ID = config.getConfig('sortableId'); +const SORTABLE_CONFIG = config.getConfig('sortable') || {}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function(bid) { - const haveSiteId = !!config.getConfig('sortableId'); - return !!(bid.params.tagId && (haveSiteId || bid.params.siteId) && bid.sizes && + const haveSiteId = !!SORTABLE_CONFIG.siteId || bid.params.siteId; + return !!(bid.params.tagId && haveSiteId && bid.sizes && bid.sizes.every(sizeArr => sizeArr.length == 2 && sizeArr.every(Number.isInteger))); }, @@ -50,7 +50,7 @@ export const spec = { page: loc.href, ref: utils.getTopWindowReferrer(), publisher: { - id: SORTABLE_ID || validBidReqs[0].params.siteId, + id: SORTABLE_CONFIG.siteId || validBidReqs[0].params.siteId, }, device: { w: screen.width, @@ -113,14 +113,15 @@ export const spec = { }, getUserSyncs: (syncOptions, responses, gdprConsent) => { - let syncUrl = `//${SERVER_URL}/sync?f=html&u=${encodeURIComponent(utils.getTopWindowLocation())}`; + const siteId = SORTABLE_CONFIG.siteId; + if (syncOptions.iframeEnabled && siteId) { + let syncUrl = `//${SERVER_URL}/sync?f=html&s=${siteId}&u=${encodeURIComponent(utils.getTopWindowLocation())}`; - if (gdprConsent) { - syncurl += '&g=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&cs=' + encodeURIComponent(gdprConsent.consentString || ''); - } + if (gdprConsent) { + syncurl += '&g=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&cs=' + encodeURIComponent(gdprConsent.consentString || ''); + } - if (syncOptions.iframeEnabled && SORTABLE_ID) { return [{ type: 'iframe', url: syncUrl diff --git a/modules/sortableBidAdapter.md b/modules/sortableBidAdapter.md index 6198ff476df..4d5f55fade0 100644 --- a/modules/sortableBidAdapter.md +++ b/modules/sortableBidAdapter.md @@ -21,7 +21,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-leaderboard', - siteId: 1, + siteId: 'example.com', 'keywords': { 'key1': 'val1', 'key2': 'val2' @@ -35,7 +35,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-banner', - siteId: 1 + siteId: 'example.com' } }] }, { @@ -45,7 +45,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-sidebar', - siteId: 1, + siteId: 'example.com', 'keywords': { 'keyA': 'valA' } diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js index f9ad113ebc8..65588f23a9b 100644 --- a/test/spec/modules/sortableBidAdapter_spec.js +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -15,7 +15,7 @@ describe('sortableBidAdapter', function() { 'bidder': 'sortable', 'params': { 'tagId': '403370', - 'siteId': 1, + 'siteId': 'example.com', 'keywords': { 'key1': 'val1', 'key2': 'val2' @@ -65,7 +65,7 @@ describe('sortableBidAdapter', function() { 'bidder': 'sortable', 'params': { 'tagId': '403370', - 'siteId': 1, + 'siteId': 'example.com', 'keywords': { 'key1': 'val1', 'key2': 'val2' @@ -91,8 +91,8 @@ describe('sortableBidAdapter', function() { }); it('sends screen dimensions', () => { - expect(requestBody.site.device.w).to.equal(800); - expect(requestBody.site.device.h).to.equal(600); + 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', () => { @@ -105,7 +105,7 @@ describe('sortableBidAdapter', function() { {'name': 'key1', 'value': 'val1'}, {'name': 'key2', 'value': 'val2'} ]); - expect(requestBody.site.publisher.id).to.equal(1); + expect(requestBody.site.publisher.id).to.equal('example.com'); expect(requestBody.imp[0].tagid).to.equal('403370'); }); }); From 893bbdda06fa9e790557e7dda1204df7616a3d9f Mon Sep 17 00:00:00 2001 From: Bryan Gahagan Date: Tue, 10 Jul 2018 18:39:03 -0400 Subject: [PATCH 3/5] update siteId --- modules/sortableBidAdapter.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/sortableBidAdapter.md b/modules/sortableBidAdapter.md index 4d5f55fade0..027d6390e87 100644 --- a/modules/sortableBidAdapter.md +++ b/modules/sortableBidAdapter.md @@ -21,7 +21,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-leaderboard', - siteId: 'example.com', + siteId: 'prebid.example.com', 'keywords': { 'key1': 'val1', 'key2': 'val2' @@ -35,7 +35,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-banner', - siteId: 'example.com' + siteId: 'prebid.example.com' } }] }, { @@ -45,7 +45,7 @@ var adUnits = [ bidder: 'sortable', params: { tagId: 'test-pb-sidebar', - siteId: 'example.com', + siteId: 'prebid.example.com', 'keywords': { 'keyA': 'valA' } From c643d1996f2dc83448493e0827ff0ff74ca6c025 Mon Sep 17 00:00:00 2001 From: Bryan Gahagan Date: Tue, 10 Jul 2018 18:43:42 -0400 Subject: [PATCH 4/5] config can be undefined when module loads --- modules/sortableBidAdapter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index e6eddacdf2e..9e85b078265 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -6,19 +6,21 @@ import { REPO_AND_VERSION } from 'src/constants'; const BIDDER_CODE = 'sortable'; const SERVER_URL = 'c.deployads.com'; -const SORTABLE_CONFIG = config.getConfig('sortable') || {}; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function(bid) { - const haveSiteId = !!SORTABLE_CONFIG.siteId || bid.params.siteId; + 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 => { @@ -50,7 +52,7 @@ export const spec = { page: loc.href, ref: utils.getTopWindowReferrer(), publisher: { - id: SORTABLE_CONFIG.siteId || validBidReqs[0].params.siteId, + id: globalSiteId || validBidReqs[0].params.siteId, }, device: { w: screen.width, @@ -113,9 +115,9 @@ export const spec = { }, getUserSyncs: (syncOptions, responses, gdprConsent) => { - const siteId = SORTABLE_CONFIG.siteId; - if (syncOptions.iframeEnabled && siteId) { - let syncUrl = `//${SERVER_URL}/sync?f=html&s=${siteId}&u=${encodeURIComponent(utils.getTopWindowLocation())}`; + 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); From 43a8407ca3bad94106ca947e6be7c83404c51a93 Mon Sep 17 00:00:00 2001 From: Bryan Gahagan Date: Tue, 10 Jul 2018 20:52:51 -0400 Subject: [PATCH 5/5] support floor param --- modules/sortableBidAdapter.js | 5 ++++- test/spec/modules/sortableBidAdapter_spec.js | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 9e85b078265..8e5e221a9d4 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -32,6 +32,9 @@ export const spec = { }, 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; @@ -48,7 +51,7 @@ export const spec = { id: utils.getUniqueIdentifierStr(), imp: sortableImps, site: { - domain: loc.host, + domain: loc.hostname, page: loc.href, ref: utils.getTopWindowReferrer(), publisher: { diff --git a/test/spec/modules/sortableBidAdapter_spec.js b/test/spec/modules/sortableBidAdapter_spec.js index 65588f23a9b..9f351e9764c 100644 --- a/test/spec/modules/sortableBidAdapter_spec.js +++ b/test/spec/modules/sortableBidAdapter_spec.js @@ -66,6 +66,7 @@ describe('sortableBidAdapter', function() { 'params': { 'tagId': '403370', 'siteId': 'example.com', + 'floor': 0.21, 'keywords': { 'key1': 'val1', 'key2': 'val2' @@ -107,6 +108,7 @@ describe('sortableBidAdapter', function() { ]); 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); }); });