From d6418a04f9913ee732114f4926f490094f765c9f Mon Sep 17 00:00:00 2001 From: Piotr Jaworski <109736938+piotrj-rtbh@users.noreply.github.com> Date: Mon, 21 Nov 2022 21:10:19 +0100 Subject: [PATCH] RTB House Bid Adapter: Process FLEDGE request/response (#9215) * RTBHouse Bid Adapter: add global vendor list id * structured user agent - browsers.brands * fix lint errors * Added sda into rtbhouse adapter * spreading ortb2: user & site props * examples reverted * init version * using mergedeep * removed wrong imp array augm.; slot imp augm. with addtl check * [SUA] merging ortb2.device into request * fledge auctionConfig adapted to our bid response structure * new bidder response structure for fledge * make sure bidderRequest has proper flag turned on * fledge endpoint hardcoded; code cleanups * remove obsolete function * obsolete function removed * [RTB House] Process FLEDGE request/response (#4) * [SDA & SUA] refactor using mergedeep * [FLEDGE] fledge auctionConfig adapted to our bid response structure * [FLEDGE] new bidder response structure for fledge * [FLEDGE] make sure bidderRequest has proper flag turned on * [FLEDGE] fledge endpoint hardcoded; code cleanups * [FLEDGE] remove obsolete functions * fixed lint errors * fledge test suites; adapter: delete imp.ext.ae when no fledge (#5) Co-authored-by: Leandro Otani Co-authored-by: rtbh-lotani <83652735+rtbh-lotani@users.noreply.github.com> Co-authored-by: Tomasz Swirski --- modules/rtbhouseBidAdapter.js | 119 ++++++++++++++----- test/spec/modules/rtbhouseBidAdapter_spec.js | 79 ++++++++++++ 2 files changed, 169 insertions(+), 29 deletions(-) diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index fdf64483da7..9f498014a8e 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,13 +1,15 @@ -import {deepAccess, isArray, logError} from '../src/utils.js'; +import {deepAccess, mergeDeep, isArray, logError, logInfo} from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {includes} from '../src/polyfill.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; const ENDPOINT_URL = 'creativecdn.com/bidder/prebid/bids'; +const FLEDGE_ENDPOINT_URL = 'creativecdn.com/bidder/prebidfledge/bids'; const DEFAULT_CURRENCY_ARR = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; @@ -50,12 +52,13 @@ export const spec = { const request = { id: validBidRequests[0].auctionId, - imp: validBidRequests.map(slot => mapImpression(slot)), + imp: validBidRequests.map(slot => mapImpression(slot, bidderRequest)), site: mapSite(validBidRequests, bidderRequest), cur: DEFAULT_CURRENCY_ARR, test: validBidRequests[0].params.test || 0, source: mapSource(validBidRequests[0]), }; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { const consentStr = (bidderRequest.gdprConsent.consentString) ? bidderRequest.gdprConsent.consentString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ''; @@ -81,37 +84,32 @@ export const spec = { } } - const ortb2Params = bidderRequest && bidderRequest.ortb2; - if (ortb2Params?.user) { - request.user = { - ...request.user, - ...(ortb2Params.user.data && { - data: { ...request.user?.data, ...ortb2Params.user.data }, - }), - ...(ortb2Params.user.ext && { - ext: { ...request.user?.ext, ...ortb2Params.user.ext }, - }), - }; + const ortb2Params = bidderRequest?.ortb2 || {}; + if (ortb2Params.site) { + mergeDeep(request, { site: ortb2Params.site }); + } + if (ortb2Params.user) { + mergeDeep(request, { user: ortb2Params.user }); + } + if (ortb2Params.device) { + mergeDeep(request, { device: ortb2Params.device }); } - if (ortb2Params?.site) { - request.site = { - ...request.site, - ...(ortb2Params.site.content && { - content: { ...request.site?.content, ...ortb2Params.site.content }, - }), - ...(ortb2Params.site.ext && { - ext: { ...request.site?.ext, ...ortb2Params.site.ext }, - }), - }; + + let computedEndpointUrl = ENDPOINT_URL; + + const fledgeConfig = config.getConfig('fledgeConfig'); + if (bidderRequest.fledgeEnabled && fledgeConfig) { + mergeDeep(request, { ext: { fledge_config: fledgeConfig } }); + computedEndpointUrl = FLEDGE_ENDPOINT_URL; } return { method: 'POST', - url: 'https://' + validBidRequests[0].params.region + '.' + ENDPOINT_URL, + url: 'https://' + validBidRequests[0].params.region + '.' + computedEndpointUrl, data: JSON.stringify(request) }; }, - interpretResponse: function (serverResponse, originalRequest) { + interpretOrtbResponse: function (serverResponse, originalRequest) { const responseBody = serverResponse.body; if (!isArray(responseBody)) { return []; @@ -119,17 +117,72 @@ export const spec = { const bids = []; responseBody.forEach(serverBid => { - if (serverBid.price === 0) { + if (!serverBid.price) { // price may exist and is === 0 or there's no price prop at all (fledge req case) return; } + + let interpretedBid; + // try...catch would be risky cause JSON.parse throws SyntaxError if (serverBid.adm.indexOf('{') === 0) { - bids.push(interpretNativeBid(serverBid)); + interpretedBid = interpretNativeBid(serverBid); } else { - bids.push(interpretBannerBid(serverBid)); + interpretedBid = interpretBannerBid(serverBid); } + if (serverBid.ext) interpretedBid.ext = serverBid.ext; + + bids.push(interpretedBid); }); return bids; + }, + interpretResponse: function (serverResponse, originalRequest) { + let bids; + + const responseBody = serverResponse.body; + let fledgeAuctionConfigs = null; + + if (responseBody.bidid && isArray(responseBody?.ext?.igbid)) { + // we have fledge response + // mimic the original response ([{},...]) + bids = this.interpretOrtbResponse({ body: responseBody.seatbid[0]?.bid }, originalRequest); + + const seller = responseBody.ext.seller; + const decisionLogicUrl = responseBody.ext.decisionLogicUrl; + const sellerTimeout = 'sellerTimeout' in responseBody.ext ? { sellerTimeout: responseBody.ext.sellerTimeout } : {}; + responseBody.ext.igbid.forEach((igbid) => { + const perBuyerSignals = {}; + igbid.igbuyer.forEach(buyerItem => { + perBuyerSignals[buyerItem.igdomain] = buyerItem.buyersignal + }); + fledgeAuctionConfigs = fledgeAuctionConfigs || {}; + fledgeAuctionConfigs[igbid.impid] = mergeDeep( + { + seller, + decisionLogicUrl, + interestGroupBuyers: Object.keys(perBuyerSignals), + perBuyerSignals, + }, + sellerTimeout + ); + }); + } else { + bids = this.interpretOrtbResponse(serverResponse, originalRequest); + } + + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return Object.assign({ + bidId, + auctionSignals: {} + }, cfg); + }); + logInfo('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); + return { + bids, + fledgeAuctionConfigs, + } + } + return bids; } }; registerBidder(spec); @@ -154,7 +207,7 @@ function applyFloor(slot) { * @param {object} slot Ad Unit Params by Prebid * @returns {object} Imp by OpenRTB 2.5 ยง3.2.4 */ -function mapImpression(slot) { +function mapImpression(slot, bidderRequest) { const imp = { id: slot.bidId, banner: mapBanner(slot), @@ -167,6 +220,14 @@ function mapImpression(slot) { imp.bidfloor = bidfloor; } + if (bidderRequest.fledgeEnabled) { + imp.ext = imp.ext || {}; + imp.ext.ae = slot?.ortb2Imp?.ext?.ae + } else { + if (imp.ext?.ae) { + delete imp.ext.ae; + } + } return imp; } diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index f4bcb48474a..6d41df7605b 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { OPENRTB, spec } from 'modules/rtbhouseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; describe('RTBHouseAdapter', () => { const adapter = newBidder(spec); @@ -97,6 +98,10 @@ describe('RTBHouseAdapter', () => { ]; }); + afterEach(function () { + config.resetConfig(); + }); + it('should build test param into the request', () => { let builtTestRequest = spec.buildRequests(bidRequests, bidderRequest).data; expect(JSON.parse(builtTestRequest).test).to.equal(1); @@ -263,6 +268,45 @@ describe('RTBHouseAdapter', () => { expect(data.source).to.not.have.property('ext'); }); + context('FLEDGE', function() { + afterEach(function () { + config.resetConfig(); + }); + + it('sends bid request to FLEDGE ENDPOINT via POST', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + config.setConfig({ fledgeConfig: true }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + expect(request.url).to.equal('https://prebid-eu.creativecdn.com/bidder/prebidfledge/bids'); + expect(request.method).to.equal('POST'); + }); + + it('when FLEDGE is disabled, should not send imp.ext.ae', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + bidRequest[0].ortb2Imp = { + ext: { ae: 2 } + }; + const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: false }); + let data = JSON.parse(request.data); + if (data.imp[0].ext) { + expect(data.imp[0].ext).to.not.have.property('ae'); + } + }); + + it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + bidRequest[0].ortb2Imp = { + ext: { ae: 2 } + }; + const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + let data = JSON.parse(request.data); + expect(data.imp[0].ext.ae).to.equal(2); + }); + }); + describe('native imp', () => { function basicRequest(extension) { return Object.assign({ @@ -460,6 +504,29 @@ describe('RTBHouseAdapter', () => { 'h': 250 }]; + let fledgeResponse = { + 'id': 'bid-identifier', + 'ext': { + 'igbid': [{ + 'impid': 'test-bid-id', + 'igbuyer': [{ + 'igdomain': 'https://buyer-domain.com', + 'buyersignal': {} + }] + }], + 'sellerTimeout': 500, + 'seller': 'https://seller-domain.com', + 'decisionLogicUrl': 'https://seller-domain.com/decision-logic.js' + }, + 'bidid': 'bid-identifier', + 'seatbid': [{ + 'bid': [{ + 'id': 'bid-response-id', + 'impid': 'test-bid-id' + }] + }] + }; + it('should get correct bid response', function () { let expectedResponse = [ { @@ -488,6 +555,18 @@ describe('RTBHouseAdapter', () => { expect(result.length).to.equal(0); }); + context('when the response contains FLEDGE interest groups config', function () { + let bidderRequest; + let response = spec.interpretResponse({body: fledgeResponse}, {bidderRequest}); + + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test-bid-id'); + }); + }); + describe('native', () => { const adm = { native: {