diff --git a/modules/jwplayerBidAdapter.js b/modules/jwplayerBidAdapter.js index 37fa480b560..21c68b1f712 100644 --- a/modules/jwplayerBidAdapter.js +++ b/modules/jwplayerBidAdapter.js @@ -1,12 +1,36 @@ -// import * as utils from 'src/utils'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -// import { config } from 'src/config'; import { VIDEO } from '../src/mediaTypes.js'; +import { + isFn, + deepAccess, + deepSetValue } from '../src/utils.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'jwplayer'; +const URL = 'https://ib.adnxs.com/openrtb2/prebid'; + const GVLID = 1046; const SUPPORTED_AD_TYPES = [VIDEO]; +// Video Parameters +// https://docs.prebid.org/dev-docs/bidder-adaptor.html#step-2-accept-video-parameters-and-pass-them-to-your-server +const VIDEO_ORTB_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'protocols', + 'startdelay', + 'placement', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -29,10 +53,25 @@ export const spec = { /** * 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. + * @param {BidRequest[]} bidRequests A non-empty list of bid requests, or ad units, which should be sent to the server. + * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ - buildRequests: function(validBidRequests, bidderRequest) {}, + buildRequests: function(bidRequests, bidderRequest) { + if (!bidRequests) { + return; + } + + return bidRequests.map(bidRequest => { + const payload = buildRequest(bidRequest, bidderRequest); + + return { + method: 'POST', + url: URL, + data: payload + } + }); + }, /** * Unpack the response from the server into a list of bids. @@ -50,4 +89,105 @@ export const spec = { // onBidderError: function({ error, bidderRequest }) {} }; +function buildRequest(bidRequest, bidderRequest) { + const openrtbRequest = { + id: bidRequest.bidId, + imp: buildRequestImpression(bidRequest, bidderRequest), + site: buildRequestSite(bidderRequest), + device: buildRequestDevice() + }; + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return JSON.stringify(openrtbRequest); +} + +function buildRequestImpression(bidRequest) { + const impressionObject = { + id: bidRequest.adUnitCode, + }; + + impressionObject.video = buildImpressionVideo(bidRequest); + + const bidFloorData = buildBidFloorData(bidRequest); + if (bidFloorData) { + impressionObject.bidfloor = bidFloorData.floor; + impressionObject.bidfloorcur = bidFloorData.currency; + } + + impressionObject.ext = buildImpressionExtension(bidRequest); + + return [impressionObject]; +} + +function buildImpressionVideo(bidRequest) { + const videoParams = deepAccess(bidRequest, 'mediaTypes.video', {}); + + const video = {}; + + VIDEO_ORTB_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + return video; +} + +function buildImpressionExtension(bidRequest) { + return { + appnexus: { + placement_id: bidRequest.params.placementId + } + }; +} + +function buildBidFloorData(bidRequest) { + const {params} = bidRequest; + const currency = params.currency || 'USD'; + + let floorData; + if (isFn(bidRequest.getFloor)) { + const bidFloorRequest = { + currency: currency, + mediaType: VIDEO, + size: '*' + }; + floorData = bidRequest.getFloor(bidFloorRequest); + } else if (params.bidfloor) { + floorData = {floor: params.bidfloor, currency: currency}; + } + + return floorData; +} + +function buildRequestSite(bidderRequest) { + const site = config.getConfig('ortb2.site') || {}; + + site.domain = site.domain || config.publisherDomain || window.location.hostname; + site.page = site.page || config.pageUrl || window.location.href; + + const referer = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + if (!site.ref && referer) { + site.ref = referer; + } + + return site; +} + +function buildRequestDevice() { + return { + ua: navigator.userAgent + }; +} + registerBidder(spec); diff --git a/test/spec/modules/jwplayerBidAdapter_spec.js b/test/spec/modules/jwplayerBidAdapter_spec.js index 5abf6b7e8c7..9f443e62654 100644 --- a/test/spec/modules/jwplayerBidAdapter_spec.js +++ b/test/spec/modules/jwplayerBidAdapter_spec.js @@ -3,16 +3,17 @@ import { spec } from 'modules/jwplayerBidAdapter.js'; import { config } from 'src/config.js'; describe('jwplayer adapter tests', function() { - var sandbox, clock, frozenNow = new Date(); - beforeEach(function() { - sandbox = sinon.sandbox.create(); - clock = sinon.useFakeTimers(frozenNow.getTime()); - }); - - afterEach(function() { - sandbox.restore(); - clock.restore(); + this.defaultBidderRequest = { + 'gdprConsent': { + 'consentString': '', + 'gdprApplies': true + }, + 'uspConsent': true, + 'refererInfo': { + 'referer': 'https://example.com' + } + } }); describe('isBidRequestValid', function() { @@ -37,7 +38,80 @@ describe('jwplayer adapter tests', function() { }); }); - describe('buildRequests for video', function() {}); + describe('buildRequests for video', function() { + it('buildRequests works', function() { + const bidRequests = [ + { + 'bidder': 'jwplayer', + 'params': { + 'placementId': 12345 + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'content': {} + } + }, + 'bidRequestsCount': 1, + 'adUnitCode': 'testAdUnitCode', + 'bidId': 'testBidId' + } + ] + + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + const config = { + 'ortb2.site': { + domain: 'page.example.com', + page: 'https://examplepage.com' + } + }; + return config[key]; + }); + + const serverRequests = spec.buildRequests(bidRequests, this.defaultBidderRequest); + + serverRequests.forEach(serverRequest => { + expect(serverRequest.url).to.have.string('https://ib.adnxs.com/openrtb2/prebid'); + expect(serverRequest.method).to.equal('POST'); + + const openrtbRequest = JSON.parse(serverRequest.data); + + expect(openrtbRequest.id).to.not.equal(null); + expect(openrtbRequest.id).to.have.string('testBidId'); + + expect(openrtbRequest.site).to.not.equal(null); + expect(openrtbRequest.site).to.be.an('object'); + expect(openrtbRequest.site.domain).to.be.a('string'); + expect(openrtbRequest.site.domain).to.have.string('page.example.com'); + expect(openrtbRequest.site.page).to.be.a('string'); + expect(openrtbRequest.site.page).to.have.string('https://examplepage.com'); + expect(openrtbRequest.site.ref).to.be.a('string'); + expect(openrtbRequest.site.ref).to.have.string('https://example.com'); + + expect(openrtbRequest.device).to.not.equal(null); + expect(openrtbRequest.device.ua).to.equal(navigator.userAgent); + + expect(openrtbRequest.imp).to.not.equal(null); + expect(openrtbRequest.imp[0]).to.not.equal(null); + expect(openrtbRequest.imp[0].video).to.not.equal(null); + expect(openrtbRequest.imp[0].ext).to.not.equal(null); + expect(openrtbRequest.imp[0].ext.appnexus).to.not.equal(null); + expect(openrtbRequest.imp[0].ext.appnexus.placement_id).to.not.equal(null); + expect(openrtbRequest.imp[0].ext.appnexus.placement_id).to.equal(12345); + + expect(openrtbRequest.user).to.not.equal(null); + expect(openrtbRequest.user.ext).to.not.equal(null); + + expect(openrtbRequest.regs).to.not.equal(null); + expect(openrtbRequest.regs.ext).to.not.equal(null); + expect(openrtbRequest.regs.ext.gdpr).to.equal(1); + expect(openrtbRequest.regs.ext.us_privacy).to.equal(true); + }); + + sandbox.restore(); + }); + }); describe('interpretResponse for video', function() {});