From 7d5f8702c2eba9607c51a19951e0e76a99449f65 Mon Sep 17 00:00:00 2001 From: Steffen Anders Date: Wed, 14 Dec 2022 13:24:11 +0100 Subject: [PATCH] AdUp Technology bid adapter: optimize floor price detection (#9332) --- modules/aduptechBidAdapter.js | 90 ++++++++----- test/spec/modules/aduptechBidAdapter_spec.js | 131 ++++++++++++++----- 2 files changed, 159 insertions(+), 62 deletions(-) diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 5d8b69af77f..c39d9e14f17 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getAdUnitSizes} from '../src/utils.js'; +import {getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -23,7 +23,7 @@ export const internal = { if (bidderRequest && bidderRequest.gdprConsent) { return { consentString: bidderRequest.gdprConsent.consentString, - consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true + consentRequired: (isBoolean(bidderRequest.gdprConsent.gdprApplies)) ? bidderRequest.gdprConsent.gdprApplies : true }; } @@ -60,8 +60,26 @@ export const internal = { */ extractBannerConfig: (bidRequest) => { const sizes = getAdUnitSizes(bidRequest); - if (Array.isArray(sizes) && sizes.length > 0) { - return { sizes: sizes }; + if (isArray(sizes) && !isEmpty(sizes)) { + const banner = { sizes: sizes }; + + // try to add floor for each banner size + banner.sizes.forEach(size => { + const floor = internal.getFloor(bidRequest, { mediaType: BANNER, size }); + if (floor) { + size.push(floor.floor); + size.push(floor.currency); + } + }); + + // try to add default floor for banner + const floor = internal.getFloor(bidRequest, { mediaType: BANNER, size: '*' }); + if (floor) { + banner.floorPrice = floor.floor; + banner.floorCurrency = floor.currency; + } + + return banner; } return null; @@ -74,8 +92,17 @@ export const internal = { * @returns {null|Object.} */ extractNativeConfig: (bidRequest) => { - if (bidRequest && deepAccess(bidRequest, 'mediaTypes.native')) { - return bidRequest.mediaTypes.native; + if (bidRequest?.mediaTypes?.native) { + const native = bidRequest.mediaTypes.native; + + // try to add default floor for native + const floor = internal.getFloor(bidRequest, { mediaType: NATIVE, size: '*' }); + if (floor) { + native.floorPrice = floor.floor; + native.floorCurrency = floor.currency; + } + + return native; } return null; @@ -96,27 +123,25 @@ export const internal = { }, /** - * Extracts the floor price params from given bidRequest + * Try to get floor information via bidRequest.getFloor() * * @param {BidRequest} bidRequest - * @returns {undefined|float} + * @param {Object} options + * @returns {null|Object.} */ - extractFloorPrice: (bidRequest) => { - let floorPrice; - if (bidRequest && bidRequest.params && bidRequest.params.floor) { - // if there is a manual floorPrice set - floorPrice = !isNaN(parseInt(bidRequest.params.floor)) ? bidRequest.params.floor : undefined; - } - if (typeof bidRequest.getFloor === 'function') { - // use prebid floor module - let floorInfo; - try { - floorInfo = bidRequest.getFloor(); - } catch (e) {} - floorPrice = typeof floorInfo === 'object' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : floorPrice; + getFloor: (bidRequest, options) => { + if (!isFn(bidRequest?.getFloor)) { + return null; } - return floorPrice; + try { + const floor = bidRequest.getFloor(options); + if (isPlainObject(floor) && !isNaN(floor.floor)) { + return floor; + } + } catch {} + + return null; }, /** @@ -128,11 +153,11 @@ export const internal = { groupBidRequestsByPublisher: (bidRequests) => { const groupedBidRequests = {}; - if (!bidRequests || bidRequests.length === 0) { + if (!bidRequests || isEmpty(bidRequests)) { return groupedBidRequests; } - bidRequests.forEach((bidRequest) => { + bidRequests.forEach(bidRequest => { const publisher = internal.extractParams(bidRequest).publisher; if (!publisher) { return; @@ -205,7 +230,7 @@ export const spec = { const requests = []; // stop here on invalid or empty data - if (!bidderRequest || !validBidRequests || validBidRequests.length === 0) { + if (!bidderRequest || !validBidRequests || isEmpty(validBidRequests)) { return requests; } @@ -237,7 +262,7 @@ export const spec = { } // handle multiple bids per request - groupedBidRequests[publisher].forEach((bidRequest) => { + groupedBidRequests[publisher].forEach(bidRequest => { const bid = { bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, @@ -257,10 +282,11 @@ export const spec = { bid.native = nativeConfig; } - // add floor price - const floorPrice = internal.extractFloorPrice(bidRequest); - if (floorPrice) { - bid.floorPrice = floorPrice; + // try to add default floor + const floor = internal.getFloor(bidRequest, { mediaType: '*', size: '*' }); + if (floor) { + bid.floorPrice = floor.floor; + bid.floorCurrency = floor.currency; } request.data.imp.push(bid); @@ -282,12 +308,12 @@ export const spec = { const bidResponses = []; // stop here on invalid or empty data - if (!response || !deepAccess(response, 'body.bids') || response.body.bids.length === 0) { + if (!response?.body?.bids || isEmpty(response.body.bids)) { return bidResponses; } // parse multiple bids per response - response.body.bids.forEach((bid) => { + response.body.bids.forEach(bid => { if (!bid || !bid.bid || !bid.creative) { return; } diff --git a/test/spec/modules/aduptechBidAdapter_spec.js b/test/spec/modules/aduptechBidAdapter_spec.js index bbc4e554f7e..8dbdbbfeab5 100644 --- a/test/spec/modules/aduptechBidAdapter_spec.js +++ b/test/spec/modules/aduptechBidAdapter_spec.js @@ -182,6 +182,54 @@ describe('AduptechBidAdapter', () => { }); }); + describe('getFloor', () => { + let bidRequest; + + beforeEach(() => { + bidRequest = { + getFloor: sinon.stub() + }; + }); + + it('should handle empty or invalid bidRequest', () => { + expect(internal.getFloor(null)).to.be.null; + expect(internal.getFloor({})).to.be.null; + expect(internal.getFloor({ getFloor: 'foo' })).to.be.null; + }); + + it('should detect floor via getFloor()', () => { + const result = { + floor: 1.11, + currency: 'USD' + }; + + const options = { + mediaType: BANNER, + size: '*' + } + + bidRequest.getFloor.returns(result); + + expect(internal.getFloor(bidRequest, options)).to.deep.equal(result); + expect(bidRequest.getFloor.calledOnceWith(options)).to.be.true; + }); + + it('should handle empty, invalid or faulty getFloor() results', () => { + bidRequest.getFloor + .onCall(0).returns({}) + .onCall(1).returns({ floor: 'foo' }) + .onCall(2).returns('bar') + .onCall(3).throws(new Error('baz')); + + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + + expect(bidRequest.getFloor.callCount).to.equal(4); + }); + }); + describe('groupBidRequestsByPublisher', () => { it('should handle empty bidRequests', () => { expect(internal.groupBidRequestsByPublisher(null)).to.deep.equal({}); @@ -541,34 +589,35 @@ describe('AduptechBidAdapter', () => { } }; - const getFloorResponse = { - currency: 'USD', - floor: '1.23' - }; - - const validBidRequests = [ - { - bidId: 'bidId1', - adUnitCode: 'adUnitCode1', - transactionId: 'transactionId1', - mediaTypes: { - banner: { - sizes: [[100, 200], [300, 400]] - } - }, - params: { - publisher: 'publisher1', - placement: 'placement1' + const bidRequest = { + bidId: 'bidId1', + adUnitCode: 'adUnitCode1', + transactionId: 'transactionId1', + mediaTypes: { + banner: { + sizes: [[100, 200], [300, 400]] }, - getFloor: () => { - return getFloorResponse + native: { + image: { + required: true + }, } - } - ]; + }, + params: { + publisher: 'publisher1', + placement: 'placement1' + }, + getFloor: sinon.stub() + .onCall(0).returns({ floor: 1.11, currency: 'USD' }) + .onCall(1).returns({ floor: 2.22, currency: 'EUR' }) + .onCall(2).returns({ floor: 3.33, currency: 'USD' }) + .onCall(3).returns({ floor: 4.44, currency: 'GBP' }) + .onCall(4).returns({ floor: 5.55, currency: 'EUR' }) + }; - expect(spec.buildRequests(validBidRequests, bidderRequest)).to.deep.equal([ + expect(spec.buildRequests([bidRequest], bidderRequest)).to.deep.equal([ { - url: internal.buildEndpointUrl(validBidRequests[0].params.publisher), + url: internal.buildEndpointUrl(bidRequest.params.publisher), method: ENDPOINT_METHOD, data: { auctionId: bidderRequest.auctionId, @@ -580,17 +629,39 @@ describe('AduptechBidAdapter', () => { }, imp: [ { - bidId: validBidRequests[0].bidId, - transactionId: validBidRequests[0].transactionId, - adUnitCode: validBidRequests[0].adUnitCode, - params: validBidRequests[0].params, - banner: validBidRequests[0].mediaTypes.banner, - floorPrice: getFloorResponse.floor + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + adUnitCode: bidRequest.adUnitCode, + params: bidRequest.params, + banner: { + sizes: [ + [100, 200, 1.11, 'USD'], + [300, 400, 2.22, 'EUR'], + ], + floorPrice: 3.33, + floorCurrency: 'USD' + }, + native: { + image: { + required: true + }, + floorPrice: 4.44, + floorCurrency: 'GBP' + }, + floorPrice: 5.55, + floorCurrency: 'EUR' } ] } } ]); + + expect(bidRequest.getFloor.callCount).to.equal(5); + expect(bidRequest.getFloor.getCall(0).calledWith({ mediaType: BANNER, size: bidRequest.mediaTypes.banner.sizes[0] })).to.be.true; + expect(bidRequest.getFloor.getCall(1).calledWith({ mediaType: BANNER, size: bidRequest.mediaTypes.banner.sizes[1] })).to.be.true; + expect(bidRequest.getFloor.getCall(2).calledWith({ mediaType: BANNER, size: '*' })).to.be.true; + expect(bidRequest.getFloor.getCall(3).calledWith({ mediaType: NATIVE, size: '*' })).to.be.true; + expect(bidRequest.getFloor.getCall(4).calledWith({ mediaType: '*', size: '*' })).to.be.true; }); });