diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js new file mode 100644 index 00000000000..33df5bd252e --- /dev/null +++ b/modules/deltaprojectsBidAdapter.js @@ -0,0 +1,252 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { + _each, _map, isFn, isNumber, createTrackPixelHtml, deepAccess, parseUrl, logWarn, logError +} from '../src/utils.js'; +import {config} from '../src/config.js'; + +export const BIDDER_CODE = 'deltaprojects'; +export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; +export const USERSYNC_URL = 'https://userservice.de17a.com/getuid/prebid'; + +/** -- isBidRequestValid --**/ +function isBidRequestValid(bid) { + if (!bid) return false; + + if (bid.bidder !== BIDDER_CODE) return false; + + // publisher id is required + const publisherId = deepAccess(bid, 'params.publisherId') + if (!publisherId) { + logError('Invalid bid request, missing publisher id in params'); + return false; + } + + return true; +} + +/** -- Build requests --**/ +function buildRequests(validBidRequests, bidderRequest) { + /** == shared ==**/ + // -- build id + const id = bidderRequest.auctionId; + + // -- build site + const loc = parseUrl(bidderRequest.refererInfo.referer); + const publisherId = setOnAny(validBidRequests, 'params.publisherId'); + const siteId = setOnAny(validBidRequests, 'params.siteId'); + const site = { + id: siteId, + domain: loc.hostname, + page: loc.href, + ref: loc.href, + publisher: { id: publisherId }, + }; + + // -- build device + const ua = navigator.userAgent; + const device = { + ua, + w: screen.width, + h: screen.height + } + + // -- build user, reg + let user = { ext: {} }; + const regs = { ext: {} }; + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + if (gdprConsent) { + user.ext = { consent: gdprConsent.consentString }; + if (typeof gdprConsent.gdprApplies == 'boolean') { + regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0 + } + } + + // -- build tmax + let tmax = (bidderRequest && bidderRequest.timeout > 0) ? bidderRequest.timeout : undefined; + + // build bid specific + return validBidRequests.map(validBidRequest => { + const openRTBRequest = buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs); + return { + method: 'POST', + url: BIDDER_ENDPOINT_URL, + data: openRTBRequest, + options: { contentType: 'application/json' }, + bids: [validBidRequest], + }; + }); +} + +function buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs) { + // build cur + const currency = config.getConfig('currency.adServerCurrency') || deepAccess(validBidRequest, 'params.currency'); + const cur = currency && [currency]; + + // build impression + const impression = buildImpression(validBidRequest, currency); + + // build test + const test = deepAccess(validBidRequest, 'params.test') ? 1 : 0 + + const at = 1 + + // build source + const source = { + tid: validBidRequest.transactionId, + fd: 1, + } + + return { + id, + at, + imp: [impression], + site, + device, + user, + test, + tmax, + cur, + source, + regs, + ext: {}, + }; +} + +function buildImpression(bid, currency) { + const impression = { + id: bid.bidId, + tagid: bid.params.tagId, + ext: {}, + }; + + const bannerMediaType = deepAccess(bid, `mediaTypes.${BANNER}`); + impression.banner = buildImpressionBanner(bid, bannerMediaType); + + // bid floor + const bidFloor = getBidFloor(bid, BANNER, '*', currency); + if (bidFloor) { + impression.bidfloor = bidFloor.floor; + impression.bidfloorcur = bidFloor.currency; + } + + return impression; +} + +function buildImpressionBanner(bid, bannerMediaType) { + const bannerSizes = (bannerMediaType && bannerMediaType.sizes) || bid.sizes; + return { + format: _map(bannerSizes, ([width, height]) => ({ w: width, h: height })), + }; +} + +/** -- Interpret response --**/ +function interpretResponse(serverResponse) { + if (!serverResponse.body) { + logWarn('Response body is invalid, return !!'); + return []; + } + + const { body: { id, seatbid, cur } } = serverResponse; + if (!id || !seatbid) { + logWarn('Id / seatbid of response is invalid, return !!'); + return []; + } + + const bidResponses = []; + + _each(seatbid, seatbid => { + _each(seatbid.bid, bid => { + const bidObj = { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + dealId: bid.dealid || null, + currency: cur, + netRevenue: true, + ttl: 60, + }; + + bidObj.mediaType = BANNER; + bidObj.ad = bid.adm; + if (bid.nurl) { + bidObj.ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } + if (bid.ext) { + bidObj[BIDDER_CODE] = bid.ext; + } + bidResponses.push(bidObj); + }); + }); + return bidResponses; +} + +/** -- On Bid Won -- **/ +function onBidWon(bid) { + let cpm = bid.cpm; + if (bid.currency && bid.currency !== bid.originalCurrency && typeof bid.getCpmInNewCurrency === 'function') { + cpm = bid.getCpmInNewCurrency(bid.originalCurrency); + } + const wonPrice = Math.round(cpm * 1000000); + const wonPriceMacroPatten = /\$\{AUCTION_PRICE:B64\}/g; + bid.ad = bid.ad.replace(wonPriceMacroPatten, wonPrice); +} + +/** -- Get user syncs --**/ +function getUserSyncs(syncOptions, serverResponses, gdprConsent) { + const syncs = [] + + if (syncOptions.pixelEnabled) { + let gdprParams; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } else { + gdprParams = ''; + } + syncs.push({ + type: 'image', + url: USERSYNC_URL + gdprParams + }); + } + return syncs; +} + +/** -- Get bid floor --**/ +export function getBidFloor(bid, mediaType, size, currency) { + if (isFn(bid.getFloor)) { + const bidFloorCurrency = currency || 'USD'; + const bidFloor = bid.getFloor({currency: bidFloorCurrency, mediaType: mediaType, size: size}); + if (isNumber(bidFloor.floor)) { + return bidFloor; + } + } +} + +/** -- Helper methods --**/ +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +/** -- Register -- */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/deltaprojectsBidAdapter.md b/modules/deltaprojectsBidAdapter.md new file mode 100644 index 00000000000..97cef4dd228 --- /dev/null +++ b/modules/deltaprojectsBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: Delta Projects Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@deltaprojects.com +``` + +# Description + +Connects to Delta Projects DSP for bids. + +# Test Parameters +``` +// define banner unit +var bannerUnit = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]], + } + }, + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'deltaprojects', + params: { + publisherId: '4' //required + } + }] +}; +``` + diff --git a/test/spec/modules/deltaprojectsBidAdapter_spec.js b/test/spec/modules/deltaprojectsBidAdapter_spec.js new file mode 100644 index 00000000000..382415eab62 --- /dev/null +++ b/test/spec/modules/deltaprojectsBidAdapter_spec.js @@ -0,0 +1,399 @@ +import { expect } from 'chai'; +import { + BIDDER_CODE, + BIDDER_ENDPOINT_URL, + spec, USERSYNC_URL, + getBidFloor +} from 'modules/deltaprojectsBidAdapter.js'; + +const BID_REQ_REFER = 'http://example.com/page?param=val'; + +describe('deltaprojectsBidAdapter', function() { + describe('isBidRequestValid', function () { + function makeBid() { + return { + bidder: BIDDER_CODE, + params: { + publisherId: '12345' + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + } + + it('should return true when bidder set correctly', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when bid request is null', function () { + expect(spec.isBidRequestValid(undefined)).to.equal(false); + }); + + it('should return false when bidder not set correctly', function () { + let bid = makeBid(); + delete bid.bidder; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisher id is not set', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const BIDREQ = { + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com', + }, + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + } + const bidRequests = [BIDREQ]; + const bannerRequest = spec.buildRequests(bidRequests, {refererInfo: { referer: BID_REQ_REFER }})[0]; + const bannerRequestBody = bannerRequest.data; + + it('send bid request with test tag if it is set in the param', function () { + const TEST_TAG = 1; + const bidRequest = Object.assign({}, BIDREQ, { + params: { ...BIDREQ.params, test: TEST_TAG }, + }); + const bidderRequest = { refererInfo: { referer: BID_REQ_REFER } }; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(request.data.test).to.equal(TEST_TAG); + }); + + it('send bid request with correct timeout', function () { + const TMAX = 10; + const bidderRequest = { refererInfo: { referer: BID_REQ_REFER }, timeout: TMAX }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.tmax).to.equal(TMAX); + }); + + it('send bid request to the correct endpoint URL', function () { + expect(bannerRequest.url).to.equal(BIDDER_ENDPOINT_URL); + }); + + it('sends bid request to our endpoint via POST', function () { + expect(bannerRequest.method).to.equal('POST'); + }); + + it('sends screen dimensions', function () { + expect(bannerRequestBody.device.w).to.equal(screen.width); + expect(bannerRequestBody.device.h).to.equal(screen.height); + }); + + it('includes the ad size in the bid request', function () { + expect(bannerRequestBody.imp[0].banner.format[0].w).to.equal(BIDREQ.sizes[0][0]); + expect(bannerRequestBody.imp[0].banner.format[0].h).to.equal(BIDREQ.sizes[0][1]); + }); + + it('sets domain and href correctly', function () { + expect(bannerRequestBody.site.domain).to.equal(BIDREQ.params.siteId); + expect(bannerRequestBody.site.page).to.equal(BID_REQ_REFER); + }); + + const gdprBidRequests = [{ + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com' + }, + sizes: [ + [300, 250] + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }]; + const consentString = 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='; + + const GDPR_REQ_REFERER = 'http://localhost:9876/' + function getGdprRequestBody(gdprApplies, consentString) { + const gdprRequest = spec.buildRequests(gdprBidRequests, { + gdprConsent: { + gdprApplies: gdprApplies, + consentString: consentString, + }, + refererInfo: { + referer: GDPR_REQ_REFERER, + }, + })[0]; + return gdprRequest.data; + } + + it('should handle gdpr applies being present and true', function() { + const gdprRequestBody = getGdprRequestBody(true, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(1); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr applies being present and false', function() { + const gdprRequestBody = getGdprRequestBody(false, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(0); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr applies being undefined', function() { + const gdprRequestBody = getGdprRequestBody(undefined, consentString); + expect(gdprRequestBody.regs).to.deep.equal({ext: {}}); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr consent being undefined', function() { + const gdprRequest = spec.buildRequests(gdprBidRequests, {refererInfo: { referer: GDPR_REQ_REFERER }})[0]; + const gdprRequestBody = gdprRequest.data; + expect(gdprRequestBody.regs).to.deep.equal({ ext: {} }); + expect(gdprRequestBody.user).to.deep.equal({ ext: {} }); + }) + }); + + describe('interpretResponse', function () { + const bidRequests = [ + { + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com', + currency: 'USD', + }, + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + ]; + const request = spec.buildRequests(bidRequests, {refererInfo: { referer: BID_REQ_REFER }})[0]; + function makeResponse() { + return { + body: { + id: '5e5c23a5ba71e78', + seatbid: [ + { + bid: [ + { + id: '6vmb3isptf', + crid: 'deltaprojectscreative', + impid: '322add653672f68', + price: 1.22, + adm: '', + attr: [5], + h: 90, + nurl: 'http://nurl', + w: 728, + } + ], + seat: 'MOCK' + } + ], + bidid: '5e5c23a5ba71e78', + cur: 'USD' + } + }; + } + const expectedBid = { + requestId: '322add653672f68', + cpm: 1.22, + width: 728, + height: 90, + creativeId: 'deltaprojectscreative', + dealId: null, + currency: 'USD', + netRevenue: true, + mediaType: 'banner', + ttl: 60, + ad: '
' + }; + + it('should get incorrect bid response if response body is missing', function () { + let response = makeResponse(); + delete response.body; + let result = spec.interpretResponse(response, request); + expect(result.length).to.equal(0); + }); + + it('should get incorrect bid response if id or seat id of response body is missing', function () { + let response1 = makeResponse(); + delete response1.body.id; + let result1 = spec.interpretResponse(response1, request); + expect(result1.length).to.equal(0); + + let response2 = makeResponse(); + delete response2.body.seatbid; + let result2 = spec.interpretResponse(response2, request); + expect(result2.length).to.equal(0); + }); + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(makeResponse(), request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should handle a missing crid', function () { + let noCridResponse = makeResponse(); + delete noCridResponse.body.seatbid[0].bid[0].crid; + const fallbackCrid = noCridResponse.body.seatbid[0].bid[0].id; + let noCridResult = Object.assign({}, expectedBid, {'creativeId': fallbackCrid}); + let result = spec.interpretResponse(noCridResponse, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noCridResult); + }); + + it('should handle a missing nurl', function () { + let noNurlResponse = makeResponse(); + delete noNurlResponse.body.seatbid[0].bid[0].nurl; + let noNurlResult = Object.assign({}, expectedBid); + noNurlResult.ad = ''; + let result = spec.interpretResponse(noNurlResponse, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noNurlResult); + }); + + it('handles empty bid response', function () { + let response = { + body: { + id: '5e5c23a5ba71e78', + seatbid: [] + } + }; + let result = spec.interpretResponse(response, request); + expect(result.length).to.equal(0); + }); + + it('should keep custom properties', () => { + const customProperties = {test: 'a test message', param: {testParam: 1}}; + const expectedResult = Object.assign({}, expectedBid, {[spec.code]: customProperties}); + const response = makeResponse(); + response.body.seatbid[0].bid[0].ext = customProperties; + const result = spec.interpretResponse(response, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedResult); + }); + }); + + describe('onBidWon', function () { + const OPEN_RTB_RESP = { + body: { + id: 'abc', + seatbid: [ + { + bid: [ + { + 'id': 'abc*123*456', + 'impid': 'xxxxxxx', + 'price': 46.657196, + 'adm': '