diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js new file mode 100644 index 00000000000..d19570d16ca --- /dev/null +++ b/modules/readpeakBidAdapter.js @@ -0,0 +1,235 @@ +import {logError, getTopWindowLocation} from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +export const ENDPOINT = '//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.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: idToBidMap[id].crid, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: bidResponse.cur, + 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 || 0, + bidfloorcur: slot.params.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..7356cd96a4e --- /dev/null +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -0,0 +1,197 @@ +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, + cid: '12', + crid: '123', + 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 native params are missing', () => { + bidRequest.nativeParams = undefined; + 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: serverResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: serverResponse.cur + }); + + 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') + }); + }); +});