diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 4a00b97614b2..88d8f95b8596 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -1,7 +1,7 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; -import {ortbConverter} from '../libraries/ortbConverter/converter.js' const BIDDER_CODE = 'eskimi'; // const ENDPOINT = 'https://hb.eskimi.com/bids' @@ -12,43 +12,35 @@ const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; const GVLID = 814; +const VIDEO_ORTB_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity', + 'battr' +]; + +const BANNER_ORTB_PARAMS = [ + 'battr' +] + export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [BANNER], - - isBidRequestValid: function (bid) { - return !!bid.params.placementId; - }, - - buildRequests(bidRequests, bidderRequest) { - const data = converter.toORTB({bidRequests, bidderRequest}) - - let bid = bidRequests.find((b) => b.params.placementId) - if (!data.site) data.site = {} - data.site.ext = {placementId: bid.params.placementId} - - if (bidderRequest.gdprConsent) { - if (!data.user) data.user = {}; - if (!data.user.ext) data.user.ext = {}; - if (!data.regs) data.regs = {}; - if (!data.regs.ext) data.regs.ext = {}; - data.user.ext.consent = bidderRequest.gdprConsent.consentString; - data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - - return [{ - method: 'POST', - url: ENDPOINT, - data, - options: {contentType: 'application/json;charset=UTF-8', withCredentials: false} - }] - }, - - interpretResponse(response, request) { - return converter.fromORTB({response: response.body, request: request.data}).bids; - }, - + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, /** * Register bidder specific code, which will execute if a bid from this bidder won the auction * @param {Bid} bid The bid that won the auction @@ -60,13 +52,151 @@ export const spec = { } } -const converter = ortbConverter({ +registerBidder(spec); + +const CONVERTER = ortbConverter({ context: { netRevenue: DEFAULT_NET_REVENUE, ttl: DEFAULT_BID_TTL, - currency: DEFAULT_CURRENCY, - mediaType: BANNER // TODO: support more types, we should set mtype on the winning bid + currency: DEFAULT_CURRENCY + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + imp.secure = Number(window.location.protocol === 'https:'); + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; + imp.bidfloorcur = utils.getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + } + + if (bidRequest.mediaTypes[VIDEO]) { + imp = buildVideoImp(bidRequest, imp); + } else if (bidRequest.mediaTypes[BANNER]) { + imp = buildBannerImp(bidRequest, imp); + } + + return imp; } }); -registerBidder(spec); +function isBidRequestValid(bidRequest) { + return (isPlacementIdValid(bidRequest) && (isValidBannerRequest(bidRequest) || isValidVideoRequest(bidRequest))); +} + +function isPlacementIdValid(bidRequest) { + return utils.isNumber(bidRequest.params.placementId); +} + +function isValidBannerRequest(bidRequest) { + const bannerSizes = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}.sizes`); + return utils.isArray(bannerSizes) && bannerSizes.length > 0 && bannerSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function isValidVideoRequest(bidRequest) { + const videoSizes = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}.playerSize`); + + return utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function buildRequests(validBids, bidderRequest) { + let videoBids = validBids.filter(bid => isVideoBid(bid)); + let bannerBids = validBids.filter(bid => isBannerBid(bid)); + let requests = []; + + bannerBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, BANNER)); + }); + + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + + return requests; +} + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; +} + +function buildVideoImp(bidRequest, imp) { + const videoAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${VIDEO}`, {}); + const videoBidderParams = utils.deepAccess(bidRequest, `params.${VIDEO}`, {}); + + const videoParams = { ...videoAdUnitParams, ...videoBidderParams }; + + const videoSizes = (videoAdUnitParams && videoAdUnitParams.playerSize) || []; + + if (videoSizes && videoSizes.length > 0) { + utils.deepSetValue(imp, 'video.w', videoSizes[0][0]); + utils.deepSetValue(imp, 'video.h', videoSizes[0][1]); + } + + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + utils.deepSetValue(imp, `video.${param}`, videoParams[param]); + } + }); + + if (imp.video && videoParams?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; + } + + return { ...imp }; +} + +function buildBannerImp(bidRequest, imp) { + const bannerAdUnitParams = utils.deepAccess(bidRequest, `mediaTypes.${BANNER}`, {}); + const bannerBidderParams = utils.deepAccess(bidRequest, `params.${BANNER}`, {}); + + const bannerParams = { ...bannerAdUnitParams, ...bannerBidderParams }; + + let sizes = bidRequest.mediaTypes.banner.sizes; + + if (sizes) { + utils.deepSetValue(imp, 'banner.w', sizes[0][0]); + utils.deepSetValue(imp, 'banner.h', sizes[0][1]); + } + + BANNER_ORTB_PARAMS.forEach((param) => { + if (bannerParams.hasOwnProperty(param)) { + utils.deepSetValue(imp, `banner.${param}`, bannerParams[param]); + } + }); + + return { ...imp }; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + + const bid = bidRequests.find((b) => b.params.placementId) + if (!data.site) data.site = {} + data.site.ext = { placementId: bid.params.placementId } + + if (bidderRequest.gdprConsent) { + if (!data.user) data.user = {}; + if (!data.user.ext) data.user.ext = {}; + if (!data.regs) data.regs = {}; + if (!data.regs.ext) data.regs.ext = {}; + data.user.ext.consent = bidderRequest.gdprConsent.consentString; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + + if (bid.params.bcat) data.bcat = bid.params.bcat; + if (bid.params.badv) data.badv = bid.params.badv; + if (bid.params.bapp) data.bapp = bid.params.bapp; + + return { + method: 'POST', + url: ENDPOINT, + data: data, + options: { contentType: 'application/json;charset=UTF-8', withCredentials: false } + } +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} diff --git a/modules/eskimiBidAdapter.md b/modules/eskimiBidAdapter.md index 83ae87fd01b6..b3494a217eb7 100644 --- a/modules/eskimiBidAdapter.md +++ b/modules/eskimiBidAdapter.md @@ -6,29 +6,48 @@ Maintainer: tech@eskimi.com # Description -An adapter to get a bid from Eskimi DSP. +Module that connects to Eskimi demand sources to fetch bids using OpenRTB standard. +Banner and video formats are supported. # Test Parameters ```javascript var adUnits = [{ - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - - bids: [{ - bidder: 'eskimi', - params: { - placementId: 612 - } - }] - - }]; + code: '/19968336/prebid_banner_example_1', + mediaTypes: { + banner: { + sizes: [[ 300, 250 ]], + ... // battr + } + }, + bids: [{ + bidder: 'eskimi', + params: { + placementId: 612, + ... // bcat, badv, bapp + } + }] + }, { + code: '/19968336/prebid_video_example_1', + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'], + api: [1, 2, 4, 6], + ... // Aditional ORTB video params (including battr) + } + }, + bids: [{ + bidder: 'eskimi', + params: { + placementId: 612, + ... // bcat, badv, bapp + } + }] + }]; ``` Where: * placementId - Placement ID of the ad unit (required) +* bcat, badv, bapp, battr - ORTB blocking parameters as specified by OpenRTB 2.5 diff --git a/test/spec/modules/eskimiBidAdapter_spec.js b/test/spec/modules/eskimiBidAdapter_spec.js index 4622b374de57..d01240c86abd 100644 --- a/test/spec/modules/eskimiBidAdapter_spec.js +++ b/test/spec/modules/eskimiBidAdapter_spec.js @@ -1,155 +1,307 @@ -import {expect} from 'chai'; -import {spec} from 'modules/eskimiBidAdapter.js'; - -const REQUEST = { - 'bidderCode': 'eskimi', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', - 'bidderRequestId': 'requestId', - 'bidRequest': [{ - 'bidder': 'eskimi', - 'params': { - 'placementId': 1003000, - 'accId': 123 +import { expect } from 'chai'; +import { spec } from 'modules/eskimiBidAdapter.js'; +import * as utils from 'src/utils'; + +const BANNER_BID = { + bidder: 'eskimi', + params: { + placementId: 1003000 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], }, - 'sizes': [ - [300, 250] - ], - 'bidId': 'bidId1', - 'adUnitCode': 'adUnitCode1', - 'bidderRequestId': 'bidderRequestId', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' }, - { - 'bidder': 'eskimi', - 'params': { - 'placementId': 1003001, - 'accId': 123 + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const VIDEO_BID = { + bidder: 'eskimi', + params: { + placementId: 1003000 + }, + mediaTypes: { + video: { + context: 'outstream', + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 }, - 'sizes': [ - [300, 250] - ], - 'bidId': 'bidId2', - 'adUnitCode': 'adUnitCode2', - 'bidderRequestId': 'bidderRequestId', - 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' - }], - 'start': 1487883186070, - 'auctionStart': 1487883186069, - 'timeout': 3000 + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', }; -const RESPONSE = { - 'headers': null, - 'body': { - 'id': 'requestId', - 'seatbid': [ - { - 'bid': [ - { - 'id': 'bidId1', - 'impid': 'bidId1', - 'price': 0.18, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'eskimi': { - 'brand_id': 334553, - 'auction_id': 514667951122925701, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'seat' - } - ] +const BIDDER_REQUEST = { + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + bidderRequestId: 'bidderRequestId', + timeout: 3000, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', } }; +const BANNER_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidId': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'w': 300, + 'h': 250 + } + ] + } + ], + 'cur': 'USD' +}; + +const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 1.09, + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'cur': 'USD' +}; + describe('Eskimi bid adapter', function () { - describe('isBidRequestValid', function () { + describe('isBidRequestValid()', function () { it('should accept request if placementId is passed', function () { let bid = { bidder: 'eskimi', params: { placementId: 123 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } } }; expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('reject requests without params', function () { + + it('should reject requests without params', function () { let bid = { bidder: 'eskimi', params: {} }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); - }); - describe('buildRequests', function () { - it('creates request data', function () { - let request = spec.buildRequests(REQUEST.bidRequest, REQUEST)[0]; - expect(request).to.exist.and.to.be.a('object'); - const payload = request.data; - expect(payload.imp[0]).to.have.property('id', REQUEST.bidRequest[0].bidId); - expect(payload.imp[1]).to.have.property('id', REQUEST.bidRequest[1].bidId); + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(BANNER_BID)).to.equal(true); + expect(spec.isBidRequestValid(VIDEO_BID)).to.equal(true); }); + }); - it('has gdpr data if applicable', function () { - const req = Object.assign({}, REQUEST, { + describe('buildRequests()', function () { + it('should have gdpr data if applicable', function () { + const bid = utils.deepClone(BANNER_BID); + + const req = Object.assign({}, BIDDER_REQUEST, { gdprConsent: { consentString: 'consentString', gdprApplies: true, } }); - let request = spec.buildRequests(REQUEST.bidRequest, req)[0]; + let request = spec.buildRequests([bid], req)[0]; const payload = request.data; expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); expect(payload.regs.ext).to.have.property('gdpr', 1); }); - }); - describe('interpretResponse', function () { - it('has bids', function () { - let request = spec.buildRequests(REQUEST.bidRequest, REQUEST)[0]; - let bids = spec.interpretResponse(RESPONSE, request); - expect(bids).to.be.an('array').that.is.not.empty; - validateBidOnIndex(0); - - function validateBidOnIndex(index) { - expect(bids[index]).to.have.property('currency', 'USD'); - expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id); - expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); - expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); - expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); - expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); - expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); - expect(bids[index]).to.have.property('ttl', 30); - expect(bids[index]).to.have.property('netRevenue', true); - } + it('should properly forward ORTB blocking params', function () { + let bid = utils.deepClone(BANNER_BID); + bid = utils.mergeDeep(bid, { + params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'] }, + mediaTypes: { banner: { battr: [1] } } + }); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + + expect(request).to.exist.and.to.be.an('object'); + const payload = request.data; + expect(payload).to.have.deep.property('bcat', ['IAB1-1']); + expect(payload).to.have.deep.property('badv', ['example.com']); + expect(payload).to.have.deep.property('bapp', ['com.example']); + expect(payload.imp[0].banner).to.have.deep.property('battr', [1]); + }); + + context('when mediaType is banner', function () { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(BANNER_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const requestData = request.data; + + expect(requestData.imp[0].banner.w).to.equal(300); + expect(requestData.imp[0].banner.h).to.equal(250); + }); + + it('should create request data', function () { + const bid = utils.deepClone(BANNER_BID); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bid.bidId); + }); }); - it('handles empty response', function () { - let request = spec.buildRequests(REQUEST.bidRequest, REQUEST)[0]; - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, request); - expect(bids).to.be.empty; + context('when mediaType is video', function () { + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should use bidder video params if they are set', () => { + const videoBidWithParams = utils.deepClone(VIDEO_BID); + const bidderVideoParams = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + minduration: 0, + maxduration: 60, + w: 1024, + h: 768, + startdelay: 0 + }; + + videoBidWithParams.params.video = bidderVideoParams; + + const requests = spec.buildRequests([videoBidWithParams], BIDDER_REQUEST); + const request = requests[0].data; + + expect(request.imp[0]).to.deep.include({ + video: { + ...bidderVideoParams, + w: videoBidWithParams.mediaTypes.video.playerSize[0][0], + h: videoBidWithParams.mediaTypes.video.playerSize[0][1], + }, + }); + }); }); }); + + describe('interpretResponse()', function () { + context('when mediaType is banner', function () { + it('should correctly interpret valid banner response', function () { + const bid = utils.deepClone(BANNER_BID); + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const response = utils.deepClone(BANNER_BID_RESPONSE); + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].burl).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].ad).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(30); + expect(bids[0].netRevenue).to.equal(true); + }); + + it('should handle empty bid response', function () { + const bid = utils.deepClone(BANNER_BID); + + let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); + }); + if (FEATURES.VIDEO) { + context('when mediaType is video', function () { + it('should correctly interpret valid instream video response', () => { + const bid = utils.deepClone(VIDEO_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].burl).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].vastXml).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(30); + expect(bids[0].netRevenue).to.equal(true); + }); + }); + } + }); });