From 8010ba5ebb48eb484faf5774ab2d402c619c3181 Mon Sep 17 00:00:00 2001 From: korys Date: Thu, 24 May 2018 18:58:23 +0200 Subject: [PATCH] added ccxAdapter (#2575) * added ccxAdapter * add ccxAdapter - lint formatting fixes --- modules/ccxBidAdapter.js | 192 ++++++++++++++ modules/ccxBidAdapter.md | 57 +++++ test/spec/modules/ccxBidAdapter_spec.js | 318 ++++++++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 modules/ccxBidAdapter.js create mode 100644 modules/ccxBidAdapter.md create mode 100644 test/spec/modules/ccxBidAdapter_spec.js diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js new file mode 100644 index 00000000000..cab5fe2e412 --- /dev/null +++ b/modules/ccxBidAdapter.js @@ -0,0 +1,192 @@ +import * as utils from 'src/utils' +import { registerBidder } from 'src/adapters/bidderFactory' +import { config } from 'src/config' +const BIDDER_CODE = 'ccx' +const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' +const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] +const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv'] +const SUPPORTED_VIDEO_PLAYBACK_METHODS = [1, 2, 3, 4] + +function _getDeviceObj () { + let device = {} + device.w = screen.width + device.y = screen.height + device.ua = navigator.userAgent + return device +} + +function _getSiteObj () { + let site = {} + let url = config.getConfig('pageUrl') || utils.getTopWindowUrl() + if (url.length > 0) { + url = url.split('?')[0] + } + site.page = url + + return site +} + +function _validateSizes (sizeObj, type) { + if (!utils.isArray(sizeObj)) { + return false + } + + if (type === 'video' && (typeof sizeObj[0] === 'undefined' || !utils.isArray(sizeObj[0]) || sizeObj[0].length !== 2)) { + return false + } + + if (type === 'banner') { + if (typeof sizeObj[0] === 'undefined') { + return false + } else { + let result = true + utils._each(sizeObj, function (size) { + if (!utils.isArray(size) || (size.length !== 2)) { + result = false + } + }) + return result + } + } + + return true +} + +function _buildBid (bid) { + let placement = {} + placement.id = bid.bidId + placement.secure = 1 + + if (utils.deepAccess(bid, 'mediaTypes.banner')) { + placement.banner = {'format': []} + let sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes') + utils._each(sizes, function (size) { + placement.banner.format.push({'w': size[0], 'h': size[1]}) + }) + } else if (utils.deepAccess(bid, 'mediaTypes.video')) { + placement.video = {} + + let size = utils.deepAccess(bid, 'mediaTypes.video.playerSize') + + if (typeof size !== 'undefined') { + placement.video.w = size[0][0] + placement.video.h = size[0][1] + } + + placement.video.protocols = utils.deepAccess(bid, 'params.video.protocols') || SUPPORTED_VIDEO_PROTOCOLS + placement.video.mimes = utils.deepAccess(bid, 'params.video.mimes') || SUPPORTED_VIDEO_MIMES + placement.video.playbackmethod = utils.deepAccess(bid, 'params.video.playbackmethod') || SUPPORTED_VIDEO_PLAYBACK_METHODS + placement.video.skip = utils.deepAccess(bid, 'params.video.skip') || 0 + if (placement.video.skip === 1 && utils.deepAccess(bid, 'params.video.skipafter')) { + placement.video.skipafter = utils.deepAccess(bid, 'params.video.skipafter') + } + } + + placement.ext = {'pid': bid.params.placementId} + + return placement +} + +function _buildResponse (bid, currency, ttl) { + let resp = { + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + netRevenue: false, + ttl: ttl, + currency: currency + } + + if (bid.ext.type === 'video') { + resp.vastXml = bid.adm + } else { + resp.ad = bid.adm + } + + if (utils.deepAccess(bid, 'dealid')) { + resp.dealId = bid.dealid + } + + return resp +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + + isBidRequestValid: function (bid) { + if (!utils.deepAccess(bid, 'params.placementId')) { + utils.logWarn('placementId param is reqeuired.') + return false + } + if (utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { + let isValid = _validateSizes(bid.mediaTypes.banner.sizes, 'banner') + if (!isValid) { + utils.logWarn('Bid sizes are invalid.') + } + return isValid + } else if (utils.deepAccess(bid, 'mediaTypes.video.playerSize')) { + let isValid = _validateSizes(bid.mediaTypes.video.playerSize, 'video') + if (!isValid) { + utils.logWarn('Bid sizes are invalid.') + } + return isValid + } else { + utils.logWarn('Bid sizes are required.') + return false + } + }, + buildRequests: function (validBidRequests, bidderRequest) { + // check if validBidRequests is not empty + if (validBidRequests.length > 0) { + let requestBody = {} + requestBody.imp = [] + requestBody.site = _getSiteObj() + requestBody.device = _getDeviceObj() + requestBody.id = bidderRequest.bids[0].auctionId + requestBody.ext = {'ce': (utils.cookiesAreEnabled() ? 1 : 0)} + utils._each(validBidRequests, function (bid) { + requestBody.imp.push(_buildBid(bid)) + }) + // Return the server request + return { + 'method': 'POST', + 'url': BID_URL, + 'data': JSON.stringify(requestBody) + } + } + }, + interpretResponse: function (serverResponse, request) { + const bidResponses = [] + + // response is not empty (HTTP 204) + if (!utils.isEmpty(serverResponse.body)) { + utils._each(serverResponse.body.seatbid, function (seatbid) { + utils._each(seatbid.bid, function (bid) { + bidResponses.push(_buildResponse(bid, serverResponse.body.cur, serverResponse.body.ext.ttl)) + }) + }) + } + + return bidResponses + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = [] + + if (utils.deepAccess(serverResponses[0], 'body.ext.usersync') && !utils.isEmpty(serverResponses[0].body.ext.usersync)) { + utils._each(serverResponses[0].body.ext.usersync, function (match) { + if ((syncOptions.iframeEnabled && match.type === 'iframe') || (syncOptions.pixelEnabled && match.type === 'image')) { + syncs.push({ + type: match.type, + url: match.url + }) + } + }) + } + + return syncs + } +} +registerBidder(spec) diff --git a/modules/ccxBidAdapter.md b/modules/ccxBidAdapter.md new file mode 100644 index 00000000000..24c10ff4cfa --- /dev/null +++ b/modules/ccxBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +Module Name: Clickonometrics Bidder Adapter +Module Type: Bidder Adapter +Maintainer: it@clickonometrics.pl + +# Description + +Module that connects to Clickonometrics's demand sources + +# Test Parameters + + var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "ccx", + params: { + placementId: 3286844 + } + } + ] + }, + { + code: 'test-video', + mediaTypes: { + video: { + playerSize: [1920, 1080] + + } + }, + bids: [ + { + bidder: "ccx", + params: { + placementId: 3287742, + //following options are not required, default values will be used. Uncomment if you want to use custom values + /*video: { + //check OpenRTB documentation for following options description + protocols: [2, 3, 5, 6], //default + mimes: ["video/mp4", "video/x-flv"], //default + playbackmethod: [1, 2, 3, 4], //default + skip: 1, //default 0 + skipafter: 5 //delete this key if skip = 0 + }*/ + } + } + ] + } + + ]; \ No newline at end of file diff --git a/test/spec/modules/ccxBidAdapter_spec.js b/test/spec/modules/ccxBidAdapter_spec.js new file mode 100644 index 00000000000..8b715439df6 --- /dev/null +++ b/test/spec/modules/ccxBidAdapter_spec.js @@ -0,0 +1,318 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ccxBidAdapter'; +import * as utils from 'src/utils'; + +describe('ccxAdapter', () => { + let bids = [{ + adUnitCode: 'banner', + auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9', + bidId: '2e56e1af51a5d7', + bidder: 'ccx', + bidderRequestId: '17e7b9f58a607e', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 607 + }, + sizes: [[300, 250]], + transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' + }, + { + adUnitCode: 'video', + auctionId: '0b9de793-8eda-481e-a548-c187d58b28d9', + bidId: '3u94t90ut39tt3t', + bidder: 'ccx', + bidderRequestId: '23ur20r239r2r', + mediaTypes: { + video: { + playerSize: [[640, 480]] + } + }, + params: { + placementId: 608 + }, + sizes: [[640, 480]], + transactionId: 'aefddd38-cfa0-48ab-8bdd-325de4bab5f9' + }]; + describe('isBidRequestValid', () => { + it('Valid bid requests', () => { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + expect(spec.isBidRequestValid(bids[1])).to.be.true; + }); + it('Invalid bid reqeusts - no placementId', () => { + let bidsClone = utils.deepClone(bids); + bidsClone[0].params = undefined; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + }); + it('Invalid bid reqeusts - invalid banner sizes', () => { + let bidsClone = utils.deepClone(bids); + bidsClone[0].mediaTypes.banner.sizes = [300, 250]; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + bidsClone[0].mediaTypes.banner.sizes = [[300, 250], [750]]; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + bidsClone[0].mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bidsClone[0])).to.be.false; + }); + it('Invalid bid reqeusts - invalid video sizes', () => { + let bidsClone = utils.deepClone(bids); + bidsClone[1].mediaTypes.video.playerSize = []; + expect(spec.isBidRequestValid(bidsClone[1])).to.be.false; + bidsClone[1].mediaTypes.video.sizes = [640, 480]; + expect(spec.isBidRequestValid(bidsClone[1])).to.be.false; + }); + }); + + describe('buildRequests', function () { + it('No valid bids', function () { + expect(spec.buildRequests([])).to.be.empty; + }); + + it('Valid bid request - default', function () { + let response = spec.buildRequests(bids, {bids}); + expect(response).to.be.not.empty; + expect(response.data).to.be.not.empty; + + let data = JSON.parse(response.data); + + expect(data).to.be.an('object'); + expect(data).to.have.keys('site', 'imp', 'id', 'ext', 'device'); + + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + }, + { + video: { + w: 640, + h: 480, + protocols: [2, 3, 5, 6], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [1, 2, 3, 4], + skip: 0 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + expect(data.imp).to.deep.have.same.members(imps); + }); + + it('Valid bid request - custom', function () { + let bidsClone = utils.deepClone(bids); + let imps = [ + { + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + }, + ext: { + pid: 607 + }, + id: '2e56e1af51a5d7', + secure: 1 + }, + { + video: { + w: 640, + h: 480, + protocols: [5, 6], + mimes: ['video/mp4'], + playbackmethod: [3], + skip: 1, + skipafter: 5 + }, + id: '3u94t90ut39tt3t', + secure: 1, + ext: { + pid: 608 + } + } + ]; + + bidsClone[1].params.video = {}; + bidsClone[1].params.video.protocols = [5, 6]; + bidsClone[1].params.video.mimes = ['video/mp4']; + bidsClone[1].params.video.playbackmethod = [3]; + bidsClone[1].params.video.skip = 1; + bidsClone[1].params.video.skipafter = 5; + + let response = spec.buildRequests(bidsClone, {'bids': bidsClone}); + let data = JSON.parse(response.data); + + expect(data.imp).to.deep.have.same.members(imps); + }); + }); + + let response = { + id: '0b9de793-8eda-481e-a548-c187d58b28d9', + seatbid: [ + {bid: [ + { + id: '2e56e1af51a5d7_221', + impid: '2e56e1af51a5d7', + price: 8.1, + adid: '221', + adm: '', + adomain: ['clickonometrics.com'], + crid: '221', + w: 300, + h: 250, + ext: { + type: 'standard' + } + }, + { + id: '2e56e1af51a5d8_222', + impid: '2e56e1af51a5d8', + price: 5.68, + adid: '222', + adm: '', + adomain: ['clickonometrics.com'], + crid: '222', + w: 640, + h: 480, + ext: { + type: 'video' + } + } + ]} + ], + cur: 'PLN', + ext: { + ttl: 5, + usersync: [ + { + type: 'image', + url: 'http://foo.sync?param=1' + }, + { + type: 'iframe', + url: 'http://foo.sync?param=2' + } + ] + } + }; + + describe('interpretResponse', function () { + it('Valid bid response - multi', function () { + let bidResponses = [ + { + requestId: '2e56e1af51a5d7', + cpm: 8.1, + width: 300, + height: 250, + creativeId: '221', + netRevenue: false, + ttl: 5, + currency: 'PLN', + ad: '' + }, + { + requestId: '2e56e1af51a5d8', + cpm: 5.68, + width: 640, + height: 480, + creativeId: '222', + netRevenue: false, + ttl: 5, + currency: 'PLN', + vastXml: '' + } + ]; + expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); + }); + + it('Valid bid response - single', function () { + delete response.seatbid[0].bid[1]; + let bidResponses = [ + { + requestId: '2e56e1af51a5d7', + cpm: 8.1, + width: 300, + height: 250, + creativeId: '221', + netRevenue: false, + ttl: 5, + currency: 'PLN', + ad: '' + } + ]; + expect(spec.interpretResponse({body: response})).to.deep.have.same.members(bidResponses); + }); + + it('Empty bid response', function () { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); + describe('getUserSyncs', function () { + it('Valid syncs - all', function () { + let syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + + let expectedSyncs = [ + { + type: 'image', + url: 'http://foo.sync?param=1' + }, + { + type: 'iframe', + url: 'http://foo.sync?param=2' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - only image', function () { + let syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + let expectedSyncs = [ + { + type: 'image', url: 'http://foo.sync?param=1' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - only iframe', function () { + let syncOptions = {iframeEnabled: true, pixelEnabled: false}; + let expectedSyncs = [ + { + type: 'iframe', url: 'http://foo.sync?param=2' + } + ]; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.deep.have.same.members(expectedSyncs); + }); + + it('Valid syncs - empty', function () { + let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + response.ext.usersync = {}; + expect(spec.getUserSyncs(syncOptions, [{body: response}])).to.be.empty; + }); + }); +});