diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js new file mode 100644 index 00000000000..26c2393c662 --- /dev/null +++ b/modules/medianetBidAdapter.js @@ -0,0 +1,161 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +const BIDDER_CODE = 'medianet'; +const BID_URL = 'https://prebid.media.net/rtb/prebid'; + +function siteDetails(site) { + site = site || {}; + + return { + domain: site.domain || utils.getTopWindowLocation().host, + page: site.page || utils.getTopWindowUrl(), + ref: site.ref || utils.getTopWindowReferrer() + } +} + +function filterUrlsByType(urls, type) { + return urls.filter(url => url.type === type); +} + +function transformSizes(sizes) { + if (utils.isArray(sizes) && sizes.length === 2 && !utils.isArray(sizes[0])) { + return [getSize(sizes)]; + } + + return sizes.map(size => getSize(size)) +} + +function getSize(size) { + return { + w: parseInt(size[0], 10), + h: parseInt(size[1], 10) + } +} + +function configuredParams(params) { + return { + customer_id: params.cid + } +} + +function slotParams(bidRequest) { + // check with Media.net Account manager for bid floor and crid parameters + let params = { + id: bidRequest.bidId, + ext: { + dfp_id: bidRequest.adUnitCode + }, + banner: transformSizes(bidRequest.sizes) + }; + + if (bidRequest.params.crid) { + params.tagid = bidRequest.params.crid.toString(); + } + + let bidFloor = parseFloat(bidRequest.params.bidfloor); + if (bidFloor) { + params.bidfloor = bidFloor; + } + return params; +} + +function generatePayload(bidRequests) { + return { + site: siteDetails(bidRequests[0].params.site), + ext: configuredParams(bidRequests[0].params), + id: bidRequests[0].bidderRequestId, + imp: bidRequests.map(request => slotParams(request)) + } +} + +function isValidBid(bid) { + return bid.no_bid === false && parseFloat(bid.cpm) > 0.0; +} + +function fetchCookieSyncUrls(response) { + if (!utils.isEmpty(response) && response[0].body && + response[0].body.ext && utils.isArray(response[0].body.ext.csUrl)) { + return response[0].body.ext.csUrl; + } + + return []; +} + +export const spec = { + + code: BIDDER_CODE, + + /** + * 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 (if cid is present), and false otherwise. + */ + isBidRequestValid: function(bid) { + if (!bid.params) { + utils.logError(`${BIDDER_CODE} : Missing bid parameters`); + return false; + } + + if (!bid.params.cid || !utils.isStr(bid.params.cid) || utils.isEmptyStr(bid.params.cid)) { + utils.logError(`${BIDDER_CODE} : cid should be a string`); + return false; + } + + return true; + }, + + /** + * 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) { + let payload = generatePayload(bidRequests); + + return { + method: 'POST', + url: BID_URL, + data: JSON.stringify(payload) + }; + }, + + /** + * 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, request) { + let validBids = []; + + if (!serverResponse || !serverResponse.body) { + utils.logInfo(`${BIDDER_CODE} : response is empty`); + return validBids; + } + + let bids = serverResponse.body.bidList; + if (!utils.isArray(bids) || bids.length === 0) { + utils.logInfo(`${BIDDER_CODE} : no bids`); + return validBids; + } + validBids = bids.filter(bid => isValidBid(bid)); + + return validBids; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + let cookieSyncUrls = fetchCookieSyncUrls(serverResponses); + + if (syncOptions.iframeEnabled) { + return filterUrlsByType(cookieSyncUrls, 'iframe'); + } + + if (syncOptions.pixelEnabled) { + return filterUrlsByType(cookieSyncUrls, 'image'); + } + } +}; +registerBidder(spec); diff --git a/modules/medianetBidAdapter.md b/modules/medianetBidAdapter.md new file mode 100644 index 00000000000..2edb120033e --- /dev/null +++ b/modules/medianetBidAdapter.md @@ -0,0 +1,59 @@ +# Overview + +``` +Module Name: media.net Bid Adapter +Module Type: Bidder Adapter +Maintainer: vedant.s@media.net +``` + +# Description + +Connects to Media.net's exchange for bids. +This adapter currently only supports Banner Ads. + +# Sample Ad Unit: For Publishers +```javascript +var adUnits = [{ + code: 'media.net-hb-ad-123456-1', + sizes: [ + [300, 250], + [300, 600], + ], + bids: [{ + bidder: 'medianet', + params: { + cid: '', + bidfloor: '', + crid: '' + } + }] +}]; +``` + +# Ad Unit and Setup: For Testing + +```html + + + +``` \ No newline at end of file diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js new file mode 100644 index 00000000000..fd3513215e8 --- /dev/null +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -0,0 +1,329 @@ +import {expect} from 'chai'; +import {spec} from 'modules/medianetBidAdapter'; + +let VALID_BID_REQUEST = [{ + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'sizes': [[300, 250]], + 'bidId': '28f8f8130a583e', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }, { + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-123', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + 'sizes': [[300, 251]], + 'bidId': '3f97ca71b1e5c2', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }], + VALID_BID_REQUEST_INVALID_BIDFLOOR = [{ + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'bidfloor': 'abcdef', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'sizes': [[300, 250]], + 'bidId': '28f8f8130a583e', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }, { + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-123', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + 'sizes': [[300, 251]], + 'bidId': '3f97ca71b1e5c2', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }], + VALID_PAYLOAD = { + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + }, + 'ext': { + 'customer_id': 'customer_id' + }, + 'id': '1e9b1f07797c1c', + 'imp': [{ + 'id': '28f8f8130a583e', + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-0' + }, + 'banner': [{ + 'w': 300, + 'h': 250 + }] + }, { + 'id': '3f97ca71b1e5c2', + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-123' + }, + 'banner': [{ + 'w': 300, + 'h': 251 + }] + }] + }, + VALID_PARAMS = { + bidder: 'medianet', + params: { + cid: '8CUV090' + } + }, + PARAMS_WITHOUT_CID = { + bidder: 'medianet', + params: {} + }, + PARAMS_WITH_INTEGER_CID = { + bidder: 'medianet', + params: { + cid: 8867587 + } + }, + PARAMS_WITH_EMPTY_CID = { + bidder: 'medianet', + params: { + cid: '' + } + }, + SYNC_OPTIONS_BOTH_ENABLED = { + iframeEnabled: true, + pixelEnabled: true, + }, + SYNC_OPTIONS_PIXEL_ENABLED = { + iframeEnabled: false, + pixelEnabled: true, + }, + SYNC_OPTIONS_IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }, + SERVER_CSYNC_RESPONSE = [{ + body: { + ext: { + csUrl: [{ + type: 'iframe', + url: 'iframe-url' + }, { + type: 'image', + url: 'pixel-url' + }] + } + } + }], + ENABLED_SYNC_IFRAME = [{ + type: 'iframe', + url: 'iframe-url' + }], + ENABLED_SYNC_PIXEL = [{ + type: 'image', + url: 'pixel-url' + }], + SERVER_RESPONSE_CPM_MISSING = { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': false, + 'requestId': '27210feac00e96', + 'ad': 'ad', + 'width': 300, + 'height': 250, + 'creativeId': '375068987', + 'netRevenue': true + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + }, + SERVER_RESPONSE_CPM_ZERO = { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': false, + 'requestId': '27210feac00e96', + 'ad': 'ad', + 'width': 300, + 'height': 250, + 'creativeId': '375068987', + 'netRevenue': true, + 'cpm': 0.0 + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + }, + SERVER_RESPONSE_NOBID = { + 'id': 'd90ca32f-3877-424a-b2f2-6a68988df57a', + 'bidList': [{ + 'no_bid': true, + 'requestId': '3a62cf7a853f84', + 'width': 0, + 'height': 0, + 'ttl': 0, + 'netRevenue': false + }], + 'ext': { + 'csUrl': [{ + 'type': 'image', + 'url': 'http://cs.media.net/cksync.php' + }, { + 'type': 'iframe', + 'url': 'http://contextual.media.net/checksync.php?&vsSync=1' + }] + } + }, + BID_REQUEST_SIZE_AS_1DARRAY = [{ + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + 'sizes': [300, 250], + 'bidId': '28f8f8130a583e', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }, { + 'bidder': 'medianet', + 'params': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest' + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-123', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + 'sizes': [300, 251], + 'bidId': '3f97ca71b1e5c2', + 'bidderRequestId': '1e9b1f07797c1c', + 'auctionId': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d' + }]; + +describe('Media.net bid adapter', () => { + describe('isBidRequestValid', () => { + it('should accept valid bid params', () => { + let isValid = spec.isBidRequestValid(VALID_PARAMS); + expect(isValid).to.equal(true); + }); + + it('should reject bid if cid is not present', () => { + let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID); + expect(isValid).to.equal(false); + }); + + it('should reject bid if cid is not a string', () => { + let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID); + expect(isValid).to.equal(false); + }); + + it('should reject bid if cid is a empty string', () => { + let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID); + expect(isValid).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build valid payload on bid', () => { + let requestObj = spec.buildRequests(VALID_BID_REQUEST); + expect(JSON.parse(requestObj.data)).to.deep.equal(VALID_PAYLOAD); + }); + + it('should accept size as a one dimensional array', () => { + let bidReq = spec.buildRequests(BID_REQUEST_SIZE_AS_1DARRAY); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD); + }); + + it('should ignore bidfloor if not a valid number', () => { + let bidReq = spec.buildRequests(VALID_BID_REQUEST_INVALID_BIDFLOOR); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD); + }); + }); + + describe('getUserSyncs', () => { + it('should exclude iframe syncs if iframe is disabled', () => { + let userSyncs = spec.getUserSyncs(SYNC_OPTIONS_PIXEL_ENABLED, SERVER_CSYNC_RESPONSE); + expect(userSyncs).to.deep.equal(ENABLED_SYNC_PIXEL); + }); + + it('should exclude pixel syncs if pixel is disabled', () => { + let userSyncs = spec.getUserSyncs(SYNC_OPTIONS_IFRAME_ENABLED, SERVER_CSYNC_RESPONSE); + expect(userSyncs).to.deep.equal(ENABLED_SYNC_IFRAME); + }); + + it('should choose iframe sync urls if both sync options are enabled', () => { + let userSyncs = spec.getUserSyncs(SYNC_OPTIONS_BOTH_ENABLED, SERVER_CSYNC_RESPONSE); + expect(userSyncs).to.deep.equal(ENABLED_SYNC_IFRAME); + }); + }); + + describe('interpretResponse', () => { + it('should not push bid response if cpm missing', () => { + let validBids = []; + let bids = spec.interpretResponse(SERVER_RESPONSE_CPM_MISSING, []); + expect(bids).to.deep.equal(validBids); + }); + + it('should not push bid response if cpm 0', () => { + let validBids = []; + let bids = spec.interpretResponse(SERVER_RESPONSE_CPM_ZERO, []); + expect(bids).to.deep.equal(validBids); + }); + + it('should not push response if no-bid', () => { + let validBids = []; + let bids = spec.interpretResponse(SERVER_RESPONSE_NOBID, []); + expect(bids).to.deep.equal(validBids) + }); + }); +});