From 320147ea3e4e5470abf2e83af750ce99ebd025f5 Mon Sep 17 00:00:00 2001 From: Justas Pupelis Date: Tue, 9 Oct 2018 13:14:54 +0300 Subject: [PATCH 1/2] adformOpenRtb native adapter --- modules/adformOpenRTBAdapter.js | 173 +++++++ modules/adformOpenRTBAdapter.md | 59 +++ .../spec/modules/adformOpenRTBAdapter_spec.js | 473 ++++++++++++++++++ 3 files changed, 705 insertions(+) create mode 100644 modules/adformOpenRTBAdapter.js create mode 100644 modules/adformOpenRTBAdapter.md create mode 100644 test/spec/modules/adformOpenRTBAdapter_spec.js diff --git a/modules/adformOpenRTBAdapter.js b/modules/adformOpenRTBAdapter.js new file mode 100644 index 00000000000..41bb9db9fcd --- /dev/null +++ b/modules/adformOpenRTBAdapter.js @@ -0,0 +1,173 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import { + registerBidder +} from 'src/adapters/bidderFactory'; +import { + NATIVE +} from 'src/mediaTypes'; +import * as utils from 'src/utils'; + +const BIDDER_CODE = 'adformOpenRTB'; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + icon: { + id: 2, + type: 1, + name: 'img' + }, + image: { + id: 3, + type: 3, + name: 'img' + }, + sponsoredBy: { + id: 5, + name: 'data', + type: 1 + }, + body: { + id: 4, + name: 'data', + type: 2 + }, + cta: { + id: 1, + type: 12, + name: 'data' + } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ NATIVE ], + isBidRequestValid: bid => !!bid.params.mid, + buildRequests: (validBidRequests, bidderRequest) => { + const page = bidderRequest.refererInfo.referer; + const adxDomain = setOnAny(validBidRequests, 'params.adxDomain') || 'adx.adform.net'; + const ua = navigator.userAgent; + const pt = setOnAny(validBidRequests, 'params.pt') || setOnAny(validBidRequests, 'params.priceType') || 'net'; + const tid = validBidRequests[0].transactionId; // ??? check with ssp + const test = setOnAny(validBidRequests, 'params.test'); + const publisher = setOnAny(validBidRequests, 'params.publisher'); + const siteId = setOnAny(validBidRequests, 'params.siteId'); + + const imp = validBidRequests.map((bid, id) => { + bid.netRevenue = pt; + const assets = utils._map(bid.nativeParams, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + asset[props.name] = { + len: bidParams.len, + wmin: bidParams.sizes && bidParams.sizes[0], + hmin: bidParams.sizes && bidParams.sizes[1], + type: props.type + }; + + return asset; + } + }).filter(Boolean); + + return { + id: id + 1, + tagid: bid.params.mid, + native: { + request: { + assets + } + } + }; + }); + + const request = { + id: bidderRequest.auctionId, + site: { id: siteId, page, publisher }, + device: { ua }, + source: { tid, fd: 1 }, + ext: { pt }, + imp + }; + + if (test) { + request.is_debug = !!test; + request.test = 1; + } + if (utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + request.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + request.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies & 1 } }; + } + + return { + method: 'POST', + url: '//' + adxDomain + '/adx/openrtb', + data: JSON.stringify(request), + options: { + contentType: 'application/json' + }, + bids: validBidRequests + }; + }, + interpretResponse: function(serverResponse, { bids }) { + if (!serverResponse.body) { + return; + } + const { seatbid, cur } = serverResponse.body; + + return bids.map((bid, id) => { + const _cbid = seatbid && seatbid[id] && seatbid[id].bid; + const bidResponse = _cbid && _cbid[0]; + if (bidResponse) { + return { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: cur, + mediaType: NATIVE, + bidderCode: BIDDER_CODE, + native: parseNative(bidResponse) + }; + } + }).filter(Boolean); + } +}; + +registerBidder(spec); + +function parseNative(bid) { + const { assets, link, imptrackers, jstracker } = bid.native; + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined, + impressionTrackers: imptrackers || undefined, + javascriptTrackers: jstracker ? [ jstracker ] : undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + + return result; +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + if (result) { + return result; + } + } +} diff --git a/modules/adformOpenRTBAdapter.md b/modules/adformOpenRTBAdapter.md new file mode 100644 index 00000000000..66cfbf117e5 --- /dev/null +++ b/modules/adformOpenRTBAdapter.md @@ -0,0 +1,59 @@ +# Overview + +Module Name: Adform OpenRTB Adapter +Module Type: Bidder Adapter +Maintainer: Scope.FL.Scripts@adform.com + +# Description + +Module that connects to Adform demand sources to fetch bids. +Only native format is supported. Using OpenRTB standard. + +# Test Parameters +``` + var adUnits = [ + code: '/19968336/prebid_native_example_1', + sizes: [ + [360, 360] + ], + mediaTypes: { + native: { + image: { + required: false, + sizes: [50, 50] + }, + title: { + required: false, + len: 140 + }, + sponsoredBy: { + required: false + }, + clickUrl: { + required: false + }, + body: { + required: false + }, + icon: { + required: false, + sizes: [50, 50] + } + } + }, + bids: [{ + bidder: 'adformOpenRTB', + params: { + mid: 161556, + adxDomain: 'axd.adform.net', //optional + site: '23455', //optional + priceType: 'gross' //optional, default is 'net' + publisher: { //optional block + id: "2706", + name: "Publishers Name", + domain: "publisher.com" + } + } + }] + ]; +``` diff --git a/test/spec/modules/adformOpenRTBAdapter_spec.js b/test/spec/modules/adformOpenRTBAdapter_spec.js new file mode 100644 index 00000000000..8e101c3d41b --- /dev/null +++ b/test/spec/modules/adformOpenRTBAdapter_spec.js @@ -0,0 +1,473 @@ +// jshint esversion: 6, es3: false, node: true +import {assert, expect} from 'chai'; +import * as url from 'src/url'; +import {spec} from 'modules/adformOpenRTBAdapter'; +import { NATIVE } from 'src/mediaTypes'; + +describe('AdformOpenRTB adapter', function () { + let serverResponse, bidRequest, bidResponses; + let bids = []; + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'adformOpenRTB', + 'params': { + 'mid': '19910113' + } + }; + + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params are missing', function () { + bid.params = { adxDomain: 'adx.adform.net' }; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests', function () { + it('should send request with correct structure', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { + siteId: 'siteId', + adxDomain: '10.8.57.207' + } + }]; + let request = spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }); + + assert.equal(request.method, 'POST'); + assert.equal(request.url, '//10.8.57.207/adx/openrtb'); + assert.deepEqual(request.options, {contentType: 'application/json'}); + assert.ok(request.data); + }); + + describe('gdpr', function () { + it('should send GDPR Consent data to adform if gdprApplies', function () { + let validBidRequests = [{ bidId: 'bidId', params: { siteId: 'siteId', test: 1 } }]; + let bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { referer: 'page' } }; + let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); + + assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); + assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); + assert.equal(typeof request.regs.ext.gdpr, 'number'); + }); + + it('should send gdpr as number', function () { + let validBidRequests = [{ bidId: 'bidId', params: { siteId: 'siteId', test: 1 } }]; + let bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentDataString' }, refererInfo: { referer: 'page' } }; + let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); + + assert.equal(typeof request.regs.ext.gdpr, 'number'); + }); + + it('should not send GDPR Consent data to adform if gdprApplies is false or undefined', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }]; + let bidderRequest = {gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, refererInfo: { referer: 'page' }}; + let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); + + assert.equal(request.user, undefined); + assert.equal(request.regs, undefined); + }); + it('should send default GDPR Consent data to adform', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.user, undefined); + assert.equal(request.regs, undefined); + }); + }); + + it('should add test and is_debug to request, if test is set in parameters', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', test: 1 } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.ok(request.is_debug); + assert.equal(request.test, 1); + }); + + it('should have default request structure', function () { + let keys = 'site,device,source,ext,imp'.split(','); + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + let data = Object.keys(request); + + assert.deepEqual(keys, data); + }); + + it('should set request keys correct values', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' }, + transactionId: 'transactionId' + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.source.tid, validBidRequests[0].transactionId); + assert.equal(request.source.fd, 1); + }); + + it('should send info about device', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.device.ua, navigator.userAgent); + }); + it('should send info about the site', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', publisher: {id: '123123', domain: 'publisher.domain.com', name: 'publisher\'s name'} } + }]; + let refererInfo = { referer: 'page' }; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo }).data); + + assert.deepEqual(request.site, { + page: refererInfo.referer, + publisher: validBidRequests[0].params.publisher, + id: validBidRequests[0].params.siteId + }); + }); + + describe('priceType', function () { + it('should send default priceType', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.ext.pt, 'net'); + }); + it('should send correct priceType value', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', priceType: 'net' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.ext.pt, 'net'); + }); + }); + + describe('bids', function () { + it('should add more than one bid to the request', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }, { + bidId: 'bidId2', + params: { siteId: 'siteId' } + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.imp.length, 2); + }); + it('should add incrementing values of id', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId' } + }, { + bidId: 'bidId2', + params: { siteId: 'siteId' } + }, { + bidId: 'bidId3', + params: { siteId: 'siteId' } + }]; + let imps = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp; + + for (let i = 0; i < 3; i++) { + assert.equal(imps[i].id, i + 1); + } + }); + + it('should add mid', function () { + let validBidRequests = [{ bidId: 'bidId', params: { siteId: 'siteId', mid: 1000 } }, + { bidId: 'bidId2', params: { siteId: 'siteId', mid: 1001 } }, + { bidId: 'bidId3', params: { siteId: 'siteId', mid: 1002 } }]; + let imps = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp; + for (let i = 0; i < 3; i++) { + assert.equal(imps[i].tagid, validBidRequests[i].params.mid); + } + }); + + describe('native', function () { + describe('assets', function () { + it('should set correct asset id', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, + body: { len: 140 } + } + }]; + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp[0].native.request.assets; + + assert.equal(assets[0].id, 0); + assert.equal(assets[1].id, 3); + assert.equal(assets[2].id, 4); + }); + it('should add required key if it is necessary', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, + body: { len: 140 }, + sponsoredBy: { required: true, len: 140 } + } + }]; + + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp[0].native.request.assets; + + assert.equal(assets[0].required, 1); + assert.ok(!assets[1].required); + assert.ok(!assets[2].required); + assert.equal(assets[3].required, 1); + }); + + it('should map img and data assets', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: true, sizes: [150, 50] }, + icon: { required: false, sizes: [50, 50] }, + body: { required: false, len: 140 }, + sponsoredBy: { required: true }, + cta: { required: false }, + clickUrl: { required: false } + } + }]; + + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp[0].native.request.assets; + assert.ok(assets[0].title); + assert.equal(assets[0].title.len, 140); + assert.deepEqual(assets[1].img, { type: 3, wmin: 150, hmin: 50 }); + assert.deepEqual(assets[2].img, { type: 1, wmin: 50, hmin: 50 }); + assert.deepEqual(assets[3].data, { type: 2, len: 140 }); + assert.deepEqual(assets[4].data, { type: 1 }); + assert.deepEqual(assets[5].data, { type: 12 }); + assert.ok(!assets[6]); + }); + }); + }); + }); + }); + + describe('interpretResponse', function () { + it('should return if no body in response', function () { + let serverResponse = {}; + let bidRequest = {}; + + assert.ok(!spec.interpretResponse(serverResponse, bidRequest)); + }); + it('should return more than one bids', function () { + let serverResponse = { + body: { + seatbid: [{ + bid: [{impid: 'impid1', native: {ver: '1.1', link: { url: 'link' }, assets: [{id: 1, title: {text: 'Asset title text'}}]}}] + }, { + bid: [{impid: 'impid2', native: {ver: '1.1', link: { url: 'link' }, assets: [{id: 1, data: {value: 'Asset title text'}}]}}] + }] + } + }; + let bidRequest = { + data: {}, + bids: [ + { + bidId: 'bidId1', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, + body: { len: 140 } + } + }, + { + bidId: 'bidId2', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, + body: { len: 140 } + } + } + ] + }; + + bids = spec.interpretResponse(serverResponse, bidRequest); + assert.equal(spec.interpretResponse(serverResponse, bidRequest).length, 2); + }); + it('should set correct values to bid', function () { + let serverResponse = { + body: { + id: null, + bidid: null, + seatbid: [{ + bid: [ + { + impid: 'impid1', + price: 93.1231, + crid: '12312312', + native: { + assets: [], + link: { url: 'link' }, + imptrackers: ['imptrackers url1', 'imptrackers url2'] + } + } + ] + }], + cur: 'NOK' + } + }; + let bidRequest = { + data: {}, + bids: [ + { + bidId: 'bidId1', + params: { siteId: 'siteId', mid: 1000 }, + nativeParams: { + title: { required: true, len: 140 }, + image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, + body: { len: 140 } + } + } + ] + }; + + const bids = spec.interpretResponse(serverResponse, bidRequest); + const bid = serverResponse.body.seatbid[0].bid[0]; + assert.deepEqual(bids[0].requestId, bidRequest.bids[0].bidId); + assert.deepEqual(bids[0].cpm, bid.price); + assert.deepEqual(bids[0].creativeId, bid.crid); + assert.deepEqual(bids[0].ttl, 360); + assert.deepEqual(bids[0].netRevenue, false); + assert.deepEqual(bids[0].currency, serverResponse.body.cur); + assert.deepEqual(bids[0].mediaType, 'native'); + assert.deepEqual(bids[0].bidderCode, 'adformOpenRTB'); + }); + it('should set correct native params', function () { + const bid = [ + { + impid: 'impid1', + price: 93.1231, + crid: '12312312', + native: { + assets: [ + { + data: null, + id: 0, + img: null, + required: 0, + title: {text: 'title', len: null}, + video: null + }, { + data: null, + id: 2, + img: {type: null, url: 'test.url.com/Files/58345/308185.jpg?bv=1', w: 30, h: 10}, + required: 0, + title: null, + video: null + }, { + data: null, + id: 3, + img: {type: null, url: 'test.url.com/Files/58345/308200.jpg?bv=1', w: 100, h: 100}, + required: 0, + title: null, + video: null + }, { + data: {type: null, len: null, value: 'body'}, + id: 4, + img: null, + required: 0, + title: null, + video: null + }, { + data: {type: null, len: null, value: 'cta'}, + id: 1, + img: null, + required: 0, + title: null, + video: null + }, { + data: {type: null, len: null, value: 'sponsoredBy'}, + id: 5, + img: null, + required: 0, + title: null, + video: null + } + ], + link: { url: 'clickUrl', clicktrackers: ['clickTracker1', 'clickTracker2'] }, + imptrackers: ['imptrackers url1', 'imptrackers url2'], + jstracker: 'jstracker' + } + } + ]; + const serverResponse = { + body: { + id: null, + bidid: null, + seatbid: [{ bid }], + cur: 'NOK' + } + }; + let bidRequest = { + data: {}, + bids: [{ bidId: 'bidId1' }] + }; + + const result = spec.interpretResponse(serverResponse, bidRequest)[0].native; + const native = bid[0].native; + const assets = native.assets; + assert.deepEqual({ + clickUrl: native.link.url, + clickTrackers: native.link.clicktrackers, + impressionTrackers: native.imptrackers, + javascriptTrackers: [ native.jstracker ], + title: assets[0].title.text, + icon: {url: assets[1].img.url, width: assets[1].img.w, height: assets[1].img.h}, + image: {url: assets[2].img.url, width: assets[2].img.w, height: assets[2].img.h}, + body: assets[3].data.value, + cta: assets[4].data.value, + sponsoredBy: assets[5].data.value + }, result); + }); + it('should return empty when there is no bids in response', function () { + const serverResponse = { + body: { + id: null, + bidid: null, + seatbid: [{ bid: [] }], + cur: 'NOK' + } + }; + let bidRequest = { + data: {}, + bids: [{ bidId: 'bidId1' }] + }; + const result = spec.interpretResponse(serverResponse, bidRequest)[0]; + assert.ok(!result); + }); + }); +}); From 7772056a8ce750639a0884695e5b888f177688d4 Mon Sep 17 00:00:00 2001 From: Justas Pupelis Date: Wed, 31 Oct 2018 10:23:38 +0200 Subject: [PATCH 2/2] Updated info --- modules/adformOpenRTBAdapter.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/adformOpenRTBAdapter.md b/modules/adformOpenRTBAdapter.md index 66cfbf117e5..f59f039f3ec 100644 --- a/modules/adformOpenRTBAdapter.md +++ b/modules/adformOpenRTBAdapter.md @@ -20,7 +20,7 @@ Only native format is supported. Using OpenRTB standard. native: { image: { required: false, - sizes: [50, 50] + sizes: [100, 50] }, title: { required: false, @@ -44,11 +44,11 @@ Only native format is supported. Using OpenRTB standard. bids: [{ bidder: 'adformOpenRTB', params: { - mid: 161556, - adxDomain: 'axd.adform.net', //optional - site: '23455', //optional - priceType: 'gross' //optional, default is 'net' - publisher: { //optional block + mid: 606169, // required + adxDomain: 'axd.adform.net', // optional + siteId: '23455', // optional + priceType: 'gross' // optional, default is 'net' + publisher: { // optional block id: "2706", name: "Publishers Name", domain: "publisher.com"