From a2b5cfa68fff56d72db649b63364239b6aec85f6 Mon Sep 17 00:00:00 2001 From: Denis Logachev Date: Tue, 7 Nov 2017 18:15:20 +0200 Subject: [PATCH] Add AdkernelAdn adapter (#1747) --- integrationExamples/gpt/pbjs_example_gpt.html | 7 + modules/adkernelAdnBidAdapter.js | 145 ++++++++++ modules/adkernelAdnBidAdapter.md | 45 ++++ src/utils.js | 8 + .../modules/adkernelAdnBidAdapter_spec.js | 254 ++++++++++++++++++ 5 files changed, 459 insertions(+) create mode 100644 modules/adkernelAdnBidAdapter.js create mode 100644 modules/adkernelAdnBidAdapter.md create mode 100644 test/spec/modules/adkernelAdnBidAdapter_spec.js diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index 77c875b9787..07e5bb8236f 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -268,6 +268,13 @@ params: { placement_id: 0 } + }, + { + bidder: 'adkernelAdn', + params: { + pubId: 50357, //REQUIRED + host: 'dsp-staging.adkernel.com' //OPTIONAL + } } ] }, { diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js new file mode 100644 index 00000000000..b1c8bbf398b --- /dev/null +++ b/modules/adkernelAdnBidAdapter.js @@ -0,0 +1,145 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import { BANNER, VIDEO } from 'src/mediaTypes'; + +const DEFAULT_ADKERNEL_DSP_DOMAIN = 'tag.adkernel.com'; +const VIDEO_TARGETING = ['mimes', 'protocols', 'api']; +const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; +const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; +const DEFAULT_APIS = [1, 2]; + +function isRtbDebugEnabled() { + return utils.getTopWindowLocation().href.indexOf('adk_debug=true') !== -1; +} + +function buildImp(bidRequest) { + const sizes = bidRequest.sizes; + let imp = { + id: bidRequest.bidId, + tagid: bidRequest.placementCode + }; + if (bidRequest.mediaType === 'video' || utils.deepAccess(bidRequest, 'mediaTypes.video')) { + imp.video = { + w: sizes[0], + h: sizes[1], + mimes: DEFAULT_MIMES, + protocols: DEFAULT_PROTOCOLS, + api: DEFAULT_APIS + }; + if (bidRequest.params.video) { + Object.keys(bidRequest.params.video) + .filter(param => VIDEO_TARGETING.includes(param)) + .forEach(param => imp.video[param] = bidRequest.params.video[param]); + } + } else { + imp.banner = { + format: utils.parseSizesInput(bidRequest.sizes) + }; + } + return imp; +} + +function buildRequestParams(auctionId, transactionId, tags) { + let loc = utils.getTopWindowLocation(); + return { + id: auctionId, + tid: transactionId, + site: { + page: loc.href, + ref: utils.getTopWindowReferrer(), + secure: ~~(loc.protocol === 'https:') + }, + imp: tags + }; +} + +function buildBid(tag) { + let bid = { + requestId: tag.impid, + bidderCode: spec.code, + cpm: tag.bid, + width: tag.w, + height: tag.h, + creativeId: tag.crid, + currency: 'USD', + ttl: 720, + netRevenue: true + }; + if (tag.tag) { + bid.ad = `${tag.tag}`; + bid.mediaType = BANNER; + } else if (tag.vast_url) { + bid.vastUrl = tag.vast_url; + bid.mediaType = VIDEO; + } + return bid; +} + +export const spec = { + + code: 'adkernelAdn', + + supportedMediaTypes: [VIDEO], + + isBidRequestValid: function(bidRequest) { + return 'params' in bidRequest && (typeof bidRequest.params.host === 'undefined' || typeof bidRequest.params.host === 'string') && + typeof bidRequest.params.pubId === 'number'; + }, + + buildRequests: function(bidRequests) { + let transactionId; + let auctionId; + let dispatch = bidRequests.map(buildImp) + .reduce((acc, curr, index) => { + let bidRequest = bidRequests[index]; + let pubId = bidRequest.params.pubId; + let host = bidRequest.params.host || DEFAULT_ADKERNEL_DSP_DOMAIN; + acc[host] = acc[host] || {}; + acc[host][pubId] = acc[host][pubId] || []; + acc[host][pubId].push(curr); + transactionId = bidRequest.transactionId; + auctionId = bidRequest.bidderRequestId; + return acc; + }, {}); + let requests = []; + Object.keys(dispatch).forEach(host => { + Object.keys(dispatch[host]).forEach(pubId => { + let request = buildRequestParams(auctionId, transactionId, dispatch[host][pubId]); + requests.push({ + method: 'POST', + url: `//${host}/tag?account=${pubId}&pb=1${isRtbDebugEnabled() ? '&debug=1' : ''}`, + data: JSON.stringify(request) + }) + }); + }); + return requests; + }, + + interpretResponse: function(serverResponse) { + let response = serverResponse.body; + if (!response.tags) { + return []; + } + if (response.debug) { + utils.logInfo(`ADKERNEL DEBUG:\n${response.debug}`); + } + return response.tags.map(buildBid); + }, + + getUserSyncs: function(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled || !serverResponses || serverResponses.length === 0) { + return []; + } + return serverResponses.filter(rps => 'syncpages' in rps.body) + .map(rsp => rsp.body.syncpages) + .reduce((a, b) => a.concat(b), []) + .map(sync_url => { + return { + type: 'iframe', + url: sync_url + } + }); + } +}; + +registerBidder(spec); diff --git a/modules/adkernelAdnBidAdapter.md b/modules/adkernelAdnBidAdapter.md new file mode 100644 index 00000000000..d69bf3b8998 --- /dev/null +++ b/modules/adkernelAdnBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: AdKernel ADN Bidder Adapter +Module Type: Bidder Adapter +Maintainer: denis@adkernel.com +``` + +# Description + +Connects to AdKernel Ad Delivery Network +Banner and video formats are supported. + + +# Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + sizes: [[300, 250], [300, 200]], + bids: [ + { + bidder: 'adkernelAdn', + params: { + pubId: 50357, + host: 'dsp-staging.adkernel.com' + } + } + ] + }, { + code: 'video-ad-player', + sizes: [640, 480], + bids: [ + { + bidder: 'adkernelAdn', + mediaType : 'video', + params: { + pubId: 50357, + host: 'dsp-staging.adkernel.com' + } + } + ] + } + ]; +``` diff --git a/src/utils.js b/src/utils.js index 9efa4f53c57..f14a9bbd46c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -178,6 +178,14 @@ exports.getTopWindowUrl = function () { return href; }; +exports.getTopWindowReferrer = function() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } +}; + exports.logWarn = function (msg) { if (debugTurnedOn() && console.warn) { console.warn('WARNING: ' + msg); diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js new file mode 100644 index 00000000000..3ba4012bd0b --- /dev/null +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -0,0 +1,254 @@ +import {expect} from 'chai'; +import {spec} from 'modules/adkernelAdnBidAdapter'; +import * as utils from 'src/utils'; + +describe('AdkernelAdn adapter', () => { + const bid1_pub1 = { + bidder: 'adkernelAdn', + transactionId: 'transact0', + bidderRequestId: 'req0', + bidId: 'bidid_1', + params: { + pubId: 1 + }, + placementCode: 'ad-unit-1', + sizes: [[300, 250], [300, 200]] + }, + bid2_pub1 = { + bidder: 'adkernelAdn', + transactionId: 'transact1', + bidderRequestId: 'req1', + bidId: 'bidid_2', + params: { + pubId: 1 + }, + placementCode: 'ad-unit-2', + sizes: [[300, 250]] + }, + bid1_pub2 = { + bidder: 'adkernelAdn', + transactionId: 'transact2', + bidderRequestId: 'req1', + bidId: 'bidid_3', + params: { + pubId: 7, + host: 'dps-test.com' + }, + placementCode: 'ad-unit-2', + sizes: [[728, 90]] + }, bid_video1 = { + bidder: 'adkernelAdn', + transactionId: 'transact3', + bidderRequestId: 'req1', + bidId: 'bidid_4', + mediaType: 'video', + sizes: [640, 300], + placementCode: 'video_wrapper', + params: { + pubId: 7, + video: { + mimes: ['video/mp4', 'video/webm', 'video/x-flv'], + api: [1, 2, 3, 4], + protocols: [1, 2, 3, 4, 5, 6] + } + } + }, bid_video2 = { + bidder: 'adkernelAdn', + transactionId: 'transact3', + bidderRequestId: 'req1', + bidId: 'bidid_5', + mediaTypes: {video: {context: 'instream'}}, + sizes: [640, 300], + placementCode: 'video_wrapper2', + params: { + pubId: 7, + video: { + mimes: ['video/mp4', 'video/webm', 'video/x-flv'], + api: [1, 2, 3, 4], + protocols: [1, 2, 3, 4, 5, 6] + } + } + }; + + const response = { + tags: [{ + id: 'ad-unit-1', + impid: '2c5e951baeeadd', + crid: '108_159810', + bid: 5.0, + tag: '', + w: 300, + h: 250 + }, { + id: 'ad-unit-2', + impid: '31d798477126c4', + crid: '108_21226', + bid: 2.5, + tag: '', + w: 300, + h: 250 + }, { + id: 'video_wrapper', + impid: '57d602ad1c9545', + crid: '108_158802', + bid: 10.0, + vast_url: 'http://vast.com/vast.xml' + }], + syncpages: ['https://dsp.adkernel.com/sync'] + }, usersyncOnlyResponse = { + syncpages: ['https://dsp.adkernel.com/sync'] + }; + + describe('input parameters validation', () => { + it('empty request shouldn\'t generate exception', () => { + expect(spec.isBidRequestValid({ + bidderCode: 'adkernelAdn' + })).to.be.equal(false); + }); + it('request without pubid should be ignored', () => { + expect(spec.isBidRequestValid({ + bidder: 'adkernelAdn', + params: {}, + placementCode: 'ad-unit-0', + sizes: [[300, 250]] + })).to.be.equal(false); + }); + it('request with invalid pubid should be ignored', () => { + expect(spec.isBidRequestValid({ + bidder: 'adkernelAdn', + params: { + pubId: 'invalid id' + }, + placementCode: 'ad-unit-0', + sizes: [[300, 250]] + })).to.be.equal(false); + }); + }); + + describe('banner request building', () => { + let pbRequest; + let tagRequest; + + before(() => { + let mock = sinon.stub(utils, 'getTopWindowLocation', () => { + return { + protocol: 'https:', + hostname: 'example.com', + host: 'example.com', + pathname: '/index.html', + href: 'https://example.com/index.html' + }; + }); + pbRequest = spec.buildRequests([bid1_pub1])[0]; + tagRequest = JSON.parse(pbRequest.data); + mock.restore(); + }); + + it('should have request id', () => { + expect(tagRequest).to.have.property('id'); + }); + it('should have transaction id', () => { + expect(tagRequest).to.have.property('tid'); + }); + it('should have sizes', () => { + expect(tagRequest.imp[0].banner).to.have.property('format'); + expect(tagRequest.imp[0].banner.format).to.be.eql(['300x250', '300x200']); + }); + it('should have impression id', () => { + expect(tagRequest.imp[0]).to.have.property('id', 'bidid_1'); + }); + it('should have tagid', () => { + expect(tagRequest.imp[0]).to.have.property('tagid', 'ad-unit-1'); + }); + it('should create proper site block', () => { + expect(tagRequest.site).to.have.property('page', 'https://example.com/index.html'); + expect(tagRequest.site).to.have.property('secure', 1); + }); + }); + + describe('video request building', () => { + let pbRequest = spec.buildRequests([bid_video1, bid_video2])[0]; + let tagRequest = JSON.parse(pbRequest.data); + + it('should have video object', () => { + expect(tagRequest.imp[0]).to.have.property('video'); + expect(tagRequest.imp[1]).to.have.property('video'); + }); + it('should have tagid', () => { + expect(tagRequest.imp[0]).to.have.property('tagid', 'video_wrapper'); + expect(tagRequest.imp[1]).to.have.property('tagid', 'video_wrapper2'); + }); + }); + + describe('requests routing', () => { + it('should issue a request for each publisher', () => { + let pbRequests = spec.buildRequests([bid1_pub1, bid_video1]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string(`account=${bid1_pub1.params.pubId}`); + expect(pbRequests[1].url).to.have.string(`account=${bid1_pub2.params.pubId}`); + let tagRequest1 = JSON.parse(pbRequests[0].data); + let tagRequest2 = JSON.parse(pbRequests[1].data); + expect(tagRequest1.imp).to.have.length(1); + expect(tagRequest2.imp).to.have.length(1); + }); + it('should issue a request for each host', () => { + let pbRequests = spec.buildRequests([bid1_pub1, bid1_pub2]); + expect(pbRequests).to.have.length(2); + expect(pbRequests[0].url).to.have.string('//tag.adkernel.com/tag'); + expect(pbRequests[1].url).to.have.string(`//${bid1_pub2.params.host}/tag`); + let tagRequest1 = JSON.parse(pbRequests[0].data); + let tagRequest2 = JSON.parse(pbRequests[1].data); + expect(tagRequest1.imp).to.have.length(1); + expect(tagRequest2.imp).to.have.length(1); + }); + }); + + describe('responses processing', () => { + let responses; + before(() => { + responses = spec.interpretResponse({body: response}); + }); + it('should parse all responses', () => { + expect(responses).to.have.length(3); + }); + it('should return fully-initialized bid-response', () => { + let resp = responses[0]; + expect(resp).to.have.property('bidderCode', 'adkernelAdn'); + expect(resp).to.have.property('requestId', '2c5e951baeeadd'); + expect(resp).to.have.property('cpm', 5.0); + expect(resp).to.have.property('width', 300); + expect(resp).to.have.property('height', 250); + expect(resp).to.have.property('creativeId', '108_159810'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('mediaType', 'banner'); + expect(resp).to.have.property('ad'); + expect(resp.ad).to.have.string(''); + }); + it('should return fully-initialized video bid-response', () => { + let resp = responses[2]; + expect(resp).to.have.property('bidderCode', 'adkernelAdn'); + expect(resp).to.have.property('requestId', '57d602ad1c9545'); + expect(resp).to.have.property('cpm', 10.0); + expect(resp).to.have.property('creativeId', '108_158802'); + expect(resp).to.have.property('currency'); + expect(resp).to.have.property('ttl'); + expect(resp).to.have.property('mediaType', 'video'); + expect(resp).to.have.property('vastUrl', 'http://vast.com/vast.xml'); + expect(resp).to.not.have.property('ad'); + }); + it('should perform usersync', () => { + let syncs = spec.getUserSyncs({iframeEnabled: false}, [{body: response}]); + expect(syncs).to.have.length(0); + syncs = spec.getUserSyncs({iframeEnabled: true}, [{body: response}]); + expect(syncs).to.have.length(1); + expect(syncs[0]).to.have.property('type', 'iframe'); + expect(syncs[0]).to.have.property('url', 'https://dsp.adkernel.com/sync'); + }); + it('should handle user-sync only response', () => { + let request = spec.buildRequests([bid1_pub1])[0]; + let resp = spec.interpretResponse({body: usersyncOnlyResponse}, request); + expect(resp).to.have.length(0); + }); + }); +});