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');
+ });
+ });
+});