From d48eb30f59c1b0d46b05f3962605d83d2fd0b87f Mon Sep 17 00:00:00 2001 From: Dima Shirokov Date: Tue, 20 Aug 2019 20:22:27 +0200 Subject: [PATCH] Add Meazy bid adapter (#4015) * add meazy bid adapter * fix dealid * jsnellbaker review * jsnellbaker review * jsnellbaker review --- modules/meazyBidAdapter.js | 149 ++++++++++++++++++ modules/meazyBidAdapter.md | 23 +++ test/spec/modules/meazyBidAdapter_spec.js | 177 ++++++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 modules/meazyBidAdapter.js create mode 100644 modules/meazyBidAdapter.md create mode 100644 test/spec/modules/meazyBidAdapter_spec.js diff --git a/modules/meazyBidAdapter.js b/modules/meazyBidAdapter.js new file mode 100644 index 00000000000..8604ef770da --- /dev/null +++ b/modules/meazyBidAdapter.js @@ -0,0 +1,149 @@ +import * as utils from '../src/utils'; +import { registerBidder } from '../src/adapters/bidderFactory'; +import { BANNER } from '../src/mediaTypes'; + +const BIDDER_CODE = 'meazy'; +const PREBID_ENDPOINT = 'rtb-filter.meazy.co'; +const SYNC_ENDPOINT = '//sync.meazy.co/sync/iframe'; +const ENDPOINT_CONFIG = { + defaultCurrency: ['USD'], + availableSize: ['300x250', '320x480', '160x600'] +}; + +const buildURI = (pid) => { + return `//${PREBID_ENDPOINT}/pbjs?host=${utils.getOrigin()}&api_key=${pid}`; +} + +const validateSize = (size) => { + return ENDPOINT_CONFIG.availableSize.indexOf(size.join('x')) !== -1; +} + +const buildImpression = (bidRequest) => { + const impression = { + id: utils.getUniqueIdentifierStr(), + tagid: bidRequest.adUnitCode, + banner: { + format: bidRequest.sizes.map(size => ({ w: size[0], h: size[1] })) + } + }; + + return impression; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!bid.params.pid && bid.sizes.some(validateSize); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests, bidderRequest) { + const payload = { + id: bidRequests[0].bidId, + site: { + domain: utils.getOrigin() + }, + device: { + w: window.screen.width, + h: window.screen.height, + language: navigator.language + }, + cur: ENDPOINT_CONFIG.defaultCurrency, + imp: bidRequests.map(buildImpression), + user: {} + }; + + if (bidderRequest.refererInfo) { + if (bidderRequest.refererInfo.referer) { + payload.site.ref = bidderRequest.refererInfo.referer; + } + + if (utils.isArray(bidderRequest.refererInfo) && bidderRequest.refererInfo.stack.length > 0) { + payload.site.page = bidderRequest.refererInfo.stack[0]; + } + } + + if (utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + payload.user.ext = { + consent: bidderRequest.gdprConsent.consentString, + gdpr: bidderRequest.gdprConsent.gdprApplies & 1 + } + } + + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: buildURI(bidRequests[0].params.pid), + data: payloadString + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse) { + const bids = []; + + if (!utils.isArray(serverResponse.body.seatbid) || !serverResponse.body.seatbid[0]) { + return bids; + } + + serverResponse.body.seatbid[0].bid.forEach(bidResponse => { + const bid = { + requestId: serverResponse.body.id, + cpm: bidResponse.price, + width: bidResponse.w, + height: bidResponse.h, + creativeId: bidResponse.crid, + netRevenue: bidResponse.netRevenue !== undefined ? bidResponse.netRevenue : true, + dealId: bidResponse.dealid, + currency: ENDPOINT_CONFIG.defaultCurrency[0], + ttl: bidResponse.exp || 900, + ad: bidResponse.adm + } + + bids.push(bid); + }); + + return bids; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + + if (syncOptions.pixelEnabled && serverResponses[0] && utils.deepAccess(serverResponses[0], 'body.ext.syncUrl')) { + syncs.push({ + type: 'image', + url: serverResponses[0].body.ext.syncUrl + }); + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: SYNC_ENDPOINT + }); + } + + return syncs; + } +} + +registerBidder(spec); diff --git a/modules/meazyBidAdapter.md b/modules/meazyBidAdapter.md new file mode 100644 index 00000000000..354673bf590 --- /dev/null +++ b/modules/meazyBidAdapter.md @@ -0,0 +1,23 @@ +# Overview + +Module Name: Meazy Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dima@meazy.co + +# Description + +Module that connects to Meazy demand sources + +# Test Parameters +``` +var adUnits = [{ + code: 'test-div', + sizes: [[300, 250]], + bids: [{ + bidder: "meazy", + params: { + pid: '6910b7344ae566a1' + } + }] +}]; +``` \ No newline at end of file diff --git a/test/spec/modules/meazyBidAdapter_spec.js b/test/spec/modules/meazyBidAdapter_spec.js new file mode 100644 index 00000000000..9abe37ece62 --- /dev/null +++ b/test/spec/modules/meazyBidAdapter_spec.js @@ -0,0 +1,177 @@ +import * as utils from 'src/utils'; +import { expect } from 'chai'; +import { spec } from 'modules/meazyBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const MEAZY_PID = '6910b7344ae566a1' +const VALID_ENDPOINT = `//rtb-filter.meazy.co/pbjs?host=${utils.getOrigin()}&api_key=${MEAZY_PID}`; + +const bidderRequest = { + refererInfo: { + referer: 'page', + stack: ['page', 'page1'] + } +}; + +const bidRequest = { + bidder: 'meazy', + adUnitCode: 'test-div', + sizes: [[300, 250], [300, 600]], + params: { + pid: MEAZY_PID + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', +}; + +const bidContent = { + 'id': '30b31c1838de1e', + 'bidid': '9780a52ff05c0e92780f5baf9cf3f4e8', + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'id': 'ccf05fb8effb3d02', + 'impid': 'B19C34BBD69DAF9F', + 'burl': 'https://track.meazy.co/imp?bidid=9780a52ff05c0e92780f5baf9cf3f4e8&user=fdc401a2-92f1-42bd-ac22-d570520ad0ec&burl=1&ssp=5&project=2&cost=${AUCTION_PRICE}', + 'adm': '', + 'adid': 'ad-2.6.75.300x250', + 'price': 1.5, + 'w': 300, + 'h': 250, + 'cid': '2.6.75', + 'crid': '2.6.75.300x250', + 'dealid': 'default' + }], + 'seat': '2' + }] +}; + +const bidContentExt = { + ...bidContent, + ext: { + 'syncUrl': '//sync.meazy.co/sync/img?api_key=6910b7344ae566a1' + } +}; + +const bidResponse = { + body: bidContent +}; + +const noBidResponse = { body: {'nbr': 2} }; + +describe('meazyBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return false', function () { + let bid = Object.assign({}, bidRequest); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + it('should format valid url', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.url).to.equal(VALID_ENDPOINT); + }); + + it('should format valid url', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.url).to.equal(VALID_ENDPOINT); + }); + + it('should format valid request body', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.id).to.exist; + expect(payload.imp).to.exist; + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].banner).to.exist; + expect(payload.imp[0].banner.format).to.exist; + expect(payload.device).to.exist; + expect(payload.site).to.exist; + expect(payload.site.domain).to.exist; + expect(payload.cur).to.exist; + }); + + it('should format valid url', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + expect(request.url).to.equal(VALID_ENDPOINT); + }); + + it('should not fill user.ext object', function () { + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.equal(undefined); + }); + + it('should fill user.ext object', function () { + const consentString = 'hellogdpr'; + const request = spec.buildRequests([bidRequest], { ...bidderRequest, gdprConsent: { gdprApplies: true, consentString } }); + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.exist.and.to.be.a('object'); + expect(payload.user.ext.consent).to.equal(consentString); + expect(payload.user.ext.gdpr).to.equal(1); + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid response', function () { + const result = spec.interpretResponse(bidResponse); + const validResponse = [{ + requestId: '30b31c1838de1e', + cpm: 1.5, + width: 300, + height: 250, + creativeId: '2.6.75.300x250', + netRevenue: true, + dealId: 'default', + currency: 'USD', + ttl: 900, + ad: '' + }]; + + expect(result).to.deep.equal(validResponse); + }); + + it('handles nobid responses', function () { + let result = spec.interpretResponse(noBidResponse); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs', function () { + const syncOptionsFF = { iframeEnabled: false }; + const syncOptionsEF = { iframeEnabled: true }; + const syncOptionsEE = { pixelEnabled: true, iframeEnabled: true }; + const syncOptionsFE = { pixelEnabled: true, iframeEnabled: false }; + + const successIFrame = { type: 'iframe', url: '//sync.meazy.co/sync/iframe' }; + const successPixel = { type: 'image', url: '//sync.meazy.co/sync/img?api_key=6910b7344ae566a1' }; + + it('should return an empty array', function () { + expect(spec.getUserSyncs(syncOptionsFF, [])).to.be.empty; + expect(spec.getUserSyncs(syncOptionsFF, [ bidResponse ])).to.be.empty; + expect(spec.getUserSyncs(syncOptionsFE, [ bidResponse ])).to.be.empty; + }); + + it('should be equal to the expected result', function () { + expect(spec.getUserSyncs(syncOptionsEF, [ bidResponse ])).to.deep.equal([successIFrame]); + expect(spec.getUserSyncs(syncOptionsFE, [ { body: bidContentExt } ])).to.deep.equal([successPixel]); + expect(spec.getUserSyncs(syncOptionsEE, [ { body: bidContentExt } ])).to.deep.equal([successPixel, successIFrame]); + expect(spec.getUserSyncs(syncOptionsEE, [])).to.deep.equal([successIFrame]); + }) + }); +});