diff --git a/modules/viqeoBidAdapter.js b/modules/viqeoBidAdapter.js new file mode 100644 index 000000000..5762a794c --- /dev/null +++ b/modules/viqeoBidAdapter.js @@ -0,0 +1,180 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logError, logInfo, _each, mergeDeep, isFn, isNumber, isPlainObject} from '../src/utils.js' +import {VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; + +const BIDDER_CODE = 'viqeo'; +const DEFAULT_MIMES = ['application/javascript']; +const VIQEO_ENDPOINT = 'https://ads.betweendigital.com/openrtb_bid'; +const RENDERER_URL = 'https://cdn.viqeo.tv/js/vq_starter.js'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_SSPID = 44697; + +function getBidFloor(bid) { + const {floor, currency} = bid.params; + const curr = currency || DEFAULT_CURRENCY; + if (!isFn(bid.getFloor)) { + return {floor: isNumber(floor) ? floor : 0, currency: curr}; + } + const floorInfo = bid.getFloor({currency: curr, mediaType: VIDEO, size: '*'}); + if (isPlainObject(floorInfo) && isNumber(floorInfo.floor) && floorInfo.currency === curr) { + return floorInfo; + } + return {floor: floor || 0, currency: currency || DEFAULT_CURRENCY}; +} + +function getVideoTargetingParams({mediaTypes: {video}}) { + const result = {}; + Object.keys(Object(video)) + .forEach(key => { + if (key === 'playerSize') { + result.w = video.playerSize[0][0]; + result.h = video.playerSize[0][1]; + } else if (key !== 'context') { + result[key] = video[key]; + } + }) + return result; +} + +/** + * @type {BidderSpec} + */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO], + /** + * @param {BidRequest} bidRequest The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: ({params}) => { + if (!params) { + logError('failed validation: params not declared'); + return false; + } + if (!params.user && !params.user?.buyeruid) { + logError('failed validation: user.buyeruid not declared'); + return false; + } + if (!params.playerOptions) { + logError('failed validation: playerOptions not declared'); + return false; + } + const {profileId, videoId, playerId} = params.playerOptions; + if (!profileId) { + logError('failed validation: profileId not declared'); + return false; + } + if (!videoId && !playerId) { + logError('failed validation: videoId or playerId not declared'); + return false; + } + return true; + }, + /** + * @param validBidRequests {BidRequest[]} + * @returns {ServerRequest[]} + */ + buildRequests: (validBidRequests) => { + logInfo('validBidRequests', validBidRequests); + const bidRequests = []; + _each(validBidRequests, (bid, i) => { + const { + params: {test, sspId, endpointUrl}, + mediaTypes: {video}, + } = bid; + const ortb2 = bid.ortb2 || {}; + const user = bid.params.user || {}; + const device = bid.params.device || {}; + const site = bid.params.site || {}; + const w = window; + const floorInfo = getBidFloor(bid); + const data = { + id: bid.bidId, + test, + imp: [{ + id: `${i}`, + tagid: bid.adUnitCode, + video: { + ...getVideoTargetingParams(bid), + mimes: video.mimes || DEFAULT_MIMES, + }, + bidfloor: floorInfo.floor, + bidfloorcur: floorInfo.currency, + secure: 1 + }], + site: test === 1 ? { + page: 'https://viqeo.tv', + domain: 'viqeo.tv' + } : mergeDeep({ + domain: w.location.hostname, + page: w.location.href + }, ortb2.site, site), + device: mergeDeep({ + w: w.screen.width, + h: w.screen.height, + ua: w.navigator.userAgent, + }, ortb2.device, device), + user: mergeDeep({...user}, ortb2.user), + app: bid.params.app, + }; + bidRequests.push({ + url: endpointUrl || `${VIQEO_ENDPOINT}/?sspId=${sspId || DEFAULT_SSPID}`, + method: 'POST', + data, + bids: validBidRequests, + }); + }); + return bidRequests; + }, + /** + * @param {ServerResponse} serverResponse + * @param {BidRequest} bidRequests + * @return {Bid[]} + */ + interpretResponse: (serverResponse, bidRequests) => { + logInfo('serverResponse', serverResponse); + const bidResponses = []; + if (!serverResponse || !serverResponse.body) { + logError('empty response'); + return []; + } + try { + const {id, seatbid, cur} = serverResponse.body; + _each(seatbid, (sb) => { + const {bid} = sb; + _each(bid, (b) => { + const bidRequest = bidRequests.bids.find(({bidId}) => bidId === id); + const renderer = Renderer.install({ + url: bidRequest?.params?.renderUrl || RENDERER_URL, + }); + renderer.setRender((bid) => { + if (window.VIQEO) { + window.VIQEO.renderPrebid(bid); + } else { + logError('failed get window.VIQEO'); + } + }); + bidResponses.push({ + requestId: id, + currency: cur, + cpm: b.price, + ttl: b.exp, + netRevenue: true, + creativeId: b.cid, + width: b.w || bidRequest?.mediaTypes[VIDEO].playerSize[0][0], + height: b.h || bidRequest?.mediaTypes[VIDEO].playerSize[0][1], + vastXml: b.adm, + vastUrl: b.nurl, + mediaType: VIDEO, + renderer, + }) + }) + }); + } catch (error) { + logError(error); + } + return bidResponses; + }, +} +registerBidder(spec); diff --git a/modules/viqeoBidAdapter.md b/modules/viqeoBidAdapter.md new file mode 100644 index 000000000..b4a020bf0 --- /dev/null +++ b/modules/viqeoBidAdapter.md @@ -0,0 +1,56 @@ +# Overview + +**Module Name**: Viqeo Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: muravjovv1@gmail.com + +# Description + +Viqeo Bidder Adapter for Prebid.js. About: https://viqeo.tv/ + +### Bid params + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-----------------------------|----------|----------------------------------------------------------------------------------------------------------------------------|--------------------------|-----------| +| `user` | required | The object containing user data (See OpenRTB spec) | `user: {}` | `object` | +| `user.buyeruid` | required | User id | `"12345"` | `string` | +| `playerOptions` | required | The object containing Viqeo player options | `playerOptions: {}` | `object` | +| `playerOptions.profileId` | required | Viqeo profile id | `1382` | `number` | +| `playerOptions.videId` | optional | Viqeo video id | `"ed584da454c7205ca7e4"` | `string` | +| `playerOptions.playerId` | optional | Viqeo player id | `1` | `number` | +| `device` | optional | The object containing device data (See OpenRTB spec) | `device: {}` | `object` | +| `site` | optional | The object containing site data (See OpenRTB spec) | `site: {}` | `object` | +| `app` | optional | The object containing app data (See OpenRTB spec) | `app: {}` | `object` | +| `floor` | optional | Bid floor price | `0.5` | `number` | +| `currency` | optional | 3-letter ISO 4217 code defining the currency of the bid. | `EUR` | `string` | +| `test` | optional | Flag which will induce a sample bid response when true; only set to true for testing purposes (1 = true, 0 = false) | `1` | `integer` | +| `sspId` | optional | For debug, request id | `1` | `number` | +| `renderUrl` | optional | For debug, script player url | `"https://viqeo.tv"` | `string` | +| `endpointUrl` | optional | For debug, api endpoint | `"https://viqeo.tv"` | `string` | + +# Test Parameters +``` + var adUnits = [{ + code: 'your-slot', // use exactly the same code as your slot div id. + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, + bids: [{ + bidder: 'viqeo', + params: { + user: { + buyeruid: '1', + }, + playerOptions: { + videoId: 'ed584da454c7205ca7e4', + profileId: 1382, + }, + test: 1, + } + }] + }]; +``` diff --git a/test/spec/modules/viqeoBidAdapter_spec.js b/test/spec/modules/viqeoBidAdapter_spec.js new file mode 100644 index 000000000..8f597318a --- /dev/null +++ b/test/spec/modules/viqeoBidAdapter_spec.js @@ -0,0 +1,124 @@ +import {expect} from 'chai'; +import {spec} from 'modules/viqeoBidAdapter'; + +describe('viqeoBidAdapter', function () { + it('minimal params', function () { + expect(spec.isBidRequestValid({ + bidder: 'viqeo', + params: { + user: { + buyeruid: '1', + }, + playerOptions: { + videoId: 'ed584da454c7205ca7e4', + profileId: 1382, + }, + }})).to.equal(true); + }); + it('minimal params no playerOptions', function () { + expect(spec.isBidRequestValid({ + bidder: 'viqeo', + params: { + currency: 'EUR', + }})).to.equal(false); + }); + it('build request check data', function () { + const bidRequestData = [{ + bidId: 'id1', + bidder: 'viqeo', + params: { + user: { + buyeruid: '1', + }, + currency: 'EUR', + floor: 0.5, + playerOptions: { + videoId: 'ed584da454c7205ca7e4', + profileId: 1382, + }, + }, + mediaTypes: { + video: { playerSize: [[240, 400]] } + }, + }]; + const request = spec.buildRequests(bidRequestData); + const requestData = request[0].data; + expect(requestData.id).to.equal('id1') + expect(requestData.imp[0].bidfloorcur).to.equal('EUR'); + expect(requestData.imp[0].bidfloor).to.equal(0.5); + expect(requestData.imp[0].video.w).to.equal(240); + expect(requestData.imp[0].video.h).to.equal(400); + expect(requestData.user.buyeruid).to.equal('1'); + }); + it('build request check url', function () { + const bidRequestData = [{ + bidder: 'viqeo', + params: { + playerOptions: { + videoId: 'ed584da454c7205ca7e4', + profileId: 1382, + }, + sspId: 42, + }, + mediaTypes: { + video: { playerSize: [[240, 400]] } + }, + }]; + const request = spec.buildRequests(bidRequestData); + expect(request[0].url).to.equal('https://ads.betweendigital.com/openrtb_bid/?sspId=42') + }); + it('response_params common case', function () { + const bidRequestData = { + bids: [{ + bidId: 'id1', + params: {}, + mediaTypes: { + video: { playerSize: [[240, 400]] } + }, + }], + }; + const serverResponse = { + body: { + id: 'id1', + cur: 'EUR', + seatbid: [{ + bid: [{ + cpm: 0.5, + ttl: 3600, + netRevenue: true, + creativeId: 'test1', + adm: '', + }], + }], + } + }; + const bids = spec.interpretResponse(serverResponse, bidRequestData); + expect(bids).to.have.lengthOf(1); + }); + it('should set flooPrice to getFloor.floor value if it is greater than params.floor', function() { + const bidRequestData = [{ + bidId: 'id1', + bidder: 'viqeo', + params: { + currency: 'EUR', + floor: 0.5, + playerOptions: { + videoId: 'ed584da454c7205ca7e4', + profileId: 1382, + }, + }, + mediaTypes: { + video: { playerSize: [[240, 400]] } + }, + getFloor: () => { + return { + currency: 'EUR', + floor: 3.32 + } + }, + }]; + const request = spec.buildRequests(bidRequestData); + const requestData = request[0].data; + expect(requestData.imp[0].bidfloor).to.equal(3.32) + }); +});