From 7281e547e2a94e3b90abdc5c635a05b0ddd7c725 Mon Sep 17 00:00:00 2001 From: Jonas Hilsen Date: Tue, 17 Dec 2019 11:01:33 +0000 Subject: [PATCH] add seedingAlliance Adapter --- modules/seedingAllianceBidAdapter.js | 207 ++++++++++++++++++ modules/seedingAllianceBidAdapter.md | 45 ++++ .../modules/seedingAllianceAdapter_spec.js | 179 +++++++++++++++ 3 files changed, 431 insertions(+) create mode 100755 modules/seedingAllianceBidAdapter.js create mode 100755 modules/seedingAllianceBidAdapter.md create mode 100755 test/spec/modules/seedingAllianceAdapter_spec.js diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js new file mode 100755 index 00000000000..e118849e666 --- /dev/null +++ b/modules/seedingAllianceBidAdapter.js @@ -0,0 +1,207 @@ +// 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'; +import { config } from '../src/config'; + +const BIDDER_CODE = 'seedingAlliance'; +const DEFAULT_CUR = 'EUR'; +const ENDPOINT_URL = 'https://b.nativendo.de/cds/rtb/bid?format=openrtb2.5&ssp=nativendo'; + +const NATIVE_ASSET_IDS = {0: 'title', 1: 'body', 2: 'sponsoredBy', 3: 'image'}; + +const NATIVE_PARAMS = { + title: { + id: 0, + name: 'title' + }, + + body: { + id: 1, + name: 'data', + type: 2 + }, + + sponsoredBy: { + id: 2, + name: 'data', + type: 1 + }, + + image: { + id: 3, + type: 3, + name: 'img' + } +}; + +export const spec = { + code: BIDDER_CODE, + + supportedMediaTypes: [NATIVE], + + isBidRequestValid: function(bid) { + return !!bid.params.adUnitId; + }, + + buildRequests: (validBidRequests, bidderRequest) => { + const pt = setOnAny(validBidRequests, 'params.pt') || setOnAny(validBidRequests, 'params.priceType') || 'net'; + const tid = validBidRequests[0].transactionId; + const cur = [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR]; + let pubcid = null; + let url = bidderRequest.refererInfo.referer; + + const imp = validBidRequests.map((bid, id) => { + 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; + + let w, h; + + if (bidParams.sizes) { + w = bidParams.sizes[0]; + h = bidParams.sizes[1]; + } + + asset[props.name] = { + len: bidParams.len, + type: props.type, + w, + h + }; + + return asset; + } + }) + .filter(Boolean); + + if (bid.params.url) { + url = bid.params.url; + } + + return { + id: String(id + 1), + tagid: bid.params.adUnitId, + tid: tid, + pt: pt, + native: { + request: { + assets + } + } + }; + }); + + if (validBidRequests[0].crumbs && validBidRequests[0].crumbs.pubcid) { + pubcid = validBidRequests[0].crumbs.pubcid; + } + + const request = { + id: bidderRequest.auctionId, + site: { + page: url + }, + device: { + ua: navigator.userAgent + }, + cur, + imp, + user: { + buyeruid: pubcid + } + }; + + return { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(request), + bids: validBidRequests + }; + }, + + interpretResponse: function(serverResponse, { bids }) { + if (utils.isEmpty(serverResponse.body)) { + return []; + } + + const { seatbid, cur } = serverResponse.body; + + const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + result[bid.impid - 1] = bid; + return result; + }, []); + + return bids + .map((bid, id) => { + const bidResponse = bidResponses[id]; + + if (bidResponse) { + return { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 1000, + 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} = bid.adm.native; + + link.clicktrackers.forEach(function (clicktracker, index) { + link.clicktrackers[index] = clicktracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + }); + + imptrackers.forEach(function (imptracker, index) { + imptrackers[index] = imptracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + }); + + const result = { + url: link.url, + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined, + impressionTrackers: imptrackers || 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; + } + } +} + +function flatten(arr) { + return [].concat(...arr); +} diff --git a/modules/seedingAllianceBidAdapter.md b/modules/seedingAllianceBidAdapter.md new file mode 100755 index 00000000000..c933e53695b --- /dev/null +++ b/modules/seedingAllianceBidAdapter.md @@ -0,0 +1,45 @@ +# Overview +Module Name: Seeding Alliance Bidder Adapter +Type: Seeding Alliance Adapter +Maintainer: tech@seeding-alliance.de + +# Description +Seeding Alliance Bidder Adapter for Prebid.js. + +# Test Parameters +``` +var adUnits = [{ + code: 'test-div', + + mediaTypes: { + native: { + title: { + required: true, + len: 50 + }, + body: { + required: true, + len: 350 + }, + url: { + required: true + }, + image: { + required: true, + sizes : [300, 175] + }, + sponsoredBy: { + required: true + } + } + }, + bids: [{ + bidder: 'seedingAlliance', + params: { + url : "https://mockup.seeding-alliance.de/ssp-testing/native.html", + adUnitId: "2sq2o" + } + }] +}]; +``` + diff --git a/test/spec/modules/seedingAllianceAdapter_spec.js b/test/spec/modules/seedingAllianceAdapter_spec.js new file mode 100755 index 00000000000..e7915bc2d22 --- /dev/null +++ b/test/spec/modules/seedingAllianceAdapter_spec.js @@ -0,0 +1,179 @@ +// jshint esversion: 6, es3: false, node: true +import {assert, expect} from 'chai'; +import * as url from 'src/url'; +import {spec} from 'modules/seedingAllianceBidAdapter'; +import { NATIVE } from 'src/mediaTypes'; +import { config } from 'src/config'; + +describe('SeedingAlliance adapter', function () { + let serverResponse, bidRequest, bidResponses; + let bid = { + 'bidder': 'seedingAlliance', + 'params': { + 'adUnitId': '1hq8' + } + }; + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when AdUnitId is not set', function () { + delete bid.params.adUnitId; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests', function () { + it('should send request with correct structure', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: {} + }]; + + let request = spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }); + + assert.equal(request.method, 'POST'); + assert.ok(request.data); + }); + + it('should have default request structure', function () { + let keys = 'site,device,cur,imp,user'.split(','); + let validBidRequests = [{ + bidId: 'bidId', + params: {} + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + let data = Object.keys(request); + + assert.deepEqual(keys, data); + }); + + it('Verify the auction ID', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: {}, + auctionId: 'auctionId' + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' }, auctionId: validBidRequests[0].auctionId }).data); + + assert.equal(request.id, validBidRequests[0].auctionId); + }); + + it('Verify the device', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: {} + }]; + let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); + + assert.equal(request.device.ua, navigator.userAgent); + }); + + it('Verify native asset ids', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: {}, + nativeParams: { + body: { + required: true, + len: 350 + }, + image: { + required: true + }, + title: { + required: true + }, + sponsoredBy: { + required: true + } + } + }]; + + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data).imp[0].native.request.assets; + + assert.equal(assets[0].id, 1); + assert.equal(assets[1].id, 3); + assert.equal(assets[2].id, 0); + assert.equal(assets[3].id, 2); + }); + }); + + describe('interpretResponse', function () { + const goodResponse = { + body: { + cur: 'EUR', + id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', + seatbid: [ + { + seat: 'seedingAlliance', + bid: [{ + adm: { + native: { + assets: [ + {id: 0, title: {text: 'this is a title'}} + ], + imptrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], + link: { + clicktrackers: ['https://domain.for/imp/tracker?price=${AUCTION_PRICE}'], + url: 'https://domain.for/ad/' + } + } + }, + impid: 1, + price: 0.55 + }] + } + ] + } + }; + const badResponse = { body: { + cur: 'EUR', + id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', + seatbid: [] + }}; + + const bidRequest = { + data: {}, + bids: [{ bidId: 'bidId1' }] + }; + + it('should return null if body is missing or empty', function () { + const result = spec.interpretResponse(badResponse, bidRequest); + assert.equal(result.length, 0); + + delete badResponse.body + + const result1 = spec.interpretResponse(badResponse, bidRequest); + assert.equal(result.length, 0); + }); + + it('should return the correct params', function () { + const result = spec.interpretResponse(goodResponse, bidRequest); + const bid = goodResponse.body.seatbid[0].bid[0]; + + assert.deepEqual(result[0].currency, goodResponse.body.cur); + assert.deepEqual(result[0].requestId, bidRequest.bids[0].bidId); + assert.deepEqual(result[0].cpm, bid.price); + assert.deepEqual(result[0].creativeId, bid.crid); + assert.deepEqual(result[0].mediaType, 'native'); + assert.deepEqual(result[0].bidderCode, 'seedingAlliance'); + }); + + it('should return the correct tracking links', function () { + const result = spec.interpretResponse(goodResponse, bidRequest); + const bid = goodResponse.body.seatbid[0].bid[0]; + const regExpPrice = new RegExp('price=' + bid.price); + + result[0].native.clickTrackers.forEach(function (clickTracker) { + assert.ok(clickTracker.search(regExpPrice) > -1); + }); + + result[0].native.impressionTrackers.forEach(function (impTracker) { + assert.ok(impTracker.search(regExpPrice) > -1); + }); + }); + }); +});