diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index f375e161f88..8fda9cba593 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,6 +1,8 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import { deepClone, logError, deepAccess } from '../src/utils.js'; +import { config } from '../src/config.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -9,7 +11,86 @@ import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; */ const BIDDER_CODE = 'vdoai'; -const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; +const ENDPOINT_URL = 'https://prebid-v2.vdo.ai/auction'; + +function getFrameNesting() { + let topmostFrame = window; + let parent = window.parent; + try { + while (topmostFrame !== topmostFrame.parent) { + parent = topmostFrame.parent; + // eslint-disable-next-line no-unused-expressions + parent.location.href; + topmostFrame = topmostFrame.parent; + } + } catch (e) { } + return topmostFrame; +} + +/** + * Returns information about the page needed by the server in an object to be converted in JSON + * + * @returns {{location: *, referrer: (*|string), stack: (*|Array.), numIframes: (*|Number), wWidth: (*|Number), wHeight: (*|Number), sWidth, sHeight, date: string, timeOffset: number}} + */ +function getPageInfo(bidderRequest) { + const topmostFrame = getFrameNesting(); + return { + referrer: deepAccess(bidderRequest, 'refererInfo.ref', null), + stack: deepAccess(bidderRequest, 'refererInfo.stack', []), + numIframes: deepAccess(bidderRequest, 'refererInfo.numIframes', 0), + wWidth: topmostFrame.innerWidth, + location: deepAccess(bidderRequest, 'refererInfo.page', null), + wHeight: topmostFrame.innerHeight, + aWidth: topmostFrame.screen.availWidth, + aHeight: topmostFrame.screen.availHeight, + oWidth: topmostFrame.outerWidth, + oHeight: topmostFrame.outerHeight, + sWidth: topmostFrame.screen.width, + sHeight: topmostFrame.screen.height, + sLeft: 'screenLeft' in topmostFrame ? topmostFrame.screenLeft : topmostFrame.screenX, + sTop: 'screenTop' in topmostFrame ? topmostFrame.screenTop : topmostFrame.screenY, + xOffset: topmostFrame.pageXOffset, + docHeight: topmostFrame.document.body ? topmostFrame.document.body.scrollHeight : null, + hLength: history.length, + yOffset: topmostFrame.pageYOffset, + version: { + prebid_version: '$prebid.version$', + adapter_version: '1.0.0', + vendor: '$$PREBID_GLOBAL$$', + } + }; +} + +export function isSchainValid(schain) { + let isValid = false; + const requiredFields = ['asi', 'sid', 'hp']; + if (!schain || !schain.nodes) return isValid; + isValid = schain.nodes.reduce((status, node) => { + if (!status) return status; + return requiredFields.every(field => node.hasOwnProperty(field)); + }, true); + if (!isValid) { + logError('VDO.AI: required schain params missing'); + } + return isValid; +} + +function parseVideoSize(bid) { + const playerSize = bid.mediaTypes.video.playerSize; + if (typeof playerSize !== 'undefined' && Array.isArray(playerSize) && playerSize.length > 0) { + return getSizes(playerSize) + } + return []; +} + +function getSizes(sizes) { + const ret = []; + for (let i = 0; i < sizes.length; i++) { + const size = sizes[i]; + ret.push({ width: size[0], height: size[1] }) + } + return ret; +} export const spec = { code: BIDDER_CODE, @@ -21,7 +102,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return !!(bid.params.placementId); + return !!(bid.params.placementId) && typeof bid.params.placementId === 'string'; }, /** @@ -31,23 +112,82 @@ export const spec = { * @param validBidRequests * @param bidderRequest */ + buildRequests: function (validBidRequests, bidderRequest) { if (validBidRequests.length === 0) { return []; } - return validBidRequests.map(bidRequest => { const sizes = getAdUnitSizes(bidRequest); - const payload = { + let payload = { placementId: bidRequest.params.placementId, sizes: sizes, bidId: bidRequest.bidId, - // TODO: is 'page' the right value here? - referer: bidderRequest.refererInfo.page, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: bidRequest.auctionId, - mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner' + mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner', + domain: bidderRequest.ortb2.site.domain, + publisherDomain: bidderRequest.ortb2.site.publisher.domain, + adUnitCode: bidRequest.adUnitCode, + bidder: bidRequest.bidder, + tmax: bidderRequest.timeout }; + + payload.bidderRequestId = bidRequest.bidderRequestId; + payload.auctionId = deepAccess(bidRequest, 'ortb2.source.tid'); + payload.transactionId = deepAccess(bidRequest, 'ortb2Imp.ext.tid'); + payload.gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + payload.ortb2Imp = deepAccess(bidRequest, 'ortb2Imp'); + + if (payload.mediaType === 'video') { + payload.context = bidRequest.mediaTypes.video.context; + payload.playerSize = parseVideoSize(bidRequest); + payload.mediaTypeInfo = deepClone(bidRequest.mediaTypes.video); + } + + if (typeof bidRequest.getFloor === 'function') { + let floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (floor && floor.floor && floor.currency === 'USD') { + payload.bidFloor = floor.floor; + } + } else if (bidRequest.params.bidFloor) { + payload.bidFloor = bidRequest.params.bidFloor; + } + + payload.pageInfo = getPageInfo(bidderRequest); + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdprConsent = { + consentRequired: bidderRequest.gdprConsent.gdprApplies, + consentString: bidderRequest.gdprConsent.consentString, + addtlConsent: bidderRequest.gdprConsent.addtlConsent + }; + } + if (bidderRequest && bidderRequest.gppConsent) { + payload.gppConsent = { + applicableSections: bidderRequest.gppConsent.applicableSections, + consentString: bidderRequest.gppConsent.gppString, + } + } + if (bidderRequest && bidderRequest.ortb2) { + payload.ortb2 = bidderRequest.ortb2; + } + if (bidderRequest && bidderRequest.uspConsent) { + payload.usPrivacy = bidderRequest.uspConsent; + } + if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].schain && isSchainValid(validBidRequests[0].schain)) { + payload.schain = validBidRequests[0].schain; + } + if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userIdAsEids) { + payload.userId = validBidRequests[0].userIdAsEids; + } + let coppaOrtb2 = !!deepAccess(bidderRequest, 'ortb2.regs.coppa'); + let coppaConfig = config.getConfig('coppa'); + if (coppaOrtb2 === true || coppaConfig === true) { + payload.coppa = true; + } return { method: 'POST', url: ENDPOINT_URL, @@ -67,35 +207,24 @@ export const spec = { const bidResponses = []; const response = serverResponse.body; const creativeId = response.adid || 0; - // const width = response.w || 0; - const width = response.width; - // const height = response.h || 0; - const height = response.height; + const width = response.w; + const height = response.h; const cpm = response.price || 0; - response.rWidth = width; - response.rHeight = height; - const adCreative = response.vdoCreative; if (width !== 0 && height !== 0 && cpm !== 0 && creativeId !== 0) { - // const dealId = response.dealid || ''; const currency = response.cur || 'USD'; const netRevenue = true; - // const referrer = bidRequest.data.referer; const bidResponse = { requestId: response.bidId, cpm: cpm, width: width, height: height, creativeId: creativeId, - // dealId: dealId, currency: currency, netRevenue: netRevenue, ttl: 60, - // referrer: referrer, - // ad: response.adm - // ad: adCreative, mediaType: response.mediaType }; @@ -104,9 +233,9 @@ export const spec = { } else { bidResponse.ad = adCreative; } - if (response.adDomain) { + if (response.adomain) { bidResponse.meta = { - advertiserDomains: response.adDomain + advertiserDomains: response.adomain }; } bidResponses.push(bidResponse); @@ -130,7 +259,7 @@ export const spec = { return []; }, - onTImeout: function(data) {}, + onTimeout: function(data) {}, onBidWon: function(bid) {}, onSetTargeting: function(bid) {} }; diff --git a/test/spec/modules/vdoaiBidAdapter_spec.js b/test/spec/modules/vdoaiBidAdapter_spec.js index b1cfa606d84..e7f153fa237 100644 --- a/test/spec/modules/vdoaiBidAdapter_spec.js +++ b/test/spec/modules/vdoaiBidAdapter_spec.js @@ -1,11 +1,19 @@ import {assert, expect} from 'chai'; import {spec} from 'modules/vdoaiBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; -const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; +const ENDPOINT_URL = 'https://prebid-v2.vdo.ai/auction'; describe('vdoaiBidAdapter', function () { const adapter = newBidder(spec); + const sandbox = sinon.sandbox.create(); + beforeEach(function() { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + }); + afterEach(function() { + sandbox.restore(); + }); describe('isBidRequestValid', function () { let bid = { 'bidder': 'vdoai', @@ -20,16 +28,33 @@ describe('vdoaiBidAdapter', function () { 'bidderRequestId': '1234asdf1234asdf', 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf120' }; + let invalidParams = { + 'bidder': 'vdoai', + 'params': { + placementId: false + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '1234asdf1234', + 'bidderRequestId': '1234asdf1234asdf', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf120' + }; it('should return true where required params found', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should return false where required params not found', function () { + expect(spec.isBidRequestValid(invalidParams)).to.equal(false); + }); }); describe('buildRequests', function () { let bidRequests = [ { 'bidder': 'vdoai', 'params': { - placementId: 'testPlacementId' + placementId: 'testPlacementId', + bidFloor: 0.1 }, 'sizes': [ [300, 250] @@ -37,16 +62,84 @@ describe('vdoaiBidAdapter', function () { 'bidId': '23beaa6af6cdde', 'bidderRequestId': '19c0c1efdf37e7', 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', - 'mediaTypes': 'banner' + 'mediaType': 'banner', + 'adUnitCode': '1234', + 'mediaTypes': { + banner: { + sizes: [300, 250] + } + } + }, + { + 'bidder': 'vdoai', + 'params': { + placementId: 'testPlacementId', + bidFloor: 0.1 + }, + 'width': '300', + 'height': '200', + 'bidId': 'bidId123', + 'referer': 'www.example.com', + 'mediaType': 'video', + 'mediaTypes': { + video: { + context: 'instream', + playerSize: [[640, 360]] + } + } } ]; let bidderRequests = { + timeout: 3000, 'refererInfo': { 'numIframes': 0, 'reachedTop': true, 'referer': 'https://example.com', - 'stack': ['https://example.com'] + 'stack': ['https://example.com'], + 'page': 'example.com', + 'ref': 'example2.com' + }, + 'ortb2': { + source: { + tid: 123456789 + }, + 'site': { + 'domain': 'abc.com', + 'publisher': { + 'domain': 'abc.com' + } + } + }, + 'ortb2Imp': { + ext: { + tid: '12345', + gpid: '1234' + } + }, + gdprConsent: { + consentString: 'abc', + gdprApplies: true, + addtlConsent: 'xyz' + }, + gppConsent: { + gppString: 'abcd', + applicableSections: '' + }, + uspConsent: { + uspConsent: '12345' + }, + userIdAsEids: {}, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'vdo.ai', + 'sid': '4359', + 'hp': 1 + } + ] } }; @@ -57,6 +150,44 @@ describe('vdoaiBidAdapter', function () { it('attaches source and version to endpoint URL as query params', function () { expect(request[0].url).to.equal(ENDPOINT_URL); }); + it('should contain all keys', function() { + expect(request[0].data.pageInfo).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'xOffset', 'yOffset', 'version'); + }) + it('should return empty array if no valid bid was passed', function () { + expect(spec.buildRequests([], bidderRequests)).to.be.empty; + }); + it('should not send invalid schain', function () { + delete bidderRequests.schain.nodes[0].asi; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[0].data.schain).to.be.undefined; + }); + it('should not send invalid schain', function () { + delete bidderRequests.schain; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[0].data.schain).to.be.undefined; + }); + it('should check for correct sizes', function () { + delete bidRequests[1].mediaTypes.video.playerSize; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[1].data.playerSize).to.be.empty; + }); + it('should not pass undefined in GDPR string', function () { + delete bidderRequests.gdprConsent; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[0].data.gdprConsent).to.be.undefined; + }); + + it('should not pass undefined in gppConsent', function () { + delete bidderRequests.gppConsent; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[0].data.gppConsent).to.be.undefined; + }); + + it('should not pass undefined in uspConsent', function () { + delete bidderRequests.uspConsent; + let result = spec.buildRequests(bidRequests, bidderRequests); + expect(result[0].data.uspConsent).to.be.undefined; + }); }); describe('interpretResponse', function () { @@ -75,29 +206,31 @@ describe('vdoaiBidAdapter', function () { } ]; let serverResponse = { - body: { - 'vdoCreative': '

I am an ad

', - 'price': 4.2, - 'adid': '12345asdfg', - 'currency': 'EUR', - 'statusMessage': 'Bid available', - 'requestId': 'bidId123', - 'width': 300, - 'height': 250, - 'netRevenue': true, - 'adDomain': ['text.abc'] + body: + { + 'price': 2, + 'adid': 'test-ad', + 'adomain': [ + 'text.abc' + ], + 'w': 300, + 'h': 250, + 'vdoCreative': '

I am an ad

', + 'bidId': '31d1375caab87a', + 'mediaType': 'banner' } }; + it('should get the correct bid response', function () { let expectedResponse = [{ - 'requestId': 'bidId123', - 'cpm': 4.2, + 'requestId': '31d1375caab87a', + 'cpm': 2, 'width': 300, 'height': 250, - 'creativeId': '12345asdfg', - 'currency': 'EUR', + 'creativeId': 'test-ad', + 'currency': 'USD', 'netRevenue': true, - 'ttl': 3000, + 'ttl': 60, 'ad': '

I am an ad

', 'meta': { 'advertiserDomains': ['text.abc'] @@ -112,9 +245,9 @@ describe('vdoaiBidAdapter', function () { let serverResponse = { body: { 'vdoCreative': '', - 'price': 4.2, + 'price': 2, 'adid': '12345asdfg', - 'currency': 'EUR', + 'currency': 'USD', 'statusMessage': 'Bid available', 'requestId': 'bidId123', 'width': 300, @@ -133,7 +266,13 @@ describe('vdoaiBidAdapter', function () { 'height': '200', 'bidId': 'bidId123', 'referer': 'www.example.com', - 'mediaType': 'video' + 'mediaType': 'video', + 'mediaTypes': { + video: { + context: 'instream', + playerSize: [[640, 360]] + } + } } } ]; @@ -142,5 +281,131 @@ describe('vdoaiBidAdapter', function () { expect(result[0]).to.have.property('vastXml'); expect(result[0]).to.have.property('mediaType', 'video'); }); + it('should not return invalid responses', function() { + serverResponse.body.w = 0; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result).to.be.empty; + }); + it('should not return invalid responses with invalid height', function() { + serverResponse.body.w = 300; + serverResponse.body.h = 0; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result).to.be.empty; + }); + it('should not return invalid responses with invalid cpm', function() { + serverResponse.body.h = 250; + serverResponse.body.price = 0; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result).to.be.empty; + }); + it('should not return invalid responses with invalid creative ID', function() { + serverResponse.body.price = 2; + serverResponse.body.adid = undefined; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result).to.be.empty; + }); + }); + describe('getUserSyncs', function() { + it('should return correct sync urls', function() { + let serverResponse = [{ + body: { + 'vdoCreative': '', + 'price': 2, + 'adid': '12345asdfg', + 'currency': 'USD', + 'statusMessage': 'Bid available', + 'requestId': 'bidId123', + 'width': 300, + 'height': 250, + 'netRevenue': true, + 'mediaType': 'video', + 'cookiesync': { + 'status': 'no_cookie', + 'bidder_status': [ + { + 'bidder': 'vdoai', + 'no_cookie': true, + 'usersync': { + 'url': 'https://rtb.vdo.ai/setuid/', + 'type': 'iframe' + } + } + ] + } + } + }]; + + let syncUrls = spec.getUserSyncs({ + iframeEnabled: true + }, serverResponse); + expect(syncUrls[0].url).to.be.equal(serverResponse[0].body.cookiesync.bidder_status[0].usersync.url); + + syncUrls = spec.getUserSyncs({ + iframeEnabled: false + }, serverResponse); + expect(syncUrls[0]).to.be.undefined; + }); + it('should not return invalid sync urls', function() { + let serverResponse = [{ + body: { + 'vdoCreative': '', + 'price': 2, + 'adid': '12345asdfg', + 'currency': 'USD', + 'statusMessage': 'Bid available', + 'requestId': 'bidId123', + 'width': 300, + 'height': 250, + 'netRevenue': true, + 'mediaType': 'video', + 'cookiesync': { + 'status': 'no_cookie', + 'bidder_status': [ + ] + } + } + }]; + + var syncUrls = spec.getUserSyncs({ + iframeEnabled: true + }, serverResponse); + expect(syncUrls[0]).to.be.undefined; + + delete serverResponse[0].body.cookiesync.bidder_status; + syncUrls = spec.getUserSyncs({ + iframeEnabled: true + }, serverResponse); + expect(syncUrls[0]).to.be.undefined; + + delete serverResponse[0].body.cookiesync; + syncUrls = spec.getUserSyncs({ + iframeEnabled: true + }, serverResponse); + expect(syncUrls[0]).to.be.undefined; + + delete serverResponse[0].body; + syncUrls = spec.getUserSyncs({ + iframeEnabled: true + }, serverResponse); + expect(syncUrls[0]).to.be.undefined; + }); + }); + + describe('onTimeout', function() { + it('should run without errors', function() { + spec.onTimeout(); + }); + }); + + describe('onBidWon', function() { + it('should run without errors', function() { + spec.onBidWon(); + }); + }); + + describe('onSetTargeting', function() { + it('should run without errors', function() { + spec.onSetTargeting(); + }); }); });