diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3f7687479ec..8f7727a61aa 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -287,6 +287,32 @@ function _appendSiteAppDevice(request, pageUrl) { } } +function addBidderFirstPartyDataToRequest(request) { + const bidderConfig = config.getBidderConfig(); + const fpdConfigs = Object.keys(bidderConfig).reduce((acc, bidder) => { + const currBidderConfig = bidderConfig[bidder]; + if (currBidderConfig.fpd) { + const fpd = {}; + if (currBidderConfig.fpd.context) { + fpd.site = currBidderConfig.fpd.context; + } + if (currBidderConfig.fpd.user) { + fpd.user = currBidderConfig.fpd.user; + } + + acc.push({ + bidders: [ bidder ], + config: { fpd } + }); + } + return acc; + }, []); + + if (fpdConfigs.length) { + utils.deepSetValue(request, 'ext.prebid.bidderconfig', fpdConfigs); + } +} + // https://iabtechlab.com/wp-content/uploads/2016/07/OpenRTB-Native-Ads-Specification-Final-1.2.pdf#page=40 let nativeDataIdMap = { sponsoredBy: 1, // sponsored @@ -661,6 +687,15 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'regs.coppa', 1); } + const commonFpd = getConfig('fpd') || {}; + if (commonFpd.context) { + utils.deepSetValue(request, 'site.ext.data', commonFpd.context); + } + if (commonFpd.user) { + utils.deepSetValue(request, 'user.ext.data', commonFpd.user); + } + addBidderFirstPartyDataToRequest(request); + return request; }, diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 801bbc3dd8c..34fdb76fcef 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -268,6 +268,27 @@ export const spec = { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } + const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); + const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); + if (!utils.isEmpty(siteData) || !utils.isEmpty(userData)) { + const bidderData = { + bidders: [ bidderRequest.bidderCode ], + config: { + fpd: {} + } + }; + + if (!utils.isEmpty(siteData)) { + bidderData.config.fpd.site = siteData; + } + + if (!utils.isEmpty(userData)) { + bidderData.config.fpd.user = userData; + } + + utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); + } + /** * Prebid AdSlot * @type {(string|undefined)} @@ -448,7 +469,6 @@ export const spec = { 'x_source.tid': bidRequest.transactionId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), - 'kw': Array.isArray(params.keywords) ? params.keywords.join(',') : '', 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), 'p_geo.longitude': isNaN(parseFloat(longitude)) ? undefined : parseFloat(longitude).toFixed(4), @@ -487,22 +507,30 @@ export const spec = { } // visitor properties - if (params.visitor !== null && typeof params.visitor === 'object') { - Object.keys(params.visitor).forEach((key) => { - if (params.visitor[key] != null) { - data[`tg_v.${key}`] = params.visitor[key].toString(); // initialize array; - } - }); - } + const visitorData = Object.assign({}, params.visitor, config.getConfig('fpd.user')); + Object.keys(visitorData).forEach((key) => { + if (visitorData[key] != null && key !== 'keywords') { + data[`tg_v.${key}`] = typeof visitorData[key] === 'object' && !Array.isArray(visitorData[key]) + ? JSON.stringify(visitorData[key]) + : visitorData[key].toString(); // initialize array; + } + }); // inventory properties - if (params.inventory !== null && typeof params.inventory === 'object') { - Object.keys(params.inventory).forEach((key) => { - if (params.inventory[key] != null) { - data[`tg_i.${key}`] = params.inventory[key].toString(); - } - }); - } + const inventoryData = Object.assign({}, params.inventory, config.getConfig('fpd.context')); + Object.keys(inventoryData).forEach((key) => { + if (inventoryData[key] != null && key !== 'keywords') { + data[`tg_i.${key}`] = typeof inventoryData[key] === 'object' && !Array.isArray(inventoryData[key]) + ? JSON.stringify(inventoryData[key]) + : inventoryData[key].toString(); + } + }); + + // keywords + const keywords = (params.keywords || []).concat( + utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || [], + utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || []); + data.kw = keywords.length ? keywords.join(',') : ''; /** * Prebid AdSlot diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 1253098a399..bc655c6e432 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1309,7 +1309,51 @@ describe('S2S Adapter', function () { adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.source.ext.schain).to.deep.equal(schainObject); - }) + }); + + it('passes first party data in request', () => { + const s2sBidRequest = utils.deepClone(REQUEST); + const bidRequests = utils.deepClone(BID_REQUESTS); + + const commonContext = { + keywords: ['power tools'], + search: 'drill' + }; + const commonUser = { + keywords: ['a', 'b'], + gender: 'M' + }; + + const context = { + content: { userrating: 4 }, + data: { + pageType: 'article', + category: 'tools' + } + }; + const user = { + yob: '1984', + geo: { country: 'ca' }, + data: { + registered: true, + interests: ['cars'] + } + }; + const allowedBidders = [ 'rubicon', 'appnexus' ]; + + const expected = allowedBidders.map(bidder => ({ + bidders: [ bidder ], + config: { fpd: { site: context, user } } + })); + + config.setConfig({ fpd: { context: commonContext, user: commonUser } }); + config.setBidderConfig({ bidders: allowedBidders, config: { fpd: { context, user } } }); + adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.bidderconfig).to.deep.equal(expected); + expect(parsedRequestBody.site.ext.data).to.deep.equal(commonContext); + expect(parsedRequestBody.user.ext.data).to.deep.equal(commonUser); + }); describe('pbAdSlot config', function () { it('should not send \"imp.ext.context.data.adslot\" if \"fpd.context\" is undefined', function () { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 2413f2703f9..66f1dd0a989 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -980,6 +980,51 @@ describe('the rubicon adapter', function () { expect(data[key]).to.equal(value); }); }); + + it('should use first party data from getConfig over the bid params, if present', () => { + const context = { + keywords: ['e', 'f'], + rating: '4-star' + }; + const user = { + keywords: ['d'], + gender: 'M', + yob: '1984', + geo: { country: 'ca' } + }; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + fpd: { + context, + user + } + }; + return utils.deepAccess(config, key); + }); + + const expectedQuery = { + 'kw': 'a,b,c,d,e,f', + 'tg_v.ucat': 'new', + 'tg_v.lastsearch': 'iphone', + 'tg_v.likes': 'sports,video games', + 'tg_v.gender': 'M', + 'tg_v.yob': '1984', + 'tg_v.geo': '{"country":"ca"}', + 'tg_i.rating': '4-star', + 'tg_i.prodtype': 'tech,mobile', + }; + + // get the built request + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + + // make sure that tg_v, tg_i, and kw values are correct + Object.keys(expectedQuery).forEach(key => { + let value = expectedQuery[key]; + expect(data[key]).to.deep.equal(value); + }); + }); }); describe('singleRequest config', function () { @@ -1641,6 +1686,44 @@ describe('the rubicon adapter', function () { expect(request.data.regs.coppa).to.equal(1); }); + it('should include first party data', () => { + createVideoBidderRequest(); + + const context = { + keywords: ['e', 'f'], + rating: '4-star' + }; + const user = { + keywords: ['d'], + gender: 'M', + yob: '1984', + geo: { country: 'ca' } + }; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + fpd: { + context, + user + } + }; + return utils.deepAccess(config, key); + }); + + const expected = [{ + bidders: [ 'rubicon' ], + config: { + fpd: { + site: Object.assign({}, bidderRequest.bids[0].params.inventory, context), + user: Object.assign({}, bidderRequest.bids[0].params.visitor, user) + } + } + }]; + + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.ext.prebid.bidderconfig).to.deep.equal(expected); + }); + it('should include storedAuctionResponse in video bid request', function () { createVideoBidderRequest();