From bc72367369494860120a1ec06948ef26845f6192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Tue, 14 Nov 2017 00:14:39 +0200 Subject: [PATCH 1/8] Add ReadPeak Bid Adapter --- modules/readpeakBidAdapter.js | 237 +++++++++++++++++++ modules/readpeakBidAdapter.md | 29 +++ test/spec/modules/readpeakBidAdapter_spec.js | 198 ++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 modules/readpeakBidAdapter.js create mode 100644 modules/readpeakBidAdapter.md create mode 100644 test/spec/modules/readpeakBidAdapter_spec.js diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js new file mode 100644 index 00000000000..e121bc0bb90 --- /dev/null +++ b/modules/readpeakBidAdapter.js @@ -0,0 +1,237 @@ +import {logError, getTopWindowLocation} from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +export const ENDPOINT = 'https://app.readpeak.com/header/prebid'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 70, + DESCR_LEN: 120, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, + CTA_LEN: 50, +}; + +const BIDDER_CODE = 'readpeak' + +export const spec = { + + code: BIDDER_CODE, + + supportedMediaTypes: ['native'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.bidfloor && bid.params.publisherId && bid.nativeParams) + ), + + buildRequests: bidRequests => { + const request = { + id: bidRequests[0].bidderRequestId, + imp: bidRequests.map(slot => impression(slot)).filter(imp => imp.native != null), + site: site(bidRequests), + app: app(bidRequests), + device: device(), + isPrebid: true, + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(request), + } + }, + + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response) + ), +}; + +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + if (!bidResponse['body']) { + return [] + } + bidResponse = bidResponse.body + parse(bidRequest.data).imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = { + requestId: id, + cpm: idToBidMap[id].price, + creativeId: id, + adId: id, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: bidResponse.cur, + bidderCode: BIDDER_CODE, + }; + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + bids.push(bid); + } + }); + return bids; +} + +function impression(slot) { + return { + id: slot.bidId, + native: nativeImpression(slot), + bidfloor: slot.params.bidfloor, + bidfloorcur: 'USD' + }; +} + +function nativeImpression(slot) { + if (slot.nativeParams) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, imageAsset(2, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, dataAsset(4, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(5, slot.nativeParams.cta, 12, NATIVE_DEFAULTS.CTA_LEN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, + }, + }; + } + return null; +} + +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.publisherId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: pubId.toString(), + ref: referrer(), + page: getTopWindowLocation().href, + domain: getTopWindowLocation().hostname + } + } + return null; +} + +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.publisherId : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + } + } + return null; +} + +function referrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +} + +function device() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + if (typeof rawResponse === 'object') { + return rawResponse + } else { + return JSON.parse(rawResponse); + } + } + } catch (ex) { + logError('readpeakBidAdapter.safeParse', 'ERROR', ex); + } + return null; +} + +function nativeResponse(imp, bid) { + if (imp && imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + if (nativeAd && nativeAd.assets) { + nativeAd.assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.id === 4 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.image = asset.img && asset.id === 2 ? asset.img.url : keys.image; + keys.cta = asset.data && asset.id === 5 ? asset.data.value : keys.cta; + }); + if (nativeAd.link) { + keys.clickUrl = encodeURIComponent(nativeAd.link.url); + } + keys.impressionTrackers = nativeAd.imptrackers; + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/modules/readpeakBidAdapter.md b/modules/readpeakBidAdapter.md new file mode 100644 index 00000000000..f8e01027793 --- /dev/null +++ b/modules/readpeakBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +Module Name: ReadPeak Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: kurre.stahlberg@readpeak.com + +# Description + +Module that connects to ReadPeak's demand sources + +This adapter requires setup and approval from ReadPeak. +Please reach out to your account team or hello@readpeak.com for more information. + +# Test Parameters +```javascript + var adUnits = [{ + code: 'test-native', + mediaTypes: { native: { type: 'image' } }, + bids: [{ + bidder: 'readpeak', + params: { + bidfloor: 5.00, + publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + }] + }]; +``` diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js new file mode 100644 index 00000000000..9a1bf79168c --- /dev/null +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -0,0 +1,198 @@ +import { expect } from 'chai'; +import { spec, ENDPOINT } from 'modules/readpeakBidAdapter'; +import * as utils from 'src/utils'; + +describe('ReadPeakAdapter', () => { + let bidRequest + let serverResponse + let serverRequest + + beforeEach(() => { + bidRequest = { + bidder: 'readpeak', + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: { }, + body: {required: false}, + cta: {required: false}, + }, + params: { + bidfloor: 5.00, + publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + auctionId: '1d1a030790a475', + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + serverResponse = { + id: bidRequest.bidderRequestId, + cur: 'USD', + seatbid: [{ + bid: [{ + id: 'bidRequest.bidId', + impid: bidRequest.bidId, + price: 0.12, + adomain: ['readpeak.com'], + adm: { + assets: [{ + id: 1, + title: { + text: 'Title', + } + }, + { + id: 3, + data: { + type: 1, + value: 'Brand Name', + }, + }, + { + id: 4, + data: { + type: 2, + value: 'Description', + }, + }, + { + id: 2, + img: { + type: 3, + url: 'http://url.to/image', + w: 320, + h: 200, + }, + }], + link: { + url: 'http://url.to/target' + }, + imptrackers: [ + 'http://url.to/pixeltracker' + ], + } + }], + }], + } + serverRequest = { + method: 'POST', + url: 'http://localhost:60080/header/prebid', + data: JSON.stringify({ + 'id': '178e34bad3658f', + 'imp': [ + { + 'id': '2ffb201a808da7', + 'native': { + 'request': '{"assets":[{"id":1,"required":1,"title":{"len":200}},{"id":2,"required":0,"data":{"type":1,"len":50}},{"id":3,"required":0,"img":{"type":3,"wmin":100,"hmin":150}}]}', + 'ver': '1.1' + }, + 'bidfloor': 5, + 'bidfloorcur': 'USD' + } + ], + 'site': { + 'publisher': { + 'id': '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + 'id': '11bc5dd5-7421-4dd8-c926-40fa653bec76', + 'ref': '', + 'page': 'http://localhost:9876/?id=48509002', + 'domain': 'localhost' + }, + 'app': null, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36', + 'language': 'en-US' + }, + 'isPrebid': true + }) + } + }); + + describe('spec.isBidRequestValid', () => { + it('should return true when the required params are passed', () => { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false when the "bidfloor" param is missing', () => { + bidRequest.params = { + publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the "publisherId" param is missing', () => { + bidRequest.params = { + bidfloor: 5.00 + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when no bid params are passed', () => { + bidRequest.params = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when a bid request is not passed', () => { + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid({})).to.equal(false); + }); + }); + + describe('spec.buildRequests', () => { + it('should create a POST request for every bid', () => { + const request = spec.buildRequests([ bidRequest ]); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); + + it('should attach request data', () => { + const request = spec.buildRequests([ bidRequest ]); + + const data = JSON.parse(request.data); + expect(data.isPrebid).to.equal(true); + expect(data.id).to.equal(bidRequest.bidderRequestId) + expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.site).to.deep.equal({ + publisher: { + id: bidRequest.params.publisherId, + }, + id: bidRequest.params.publisherId, + ref: window.top.document.referrer, + page: utils.getTopWindowLocation().href, + domain: utils.getTopWindowLocation().hostname, + }); + expect(data.device).to.deep.contain({ ua: navigator.userAgent }); + }); + }); + + describe('spec.interpretResponse', () => { + it('should return no bids if the response is not valid', () => { + const bidResponse = spec.interpretResponse({ body: null }, serverRequest); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid bid response', () => { + const bidResponse = spec.interpretResponse({ body: serverResponse }, serverRequest)[0]; + expect(bidResponse).to.contain({ + requestId: bidRequest.bidId, + cpm: serverResponse.seatbid[0].bid[0].price, + creativeId: bidRequest.bidId, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: serverResponse.cur, + bidderCode: spec.code, + }); + + expect(bidResponse.native.title).to.equal('Title') + expect(bidResponse.native.body).to.equal('Description') + expect(bidResponse.native.image).to.equal('http://url.to/image') + expect(bidResponse.native.clickUrl).to.equal('http%3A%2F%2Furl.to%2Ftarget') + expect(bidResponse.native.impressionTrackers).to.contain('http://url.to/pixeltracker') + }); + }); +}); From 46a98f0ece7f7e28cf465558783a16d9bb902201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Wed, 15 Nov 2017 15:55:15 +0200 Subject: [PATCH 2/8] Remove protocol from endpoint address --- modules/readpeakBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index e121bc0bb90..a4635a9bde9 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -1,7 +1,7 @@ import {logError, getTopWindowLocation} from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; -export const ENDPOINT = 'https://app.readpeak.com/header/prebid'; +export const ENDPOINT = '//app.readpeak.com/header/prebid'; const NATIVE_DEFAULTS = { TITLE_LEN: 70, From ff814adeb4284eebf1144a502e0723320385686a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Thu, 16 Nov 2017 19:15:12 +0200 Subject: [PATCH 3/8] Make bidfloor optional --- modules/readpeakBidAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index a4635a9bde9..a57ea6c670d 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -21,7 +21,7 @@ export const spec = { supportedMediaTypes: ['native'], isBidRequestValid: bid => ( - !!(bid && bid.params && bid.params.bidfloor && bid.params.publisherId && bid.nativeParams) + !!(bid && bid.params && bid.params.publisherId && bid.nativeParams) ), buildRequests: bidRequests => { @@ -86,7 +86,7 @@ function impression(slot) { return { id: slot.bidId, native: nativeImpression(slot), - bidfloor: slot.params.bidfloor, + bidfloor: slot.params.bidfloor || 0, bidfloorcur: 'USD' }; } From ce9b2a38bdcd42aaeefea5febc7a4078cd5e84a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Thu, 16 Nov 2017 19:44:39 +0200 Subject: [PATCH 4/8] Update ReadPeak Bidder tests --- test/spec/modules/readpeakBidAdapter_spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index 9a1bf79168c..dd8314203f2 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -116,10 +116,8 @@ describe('ReadPeakAdapter', () => { expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - it('should return false when the "bidfloor" param is missing', () => { - bidRequest.params = { - publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76' - }; + it('should return false when the native params are missing', () => { + bidRequest.nativeParams = undefined; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); From 207a9ab1cbc328381a367a60ad7273b4898e4ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Wed, 29 Nov 2017 00:40:40 +0200 Subject: [PATCH 5/8] Fix creative id, remove ad id from response. --- modules/readpeakBidAdapter.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index a57ea6c670d..68f3f9299e8 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -67,15 +67,14 @@ function bidResponseAvailable(bidRequest, bidResponse) { const bid = { requestId: id, cpm: idToBidMap[id].price, - creativeId: id, - adId: id, + creativeId: idToBidMap[id].crid, ttl: 300, netRevenue: true, mediaType: 'native', currency: bidResponse.cur, bidderCode: BIDDER_CODE, + native: nativeResponse(idToImpMap[id], idToBidMap[id]), }; - bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); bids.push(bid); } }); @@ -87,7 +86,7 @@ function impression(slot) { id: slot.bidId, native: nativeImpression(slot), bidfloor: slot.params.bidfloor || 0, - bidfloorcur: 'USD' + bidfloorcur: slot.params.bidfloorcur || 'USD' }; } From e2449a337f5f991765359d573b63a14a19457ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Wed, 29 Nov 2017 01:02:14 +0200 Subject: [PATCH 6/8] Fix tests --- test/spec/modules/readpeakBidAdapter_spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index dd8314203f2..bf827ac1c28 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -35,6 +35,8 @@ describe('ReadPeakAdapter', () => { id: 'bidRequest.bidId', impid: bidRequest.bidId, price: 0.12, + cid: '12', + crid: '123', adomain: ['readpeak.com'], adm: { assets: [{ @@ -178,7 +180,7 @@ describe('ReadPeakAdapter', () => { expect(bidResponse).to.contain({ requestId: bidRequest.bidId, cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: bidRequest.bidId, + creativeId: serverResponse.seatbid[0].bid[0].crid, ttl: 300, netRevenue: true, mediaType: 'native', From 75a8208410cb9e44aaba43bf53bf7ce66771e119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Wed, 29 Nov 2017 19:18:48 +0200 Subject: [PATCH 7/8] Drop bidderCode --- modules/readpeakBidAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 68f3f9299e8..d19570d16ca 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -72,7 +72,6 @@ function bidResponseAvailable(bidRequest, bidResponse) { netRevenue: true, mediaType: 'native', currency: bidResponse.cur, - bidderCode: BIDDER_CODE, native: nativeResponse(idToImpMap[id], idToBidMap[id]), }; bids.push(bid); From de02c081f078b45f509f7517dc760ed781bce355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurre=20Sta=CC=8Ahlberg?= Date: Wed, 29 Nov 2017 19:23:39 +0200 Subject: [PATCH 8/8] Fix tests --- test/spec/modules/readpeakBidAdapter_spec.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index bf827ac1c28..7356cd96a4e 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -184,8 +184,7 @@ describe('ReadPeakAdapter', () => { ttl: 300, netRevenue: true, mediaType: 'native', - currency: serverResponse.cur, - bidderCode: spec.code, + currency: serverResponse.cur }); expect(bidResponse.native.title).to.equal('Title')