diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js new file mode 100644 index 00000000000..e9fd3c42312 --- /dev/null +++ b/modules/readpeakBidAdapter.js @@ -0,0 +1,311 @@ +import { logError, replaceAuctionPrice } from '../src/utils'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { config } from '../src/config'; +import { NATIVE } from '../src/mediaTypes'; +import { parse as parseUrl } from '../src/url'; + +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.publisherId && bid.nativeParams), + + buildRequests: (bidRequests, bidderRequest) => { + const currencyObj = config.getConfig('currency'); + const currency = (currencyObj && currencyObj.adServerCurrency) || 'USD'; + + const request = { + id: bidRequests[0].bidderRequestId, + imp: bidRequests + .map(slot => impression(slot)) + .filter(imp => imp.native != null), + site: site(bidRequests, bidderRequest), + app: app(bidRequests), + device: device(), + cur: [currency], + source: { + fd: 1, + tid: bidRequests[0].transactionId, + ext: { + prebid: '$prebid.version$' + } + } + }; + + 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, + slot.nativeParams.wmin || NATIVE_DEFAULTS.IMG_MIN, + slot.nativeParams.hmin || 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(bidRequests, bidderRequest) { + const url = + config.getConfig('pageUrl') || + (bidderRequest && + bidderRequest.refererInfo && + bidderRequest.refererInfo.referer); + + const pubId = + bidRequests && bidRequests.length > 0 + ? bidRequests[0].params.publisherId + : '0'; + const siteId = + bidRequests && bidRequests.length > 0 ? bidRequests[0].params.siteId : '0'; + const appParams = bidRequests[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: config.getConfig('publisherDomain') + }, + id: siteId ? siteId.toString() : pubId.toString(), + page: url, + domain: + (url && parseUrl(url).hostname) || config.getConfig('publisherDomain') + }; + } + return undefined; +} + +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 undefined; +} + +function isMobile() { + return /(ios|ipod|ipad|iphone|android)/i.test(global.navigator.userAgent); +} + +function isConnectedTV() { + return /(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i.test( + global.navigator.userAgent + ); +} + +function device() { + return { + ua: navigator.userAgent, + language: + navigator.language || + navigator.browserLanguage || + navigator.userLanguage || + navigator.systemLanguage, + devicetype: isMobile() ? 1 : isConnectedTV() ? 3 : 2 + }; +} + +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 + ? { + url: asset.img.url, + width: asset.img.w || 750, + height: asset.img.h || 500 + } + : keys.image; + keys.cta = asset.data && asset.id === 5 ? asset.data.value : keys.cta; + }); + if (nativeAd.link) { + keys.clickUrl = encodeURIComponent(nativeAd.link.url); + } + const trackers = nativeAd.imptrackers || []; + trackers.unshift(replaceAuctionPrice(bid.burl, bid.price)); + keys.impressionTrackers = trackers; + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js new file mode 100644 index 00000000000..06971169e2a --- /dev/null +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -0,0 +1,233 @@ +import { expect } from 'chai'; +import { spec, ENDPOINT } from 'modules/readpeakBidAdapter'; +import { config } from 'src/config'; +import { parse as parseUrl } from 'src/url'; + +describe('ReadPeakAdapter', function() { + let bidRequest; + let serverResponse; + let serverRequest; + let bidderRequest; + + beforeEach(function() { + bidderRequest = { + refererInfo: { + referer: 'https://publisher.com/home' + } + }; + + bidRequest = { + bidder: 'readpeak', + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: {}, + body: { required: false }, + cta: { required: false } + }, + params: { + bidfloor: 5.0, + publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', + siteId: '11bc5dd5-7421-4dd8-c926-40fa653bec77' + }, + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + auctionId: '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: 750, + h: 500 + } + } + ], + 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-40fa653bec77', + ref: '', + page: 'http://localhost', + 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', function() { + it('should return true when the required params are passed', function() { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false when the native params are missing', function() { + bidRequest.nativeParams = undefined; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the "publisherId" param is missing', function() { + bidRequest.params = { + bidfloor: 5.0 + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when no bid params are passed', function() { + bidRequest.params = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when a bid request is not passed', function() { + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid({})).to.equal(false); + }); + }); + + describe('spec.buildRequests', function() { + it('should create a POST request for every bid', function() { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); + + it('should attach request data', function() { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + 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, + domain: 'http://localhost:9876' + }, + id: bidRequest.params.siteId, + page: bidderRequest.refererInfo.referer, + domain: parseUrl(bidderRequest.refererInfo.referer).hostname + }); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language + }); + expect(data.cur).to.deep.equal(['EUR']); + }); + }); + + describe('spec.interpretResponse', function() { + it('should return no bids if the response is not valid', function() { + const bidResponse = spec.interpretResponse({ body: null }, serverRequest); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid bid response', function() { + 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.deep.equal({ + url: 'http://url.to/image', + width: 750, + height: 500 + }); + expect(bidResponse.native.clickUrl).to.equal( + 'http%3A%2F%2Furl.to%2Ftarget' + ); + expect(bidResponse.native.impressionTrackers).to.contain( + 'http://url.to/pixeltracker' + ); + }); + }); +});