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;
+ });
+ });
+});