From 127ebb7424986569417cfad471282b469af37570 Mon Sep 17 00:00:00 2001 From: andbeyondmedia <38574037+andbeyondmedia@users.noreply.github.com> Date: Mon, 4 Jun 2018 08:00:06 -0700 Subject: [PATCH] Adding new Bidder Andbeyond (#2671) * Create andbeyondBidAdapter.js * Create andbeyondBidAdapter_spec.js * Create andbeyondBidAdapter.md * Update andbeyondBidAdapter.md * Update andbeyondBidAdapter_spec.js * Update andbeyondBidAdapter_spec.js * Update andbeyondBidAdapter.js * Update andbeyondBidAdapter_spec.js --- modules/andbeyondBidAdapter.js | 168 ++++++++++++++ modules/andbeyondBidAdapter.md | 32 +++ test/spec/modules/andbeyondBidAdapter_spec.js | 208 ++++++++++++++++++ 3 files changed, 408 insertions(+) create mode 100644 modules/andbeyondBidAdapter.js create mode 100644 modules/andbeyondBidAdapter.md create mode 100644 test/spec/modules/andbeyondBidAdapter_spec.js diff --git a/modules/andbeyondBidAdapter.js b/modules/andbeyondBidAdapter.js new file mode 100644 index 000000000000..1a3535f47047 --- /dev/null +++ b/modules/andbeyondBidAdapter.js @@ -0,0 +1,168 @@ +import * as utils from 'src/utils'; +import { BANNER } from 'src/mediaTypes'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import find from 'core-js/library/fn/array/find'; + +const VERSION = '1.1'; + +/** + * Adapter for requesting bids from andbeyond white-label display platform + */ +export const spec = { + + code: 'andbeyond', + aliases: ['headbidding'], + supportedMediaTypes: [BANNER], + isBidRequestValid: function(bidRequest) { + return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' && + 'zoneId' in bidRequest.params && !isNaN(Number(bidRequest.params.zoneId)); + }, + buildRequests: function(bidRequests) { + let auctionId; + let dispatch = bidRequests.map(buildImp) + .reduce((acc, curr, index) => { + let bidRequest = bidRequests[index]; + let zoneId = bidRequest.params.zoneId; + let host = bidRequest.params.host; + acc[host] = acc[host] || {}; + acc[host][zoneId] = acc[host][zoneId] || []; + acc[host][zoneId].push(curr); + auctionId = bidRequest.bidderRequestId; + return acc; + }, {}); + let requests = []; + Object.keys(dispatch).forEach(host => { + Object.keys(dispatch[host]).forEach(zoneId => { + const request = buildRtbRequest(dispatch[host][zoneId], auctionId); + requests.push({ + method: 'GET', + url: `${window.location.protocol}//${host}/rtbg`, + data: { + zone: Number(zoneId), + ad_type: 'rtb', + v: VERSION, + r: JSON.stringify(request) + } + }); + }); + }); + return requests; + }, + interpretResponse: function(serverResponse, request) { + let response = serverResponse.body; + if (!response.seatbid) { + return []; + } + + let rtbRequest = JSON.parse(request.data.r); + let rtbImps = rtbRequest.imp; + let rtbBids = response.seatbid + .map(seatbid => seatbid.bid) + .reduce((a, b) => a.concat(b), []); + + return rtbBids.map(rtbBid => { + let imp = find(rtbImps, imp => imp.id === rtbBid.impid); + let prBid = { + requestId: rtbBid.impid, + cpm: rtbBid.price, + creativeId: rtbBid.crid, + currency: 'USD', + ttl: 360, + netRevenue: true + }; + if ('banner' in imp) { + prBid.mediaType = BANNER; + prBid.width = rtbBid.w; + prBid.height = rtbBid.h; + prBid.ad = formatAdMarkup(rtbBid); + } + return prBid; + }); + }, + getUserSyncs: function(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled || !serverResponses || serverResponses.length === 0) { + return []; + } + return serverResponses.filter(rsp => rsp.body && rsp.body.ext && rsp.body.ext.adk_usersync) + .map(rsp => rsp.body.ext.adk_usersync) + .reduce((a, b) => a.concat(b), []) + .map(sync_url => ({type: 'iframe', url: sync_url})); + } +}; + +registerBidder(spec); + +/** + * Builds parameters object for single impression + */ +function buildImp(bid) { + const sizes = bid.sizes; + const imp = { + 'id': bid.bidId, + 'tagid': bid.placementCode + }; + + imp.banner = { + format: sizes.map(s => ({'w': s[0], 'h': s[1]})), + topframe: 0 + }; + if (utils.getTopWindowLocation().protocol === 'https:') { + imp.secure = 1; + } + return imp; +} + +/** + * Builds complete rtb request + * @param imps collection of impressions + * @param auctionId + */ +function buildRtbRequest(imps, auctionId) { + let req = { + 'id': auctionId, + 'imp': imps, + 'site': createSite(), + 'at': 1, + 'device': { + 'ip': 'caller', + 'ua': 'caller', + 'js': 1, + 'language': getLanguage() + }, + 'ext': { + 'adk_usersync': 1 + } + }; + if (utils.getDNT()) { + req.device.dnt = 1; + } + return req; +} + +function getLanguage() { + const language = navigator.language ? 'language' : 'userLanguage'; + return navigator[language].split('-')[0]; +} + +/** + * Creates site description object + */ +function createSite() { + var location = utils.getTopWindowLocation(); + return { + 'domain': location.hostname, + 'page': location.href.split('?')[0] + }; +} + +/** + * Format creative with optional nurl call + * @param bid rtb Bid object + */ +function formatAdMarkup(bid) { + var adm = bid.adm; + if ('nurl' in bid) { + adm += utils.createTrackPixelHtml(`${bid.nurl}&px=1`); + } + return `${adm}`; +} diff --git a/modules/andbeyondBidAdapter.md b/modules/andbeyondBidAdapter.md new file mode 100644 index 000000000000..7d58bac0abcb --- /dev/null +++ b/modules/andbeyondBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: andbeyond Bidder Adapter +Module Type: Bidder Adapter +Maintainer: shreyanschopra@rtbdemand.com +``` + +# Description + +Connects to andbeyond whitelabel platform. +Banner formats are supported. + + +# Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + sizes: [[300, 250]], // banner size + bids: [ + { + bidder: 'andbeyond', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/andbeyondBidAdapter_spec.js b/test/spec/modules/andbeyondBidAdapter_spec.js new file mode 100644 index 000000000000..5e58101ef664 --- /dev/null +++ b/test/spec/modules/andbeyondBidAdapter_spec.js @@ -0,0 +1,208 @@ +import {expect} from 'chai'; +import {spec} from 'modules/andbeyondBidAdapter'; +import * as utils from 'src/utils'; + +describe('andbeyond adapter', () => { + const bid1_zone1 = { + bidder: 'andbeyond', + bidId: 'Bid_01', + params: {zoneId: 1, host: 'rtb.andbeyond.com'}, + placementCode: 'ad-unit-1', + sizes: [[300, 250], [300, 200]] + }, bid2_zone2 = { + bidder: 'andbeyond', + bidId: 'Bid_02', + params: {zoneId: 2, host: 'rtb.andbeyond.com'}, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] + }, bid3_host2 = { + bidder: 'andbeyond', + bidId: 'Bid_02', + params: {zoneId: 1, host: 'rtb-private.andbeyond.com'}, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] + }, bid_without_zone = { + bidder: 'andbeyond', + bidId: 'Bid_W', + params: {host: 'rtb-private.andbeyond.com'}, + placementCode: 'ad-unit-1', + sizes: [[728, 90]] + }, bid_without_host = { + bidder: 'andbeyond', + bidId: 'Bid_W', + params: {zoneId: 1}, + placementCode: 'ad-unit-1', + sizes: [[728, 90]] + }, bid_with_wrong_zoneId = { + bidder: 'andbeyond', + bidId: 'Bid_02', + params: {zoneId: 'wrong id', host: 'rtb.andbeyond.com'}, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] + }, usersyncOnlyResponse = { + id: 'nobid1', + ext: { + adk_usersync: ['http://adk.sync.com/sync'] + } + }; + + const bidResponse1 = { + id: 'bid1', + seatbid: [{ + bid: [{ + id: '1', + impid: 'Bid_01', + crid: '100_001', + price: 3.01, + nurl: 'https://rtb.com/win?i=ZjKoPYSFI3Y_0', + adm: '', + w: 300, + h: 250 + }] + }], + cur: 'USD', + ext: { + adk_usersync: ['http://adk.sync.com/sync'] + } + }, bidResponse2 = { + id: 'bid2', + seatbid: [{ + bid: [{ + id: '2', + impid: 'Bid_02', + crid: '100_002', + price: 1.31, + adm: '', + w: 300, + h: 250 + }] + }], + cur: 'USD' + }; + + describe('input parameters validation', () => { + it('empty request shouldn\'t generate exception', () => { + expect(spec.isBidRequestValid({ + bidderCode: 'andbeyond' + })).to.be.equal(false); + }); + + it('request without zone shouldn\'t issue a request', () => { + expect(spec.isBidRequestValid(bid_without_zone)).to.be.equal(false); + }); + + it('request without host shouldn\'t issue a request', () => { + expect(spec.isBidRequestValid(bid_without_host)).to.be.equal(false); + }); + + it('empty request shouldn\'t generate exception', () => { + expect(spec.isBidRequestValid(bid_with_wrong_zoneId)).to.be.equal(false); + }); + }); + + describe('banner request building', () => { + let bidRequest; + before(() => { + let wmock = sinon.stub(utils, 'getTopWindowLocation').callsFake(() => ({ + protocol: 'https:', + hostname: 'example.com', + host: 'example.com', + pathname: '/index.html', + href: 'https://example.com/index.html' + })); + let dntmock = sinon.stub(utils, 'getDNT').callsFake(() => true); + let request = spec.buildRequests([bid1_zone1])[0]; + bidRequest = JSON.parse(request.data.r); + wmock.restore(); + dntmock.restore(); + }); + + it('should be a first-price auction', () => { + expect(bidRequest).to.have.property('at', 1); + }); + + it('should have banner object', () => { + expect(bidRequest.imp[0]).to.have.property('banner'); + }); + + it('should have w/h', () => { + expect(bidRequest.imp[0].banner).to.have.property('format'); + expect(bidRequest.imp[0].banner.format).to.be.eql([{w: 300, h: 250}, {w: 300, h: 200}]); + }); + + it('should respect secure connection', () => { + expect(bidRequest.imp[0]).to.have.property('secure', 1); + }); + + it('should have tagid', () => { + expect(bidRequest.imp[0]).to.have.property('tagid', 'ad-unit-1'); + }); + + it('should create proper site block', () => { + expect(bidRequest.site).to.have.property('domain', 'example.com'); + expect(bidRequest.site).to.have.property('page', 'https://example.com/index.html'); + }); + + it('should fill device with caller macro', () => { + expect(bidRequest).to.have.property('device'); + expect(bidRequest.device).to.have.property('ip', 'caller'); + expect(bidRequest.device).to.have.property('ua', 'caller'); + expect(bidRequest.device).to.have.property('dnt', 1); + }); + }); + + describe('requests routing', () => { + it('should issue a request for each host', () => { + let pbRequests = spec.buildRequests([bid1_zone1, bid3_host2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string(`//${bid1_zone1.params.host}/`); + expect(pbRequests[1].url).to.have.string(`//${bid3_host2.params.host}/`); + }); + + it('should issue a request for each zone', () => { + let pbRequests = spec.buildRequests([bid1_zone1, bid2_zone2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].data.zone).to.be.equal(bid1_zone1.params.zoneId); + expect(pbRequests[1].data.zone).to.be.equal(bid2_zone2.params.zoneId); + }); + }); + + describe('responses processing', () => { + it('should return fully-initialized banner bid-response', () => { + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: bidResponse1}, request)[0]; + expect(resp).to.have.property('requestId', 'Bid_01'); + expect(resp).to.have.property('cpm', 3.01); + expect(resp).to.have.property('width', 300); + expect(resp).to.have.property('height', 250); + expect(resp).to.have.property('creativeId', '100_001'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('mediaType', 'banner'); + expect(resp).to.have.property('ad'); + expect(resp.ad).to.have.string(''); + }); + + it('should add nurl as pixel for banner response', () => { + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: bidResponse1}, request)[0]; + let expectedNurl = bidResponse1.seatbid[0].bid[0].nurl + '&px=1'; + expect(resp.ad).to.have.string(expectedNurl); + }); + + it('should handle bidresponse with user-sync only', () => { + let request = spec.buildRequests([bid1_zone1])[0]; + let resp = spec.interpretResponse({body: usersyncOnlyResponse}, request); + expect(resp).to.have.length(0); + }); + + it('should perform usersync', () => { + let syncs = spec.getUserSyncs({iframeEnabled: false}, [{body: bidResponse1}]); + expect(syncs).to.have.length(0); + syncs = spec.getUserSyncs({iframeEnabled: true}, [{body: bidResponse1}]); + expect(syncs).to.have.length(1); + expect(syncs[0]).to.have.property('type', 'iframe'); + expect(syncs[0]).to.have.property('url', 'http://adk.sync.com/sync'); + }); + }); +});