diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js new file mode 100644 index 00000000000..ecb56c00d29 --- /dev/null +++ b/modules/codefuelBidAdapter.js @@ -0,0 +1,183 @@ +import { deepAccess, isArray } from '../src/utils.js'; +import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'codefuel'; +const CURRENCY = 'USD'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ BANNER ], + aliases: ['ex'], // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (bid.nativeParams) { + return false; + } + return !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const page = bidderRequest.refererInfo.referer; + const domain = getDomainFromURL(page) + const ua = navigator.userAgent; + const devicetype = getDeviceType() + const publisher = setOnAny(validBidRequests, 'params.publisher'); + const cur = CURRENCY; + // const endpointUrl = 'http://localhost:5000/prebid' + const endpointUrl = config.getConfig('codefuel.bidderUrl'); + const timeout = bidderRequest.timeout; + + validBidRequests.forEach(bid => bid.netRevenue = 'net'); + + const imps = validBidRequests.map((bid, idx) => { + const imp = { + id: idx + 1 + '' + } + + if (bid.params.tagid) { + imp.tagid = bid.params.tagid + } + + if (bid.sizes) { + imp.banner = { + format: transformSizes(bid.sizes) + } + } + + return imp; + }); + + const request = { + id: bidderRequest.auctionId, + site: { page, domain, publisher }, + device: { ua, devicetype }, + source: { fd: 1 }, + cur: [cur], + tmax: timeout, + imp: imps, + }; + + return { + method: 'POST', + url: endpointUrl, + data: request, + bids: validBidRequests, + options: { + withCredentials: false + } + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse, { bids }) => { + if (!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) { + const bidObject = { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 360, + netRevenue: true, + currency: cur, + mediaType: BANNER, + ad: bidResponse.adm, + width: bidResponse.w, + height: bidResponse.h, + meta: { advertiserDomains: bid.adomain ? bid.adomain : [] } + }; + return bidObject; + } + }).filter(Boolean); + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + return []; + } + +} +registerBidder(spec); + +function getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function getDeviceType() { + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; // 'tablet' + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; // 'mobile' + } + return 2; // 'desktop' +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +function flatten(arr) { + return [].concat(...arr); +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + if (!isArray(requestSizes)) { + return []; + } + + if (requestSizes.length === 2 && !isArray(requestSizes[0])) { + return [{ + w: parseInt(requestSizes[0], 10), + h: parseInt(requestSizes[1], 10) + }]; + } else if (isArray(requestSizes[0])) { + return requestSizes.map(item => + ({ + w: parseInt(item[0], 10), + h: parseInt(item[1], 10) + }) + ); + } + + return []; +} diff --git a/modules/codefuelBidAdapter.md b/modules/codefuelBidAdapter.md new file mode 100644 index 00000000000..321ae3b6644 --- /dev/null +++ b/modules/codefuelBidAdapter.md @@ -0,0 +1,111 @@ +# Overview + +``` +Module Name: Codefuel Adapter +Module Type: Bidder Adapter +Maintainer: hayimm@codefuel.com +``` + +# Description + +Module that connects to Codefuel bidder to fetch bids. +Display format is supported but not native format. Using OpenRTB standard. + +# Configuration + +## Bidder and usersync URLs + +The Codefuel adapter does not work without setting the correct bidder. +You will receive the URLs when contacting us. + +``` +pbjs.setConfig({ + codefuel: { + bidderUrl: 'https://ai-i-codefuel-ds-rtb-us-east-1-k8s-internal.seccint.com/prebid', + usersyncUrl: 'https://usersync-url.com' + } +}); +``` + + +# Test Native Parameters +``` + var adUnits = [ + code: '/19968336/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: false, + sizes: [100, 50] + }, + title: { + required: false, + len: 140 + }, + sponsoredBy: { + required: false + }, + clickUrl: { + required: false + }, + body: { + required: false + }, + icon: { + required: false, + sizes: [50, 50] + } + } + }, + bids: [{ + bidder: 'codefuel', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id', + bcat: ['IAB1-1'], + badv: ['example.com'] + } + }] + ]; + + pbjs.setConfig({ + codefuel: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` + +# Test Display Parameters +``` + var adUnits = [ + code: '/19968336/prebid_display_example_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'codefuel', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id', + bcat: ['IAB1-1'], + badv: ['example.com'] + }, + }] + ]; + + pbjs.setConfig({ + codefuel: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/test/spec/modules/codefuelBidAdapter_spec.js b/test/spec/modules/codefuelBidAdapter_spec.js new file mode 100644 index 00000000000..808c221af07 --- /dev/null +++ b/test/spec/modules/codefuelBidAdapter_spec.js @@ -0,0 +1,316 @@ +import {expect} from 'chai'; +import {spec} from 'modules/codefuelBidAdapter.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr'; + +const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/92.0.4515.159 Safari/537.36'; +const DEFAULT_USER_AGENT = window.navigator.userAgent; +const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMock = () => { window.navigator.__defineGetter__('userAgent', function () { return USER_AGENT }) }; + +describe('Codefuel Adapter', function () { + describe('Bid request and response', function () { + const commonBidRequest = { + bidder: 'codefuel', + params: { + publisher: { + id: 'publisher-id' + }, + }, + bidId: '2d6815a92ba1ba', + auctionId: '12043683-3254-4f74-8934-f941b085579e', + } + const nativeBidRequestParams = { + nativeParams: { + image: { + required: true, + sizes: [ + 120, + 100 + ], + sendId: true + }, + title: { + required: true, + sendId: true + }, + sponsoredBy: { + required: false + } + }, + } + + const displayBidRequestParams = { + sizes: [ + [ + 300, + 250 + ] + ] + } + + describe('isBidRequestValid', function () { + before(() => { + config.setConfig({ + codefuel: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should fail when bid is invalid', function () { + const bid = { + bidder: 'codefuel', + params: { + publisher: { + id: 'publisher-id', + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should not succeed when bid contains native params', function () { + const bid = { + bidder: 'codefuel', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should not succeed when bid contains only sizes', function () { + const bid = { + bidder: 'codefuel', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if publisher id is not set', function () { + const bid = { + bidder: 'codefuel', + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + + it('should fail if bidder url is not set', function () { + const bid = { + bidder: 'codefuel', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + before(() => { + setUAMock() + config.setConfig({ + codefuel: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + setUADefault() + }) + + const commonBidderRequest = { + timeout: 500, + auctionId: '12043683-3254-4f74-8934-f941b085579e', + refererInfo: { + referer: 'https://example.com/', + } + } + + it('should build display request', function () { + const bidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + const expectedData = { + cur: [ + 'USD' + ], + device: { + devicetype: 2, + ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/92.0.4515.159 Safari/537.36' + }, + id: '12043683-3254-4f74-8934-f941b085579e', + imp: [ + { + banner: { + format: [ + { + h: 250, + w: 300 + } + ] + }, + id: '1' + } + ], + site: { + domain: 'example.com', + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + source: { + fd: 1 + }, + tmax: 500 + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(expectedData) + }) + + it('should pass bidder timeout', function () { + const bidRequest = { + ...commonBidRequest, + } + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = res.data + expect(resData.tmax).to.equal(500) + }); + }) + + describe('interpretResponse', function () { + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse({}, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should interpret display response', function () { + const serverResponse = { + body: { + id: '6b2eedc8-8ff5-46ef-adcf-e701b508943e', + seatbid: [ + { + bid: [ + { + id: 'd90fe7fa-28d7-11eb-8ce4-462a842a7cf9', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '