diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 998f4826ead..fddf70229af 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -260,6 +260,10 @@ export const spec = { utils.deepSetValue(data, 'regs.coppa', 1); } + if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { + utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); + } + return { method: 'POST', url: VIDEO_ENDPOINT, @@ -277,7 +281,7 @@ export const spec = { url: FASTLANE_ENDPOINT, data: spec.getOrderedParams(bidParams).reduce((paramString, key) => { const propValue = bidParams[key]; - return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${key}=${encodeURIComponent(propValue)}&` : paramString; + return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; }, '') + `slots=1&rand=${Math.random()}`, bidRequest }; @@ -308,7 +312,7 @@ export const spec = { url: FASTLANE_ENDPOINT, data: spec.getOrderedParams(combinedSlotParams).reduce((paramString, key) => { const propValue = combinedSlotParams[key]; - return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${key}=${encodeURIComponent(propValue)}&` : paramString; + return ((utils.isStr(propValue) && propValue !== '') || utils.isNumber(propValue)) ? `${paramString}${encodeParam(key, propValue)}&` : paramString; }, '') + `slots=${bidsInGroup.length}&rand=${Math.random()}`, bidRequest: bidsInGroup }); @@ -332,6 +336,8 @@ export const spec = { 'p_pos', 'gdpr', 'gdpr_consent', + 'rp_schain', + 'rf', 'tpid_tdid', 'tpid_liveintent.com', 'tg_v.LIseg', @@ -482,9 +488,38 @@ export const spec = { data['coppa'] = 1; } + // if SupplyChain is supplied and contains all required fields + if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { + data.rp_schain = spec.serializeSupplyChain(bidRequest.schain); + } + return data; }, + /** + * Serializes schain params according to OpenRTB requirements + * @param {Object} supplyChain + * @returns {String} + */ + serializeSupplyChain: function (supplyChain) { + const supplyChainIsValid = hasValidSupplyChainParams(supplyChain); + if (!supplyChainIsValid) return ''; + const { ver, complete, nodes } = supplyChain; + return `${ver},${complete}!${spec.serializeSupplyChainNodes(nodes)}`; + }, + + /** + * Properly sorts schain object params + * @param {Array} nodes + * @returns {String} + */ + serializeSupplyChainNodes: function (nodes) { + const nodePropOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + return nodes.map(node => { + return nodePropOrder.map(prop => encodeURIComponent(node[prop] || '')).join(','); + }).join('!'); + }, + /** * @param {*} responseObj * @param {BidRequest|Object.} bidRequest - if request was SRA the bidRequest argument will be a keyed BidRequest array object, @@ -982,6 +1017,35 @@ export function hasValidVideoParams(bid) { return isValid; } +/** + * Make sure the required params are present + * @param {Object} schain + * @param {Bool} + */ +export function hasValidSupplyChainParams(schain) { + let isValid = false; + const requiredFields = ['asi', 'sid', 'hp']; + if (!schain.nodes) return isValid; + isValid = schain.nodes.reduce((status, node) => { + if (!status) return status; + return requiredFields.every(field => node[field]); + }, true); + if (!isValid) utils.logError('Rubicon: required schain params missing'); + return isValid; +} + +/** + * Creates a URL key value param, encoding the + * param unless the key is schain + * @param {String} key + * @param {String} param + * @returns {String} + */ +export function encodeParam(key, param) { + if (key === 'rp_schain') return `rp_schain=${param}`; + return `${key}=${encodeURIComponent(param)}`; +} + /** * split array into multiple arrays of defined size * @param {Array} array diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index e037c7d5ed7..64c0f450e39 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -68,6 +68,49 @@ describe('the rubicon adapter', function () { * @param {Array.} [indexOverMap] * @return {{status: string, cpm: number, zone_id: *, size_id: *, impression_id: *, ad_id: *, creative_id: string, type: string, targeting: *[]}} */ + + function getBidderRequest() { + return { + bidderCode: 'rubicon', + auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + bidderRequestId: '178e34bad3658f', + bids: [ + { + bidder: 'rubicon', + params: { + accountId: '14062', + siteId: '70608', + zoneId: '335918', + userId: '12346', + keywords: ['a', 'b', 'c'], + inventory: { + rating: '5-star', // This actually should not be sent to frank!! causes 400 + prodtype: ['tech', 'mobile'] + }, + visitor: { + ucat: 'new', + lastsearch: 'iphone', + likes: ['sports', 'video games'] + }, + position: 'atf', + referrer: 'localhost', + latLong: [40.7607823, '111.8910325'] + }, + adUnitCode: '/19968336/header-bid-tag-0', + code: 'div-1', + sizes: [[300, 250], [320, 50]], + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + } + ], + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000 + }; + }; + function createResponseAdByIndex(i, sizeId, indexOverMap) { const overridePropMap = (indexOverMap && indexOverMap[i] && typeof indexOverMap[i] === 'object') ? indexOverMap[i] : {}; const overrideProps = Object.keys(overridePropMap).reduce((aggregate, key) => { @@ -2267,4 +2310,98 @@ describe('the rubicon adapter', function () { }); }); }); + + describe.only('Supply Chain Support', function() { + const nodePropsOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + let bidRequests; + let schainConfig; + + const getSupplyChainConfig = () => { + return { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'rubicon.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'pub one', + domain: 'pub1.com' + }, + { + asi: 'theexchange.com', + sid: '5678', + hp: 1, + rid: 'bid-request-2', + name: 'pub two', + domain: 'pub2.com' + }, + { + asi: 'wesellads.com', + sid: '9876', + hp: 1, + rid: 'bid-request-3', + // name: 'alladsallthetime', + domain: 'alladsallthetime.com' + } + ] + }; + }; + + beforeEach(() => { + bidRequests = getBidderRequest(); + schainConfig = getSupplyChainConfig(); + bidRequests.bids[0].schain = schainConfig; + }); + + it('should properly serialize schain object with correct delimiters', () => { + const results = spec.buildRequests(bidRequests.bids, bidRequests); + const numNodes = schainConfig.nodes.length; + const schain = parseQuery(results[0].data).rp_schain; + + // each node serialization should start with an ! + expect(schain.match(/!/g).length).to.equal(numNodes); + + // 5 commas per node plus 1 for version + expect(schain.match(/,/g).length).to.equal(numNodes * 5 + 1); + }); + + it('should send the proper version for the schain', () => { + const results = spec.buildRequests(bidRequests.bids, bidRequests); + const schain = parseQuery(results[0].data).rp_schain.split('!'); + const version = schain.shift().split(',')[0]; + expect(version).to.equal(bidRequests.bids[0].schain.ver); + }); + + it('should send the correct value for complete in schain', () => { + const results = spec.buildRequests(bidRequests.bids, bidRequests); + const schain = parseQuery(results[0].data).rp_schain.split('!'); + const complete = schain.shift().split(',')[1]; + expect(complete).to.equal(String(bidRequests.bids[0].schain.complete)); + }); + + it('should send available params in the right order', () => { + const results = spec.buildRequests(bidRequests.bids, bidRequests); + const schain = parseQuery(results[0].data).rp_schain.split('!'); + schain.shift(); + + schain.forEach((serializeNode, nodeIndex) => { + const nodeProps = serializeNode.split(','); + nodeProps.forEach((nodeProp, propIndex) => { + const node = schainConfig.nodes[nodeIndex]; + const key = nodePropsOrder[propIndex]; + expect(nodeProp).to.equal(node[key] ? String(node[key]) : ''); + }); + }); + }); + + it('should copy the schain JSON to to bid.source.ext.schain', () => { + createVideoBidderRequest(); + const schain = getSupplyChainConfig(); + bidderRequest.bids[0].schain = schain; + const request = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request[0].data.source.ext.schain).to.deep.equal(schain); + }); + }); });