From b12e6ddbcc1a09c8f3a1f85330d31f5dcd389380 Mon Sep 17 00:00:00 2001 From: afsheenb Date: Thu, 21 Feb 2019 23:35:51 -0500 Subject: [PATCH] added setTargeting code for custom key-value pairs (#3550) * added setTargeting code for custom key-value pairs * v1.4.3 - refactored setTargeting code, includes support for pub common ID being passed inside ext.ozone object, refactored the ext.ozone object to carry any data objects defined in on-page bidder config * Update to setTargeting functionality per feedback from prebid.org. * switching from // to https:// to match unit test --- modules/ozoneBidAdapter.js | 297 +++++++++++++++------- test/spec/modules/ozoneBidAdapter_spec.js | 51 ++++ 2 files changed, 252 insertions(+), 96 deletions(-) diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 06b831fe21c..7a7033a79ac 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -1,11 +1,13 @@ import * as utils from '../src/utils'; import { registerBidder } from '../src/adapters/bidderFactory'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes'; +import {config} from '../src/config'; const BIDDER_CODE = 'ozone'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const OZONECOOKIESYNC = 'https://elb.the-ozone-project.com/static/load-cookie.html'; - +const OZONEVERSION = '1.4.4'; export const spec = { code: BIDDER_CODE, @@ -63,59 +65,17 @@ export const spec = { } return true; }, - /** - * Interpret the response if the array contains BIDDER elements, in the format: [ [bidder1 bid 1, bidder1 bid 2], [bidder2 bid 1, bidder2 bid 2] ] - * @param serverResponse - * @param request - * @returns {*} - */ - interpretResponse(serverResponse, request) { - utils.logInfo('ozone interpretResponse version 2018-12-05 16:46'); - serverResponse = serverResponse.body || {}; - if (serverResponse.seatbid) { - if (utils.isArray(serverResponse.seatbid)) { - // serverResponse seems good, let's get the list of bids from the request object: - let arrRequestBids = request.bidderRequest.bids; - // build up a list of winners, one for each bidId in arrBidIds - let arrWinners = []; - for (let i = 0; i < arrRequestBids.length; i++) { - let winner = ozoneGetWinnerForRequestBid(arrRequestBids[i], serverResponse.seatbid); - if (winner !== null) { - const {defaultWidth, defaultHeight} = defaultSize(arrRequestBids[i]); - winner = ozoneAddStandardProperties(winner, defaultWidth, defaultHeight); - arrWinners.push(winner); - } - } - let winnersClean = arrWinners.filter(w => { - return (w.bidId); // will be cast to boolean - }); - utils.logInfo(['going to return winnersClean:', winnersClean]); - return winnersClean; - } else { - return []; - } - } else { - return []; - } - }, buildRequests(validBidRequests, bidderRequest) { - let ozoneRequest = validBidRequests[0].params; - ozoneRequest['id'] = utils.generateUUID(); - ozoneRequest['auctionId'] = bidderRequest['auctionId']; - - if (bidderRequest.hasOwnProperty('placementId')) { - bidderRequest.placementId = (bidderRequest.placementId).toString(); - } - if (bidderRequest.hasOwnProperty('siteId')) { - bidderRequest.siteId = (bidderRequest.siteId).toString(); - } - if (bidderRequest.hasOwnProperty('publisherId')) { - bidderRequest.publisherId = (bidderRequest.publisherId).toString(); - } + utils.logInfo('ozone v' + OZONEVERSION + ' validBidRequests', validBidRequests, 'bidderRequest', bidderRequest); + utils.logInfo('buildRequests setting auctionId', bidderRequest.auctionId); + let singleRequest = config.getConfig('ozone.singleRequest'); + singleRequest = singleRequest !== false; // undefined & true will be true + utils.logInfo('config ozone.singleRequest : ', singleRequest); + let htmlParams = validBidRequests[0].params; // the html page config params will be included in each element + let ozoneRequest = {}; // we only want to set specific properties on this, not validBidRequests[0].params + // ozoneRequest['id'] = utils.generateUUID(); - // if (!ozoneRequest.test) { delete ozoneRequest.test; // don't allow test to be set in the config - ONLY use $_GET['pbjs_debug'] - // } if (bidderRequest.gdprConsent) { ozoneRequest.regs = {}; ozoneRequest.regs.ext = {}; @@ -126,25 +86,49 @@ export const spec = { } let tosendtags = validBidRequests.map(ozoneBidRequest => { var obj = {}; - obj.id = ozoneBidRequest.bidId; - obj.tagid = String(ozoneBidRequest.params.ozoneid); + obj.id = ozoneBidRequest.bidId; // this causes a failure if we change it to something else + // obj.id = ozoneBidRequest.adUnitCode; // (eg. 'mpu' or 'leaderboard') A unique identifier for this impression within the context of the bid request (typically, starts with 1 and increments. + obj.tagid = (ozoneBidRequest.params.placementId).toString(); obj.secure = window.location.protocol === 'https:' ? 1 : 0; - obj.banner = { - topframe: 1, - w: ozoneBidRequest.sizes[0][0] || 0, - h: ozoneBidRequest.sizes[0][1] || 0, - format: ozoneBidRequest.sizes.map(s => { - return {w: s[0], h: s[1]}; - }) - }; - if (ozoneBidRequest.params.hasOwnProperty('customData')) { - obj.customData = ozoneBidRequest.params.customData; + // is there a banner (or nothing declared, so banner is the default)? + let arrBannerSizes = []; + /* NOTE - if there is sizes element in the config root then there will be a mediaTypes.banner element automatically generated for us, so this code is deprecated */ + if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { + if (ozoneBidRequest.hasOwnProperty('sizes')) { + utils.logInfo('no mediaTypes detected - will use the sizes array in the config root'); + arrBannerSizes = ozoneBidRequest.sizes; + } else { + utils.logInfo('no mediaTypes detected, no sizes array in the config root either. Cannot set sizes for banner type'); + } + } else { + if (ozoneBidRequest.mediaTypes.hasOwnProperty(BANNER)) { + arrBannerSizes = ozoneBidRequest.mediaTypes[BANNER].sizes; /* Note - if there is a sizes element in the config root it will be pushed into here */ + utils.logInfo('setting banner size from the mediaTypes.banner element for bidId ' + obj.id + ': ', arrBannerSizes); + } + // Video integration is not complete yet + if (ozoneBidRequest.mediaTypes.hasOwnProperty(VIDEO)) { + obj.video = ozoneBidRequest.mediaTypes[VIDEO]; + utils.logInfo('setting video object from the mediaTypes.video element: ' + obj.id + ':', obj.video); + } + // Native integration is not complete yet + if (ozoneBidRequest.mediaTypes.hasOwnProperty(NATIVE)) { + obj.native = ozoneBidRequest.mediaTypes[NATIVE]; + utils.logInfo('setting native object from the mediaTypes.native element: ' + obj.id + ':', obj.native); + } } - if (ozoneBidRequest.params.hasOwnProperty('ozoneData')) { - obj.ozoneData = ozoneBidRequest.params.ozoneData; + // build the banner request using banner sizes we found in either possible location: + if (arrBannerSizes.length > 0) { + obj.banner = { + topframe: 1, + w: arrBannerSizes[0][0] || 0, + h: arrBannerSizes[0][1] || 0, + format: arrBannerSizes.map(s => { + return {w: s[0], h: s[1]}; + }) + }; } - if (ozoneBidRequest.params.hasOwnProperty('lotameData')) { - obj.lotameData = ozoneBidRequest.params.lotameData; + if (ozoneBidRequest.params.hasOwnProperty('placementId')) { + obj.placementId = (ozoneBidRequest.params.placementId).toString(); } if (ozoneBidRequest.params.hasOwnProperty('publisherId')) { obj.publisherId = (ozoneBidRequest.params.publisherId).toString(); @@ -152,22 +136,132 @@ export const spec = { if (ozoneBidRequest.params.hasOwnProperty('siteId')) { obj.siteId = (ozoneBidRequest.params.siteId).toString(); } - obj.ext = {'prebid': {'storedrequest': {'id': (ozoneBidRequest.params.placementId).toString()}}}; + // build the imp['ext'] object + obj.ext = {'prebid': {'storedrequest': {'id': (ozoneBidRequest.params.placementId).toString()}}, 'ozone': {}}; + obj.ext.ozone.adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' + obj.ext.ozone.transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit + if (ozoneBidRequest.params.hasOwnProperty('customData')) { + obj.ext.ozone.customData = ozoneBidRequest.params.customData; + } + if (ozoneBidRequest.params.hasOwnProperty('ozoneData')) { + obj.ext.ozone.ozoneData = ozoneBidRequest.params.ozoneData; + } + if (ozoneBidRequest.params.hasOwnProperty('lotameData')) { + obj.ext.ozone.lotameData = ozoneBidRequest.params.lotameData; + } + if (ozoneBidRequest.hasOwnProperty('crumbs') && ozoneBidRequest.crumbs.hasOwnProperty('pubcid')) { + obj.ext.ozone.pubcid = ozoneBidRequest.crumbs.pubcid; + } return obj; }); - ozoneRequest.imp = tosendtags; - ozoneRequest.site = {'publisher': {'id': ozoneRequest.publisherId}, 'page': document.location.href}; + utils.logInfo('tosendtags = ', tosendtags); + + ozoneRequest.site = {'publisher': {'id': htmlParams.publisherId}, 'page': document.location.href}; ozoneRequest.test = parseInt(getTestQuerystringValue()); // will be 1 or 0 - var ret = { - method: 'POST', - url: OZONEURI, - data: JSON.stringify(ozoneRequest), - bidderRequest: bidderRequest - }; - utils.logInfo(['buildRequests going to return', ret]); - return ret; + // utils.logInfo('_ozoneInternal is', _ozoneInternal); + // return the single request object OR the array: + if (singleRequest) { + utils.logInfo('buildRequests starting to generate response for a single request'); + ozoneRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. + ozoneRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + ozoneRequest.imp = tosendtags; + ozoneRequest.source = {'tid': bidderRequest.auctionId}; // RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). + var ret = { + method: 'POST', + url: OZONEURI, + data: JSON.stringify(ozoneRequest), + bidderRequest: bidderRequest + }; + utils.logInfo('buildRequests ozoneRequest for single = ', ozoneRequest); + utils.logInfo('buildRequests going to return for single: ', ret); + return ret; + } + + // not single request - pull apart the tosendtags array & return an array of objects each containing one element in the imp array. + let arrRet = tosendtags.map(imp => { + utils.logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); + let ozoneRequestSingle = Object.assign({}, ozoneRequest); + imp.ext.ozone.pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object + ozoneRequestSingle.id = imp.ext.ozone.transactionId; // Unique ID of the bid request, provided by the exchange. + ozoneRequestSingle.auctionId = imp.ext.ozone.transactionId; // not sure if this should be here? + ozoneRequestSingle.imp = [imp]; + ozoneRequestSingle.source = {'tid': imp.ext.ozone.transactionId}; + utils.logInfo('buildRequests ozoneRequestSingle (for non-single) = ', ozoneRequestSingle); + return { + method: 'POST', + url: OZONEURI, + data: JSON.stringify(ozoneRequestSingle), + bidderRequest: bidderRequest + }; + }); + utils.logInfo('buildRequests going to return for non-single: ', arrRet); + return arrRet; }, + /** + * Interpret the response if the array contains BIDDER elements, in the format: [ [bidder1 bid 1, bidder1 bid 2], [bidder2 bid 1, bidder2 bid 2] ] + * NOte that in singleRequest mode this will be called once, else it will be called for each adSlot's response + * @param serverResponse + * @param request + * @returns {*} + */ + interpretResponse(serverResponse, request) { + utils.logInfo('ozone v' + OZONEVERSION + ' interpretResponse', serverResponse, request); + serverResponse = serverResponse.body || {}; + if (serverResponse.seatbid) { + if (utils.isArray(serverResponse.seatbid)) { + // serverResponse seems good, let's get the list of bids from the request object: + let arrRequestBids = request.bidderRequest.bids; + // build up a list of winners, one for each bidId in arrBidIds + let arrWinners = []; + for (let i = 0; i < arrRequestBids.length; i++) { + let thisBid = arrRequestBids[i]; + let ozoneInternalKey = thisBid.bidId; + let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid, serverResponse.seatbid); + + if (winningBid == null) { + utils.logInfo('FAILED to get winning bid for bid : ', thisBid, 'will skip. Possibly a non-single request, which will be missing some bid IDs'); + continue; + } + const {defaultWidth, defaultHeight} = defaultSize(arrRequestBids[i]); + winningBid = ozoneAddStandardProperties(winningBid, defaultWidth, defaultHeight); + + utils.logInfo('Going to add the adserverTargeting custom parameters for key: ', ozoneInternalKey); + let adserverTargeting = {}; + let allBidsForThisBidid = ozoneGetAllBidsForBidId(ozoneInternalKey, serverResponse.seatbid); + // add all the winning & non-winning bids for this bidId: + Object.keys(allBidsForThisBidid).forEach(function(bidderName, index, ar2) { + utils.logInfo('inside allBidsForThisBidid:foreach', bidderName, index, ar2, allBidsForThisBidid); + adserverTargeting['oz_' + bidderName] = bidderName; + adserverTargeting['oz_' + bidderName + '_pb'] = String(allBidsForThisBidid[bidderName].price); + adserverTargeting['oz_' + bidderName + '_crid'] = String(allBidsForThisBidid[bidderName].crid); + adserverTargeting['oz_' + bidderName + '_adv'] = String(allBidsForThisBidid[bidderName].adomain); + adserverTargeting['oz_' + bidderName + '_imp_id'] = String(allBidsForThisBidid[bidderName].impid); + }); + // now add the winner data: + adserverTargeting['oz_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting['oz_winner'] = String(winningSeat); + adserverTargeting['oz_winner_auc_id'] = String(winningBid.id); + adserverTargeting['oz_winner_imp_id'] = String(winningBid.impid); + adserverTargeting['oz_response_id'] = String(serverResponse.id); + + winningBid.adserverTargeting = adserverTargeting; + utils.logInfo('winner is', winningBid); + arrWinners.push(winningBid); + utils.logInfo('arrWinners is', arrWinners); + } + let winnersClean = arrWinners.filter(w => { + return (w.bidId); // will be cast to boolean + }); + utils.logInfo('going to return winnersClean:', winnersClean); + return winnersClean; + } else { + return []; + } + } else { + return []; + } + }, getUserSyncs(optionsType, serverResponse) { if (!serverResponse || serverResponse.length === 0) { return []; @@ -180,22 +274,6 @@ export const spec = { } } } - -/** - * Function matchRequest(id: string, BidRequest: object) - * @param id - * @type string - * @param bidRequest - * @type Object - * @returns Object - * - */ -export function matchRequest(id, bidRequest) { - const {bids} = bidRequest.bidderRequest; - const [returnValue] = bids.filter(bid => bid.bidId === id); - return returnValue; -} - export function checkDeepArray(Arr) { if (Array.isArray(Arr)) { if (Array.isArray(Arr[0])) { @@ -223,19 +301,45 @@ export function defaultSize(thebidObj) { */ export function ozoneGetWinnerForRequestBid(requestBid, serverResponseSeatBid) { let thisBidWinner = null; + let winningSeat = null; for (let j = 0; j < serverResponseSeatBid.length; j++) { let theseBids = serverResponseSeatBid[j].bid; let thisSeat = serverResponseSeatBid[j].seat; for (let k = 0; k < theseBids.length; k++) { if (theseBids[k].impid === requestBid.bidId) { // we've found a matching server response bid for this request bid + // if (theseBids[k].impid === requestBid.adUnitCode) { // we've found a matching server response bid for this request bid if ((thisBidWinner == null) || (thisBidWinner.price < theseBids[k].price)) { thisBidWinner = theseBids[k]; - thisBidWinner.seat = thisSeat; // we need to add this here - it's the name of the winning bidder, not guaranteed to be available in the bid object. + winningSeat = thisSeat; + break; } } } } - return thisBidWinner; + return {'seat': winningSeat, 'bid': thisBidWinner}; +} + +/** + * Get a list of all the bids, for this bidId + * @param matchBidId + * @param serverResponseSeatBid + * @returns {} = {ozone:{obj}, appnexus:{obj}, ... } + */ +export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid) { + utils.logInfo('ozoneGetAllBidsForBidId - starting, with: ', matchBidId, serverResponseSeatBid); + let objBids = {}; + for (let j = 0; j < serverResponseSeatBid.length; j++) { + let theseBids = serverResponseSeatBid[j].bid; + let thisSeat = serverResponseSeatBid[j].seat; + for (let k = 0; k < theseBids.length; k++) { + if (theseBids[k].impid === matchBidId) { // we've found a matching server response bid for the request bid we're looking for + utils.logInfo('ozoneGetAllBidsForBidId - found matching bid: ', matchBidId, theseBids[k]); + objBids[thisSeat] = theseBids[k]; + } + } + } + utils.logInfo('ozoneGetAllBidsForBidId - going to return: ', objBids); + return objBids; } /** @@ -275,3 +379,4 @@ export function getTestQuerystringValue() { } registerBidder(spec); +utils.logInfo('ozoneBidAdapter ended'); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 6b4f6c88bdd..c404d9003fe 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/ozoneBidAdapter'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; +// const OZONEURI = 'https://www.1in39.co.uk/openrtb2/auction'; const BIDDER_CODE = 'ozone'; /* @@ -22,6 +23,34 @@ var validBidRequests = [ transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; +var validBidRequestsNoSizes = [ + { + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', + bidId: '2899ec066a91ff8', + bidRequestsCount: 1, + bidder: 'ozone', + bidderRequestId: '1c1586b27a1b5c8', + crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, + params: { publisherId: '9876abcd12-3', customData: {'gender': 'bart', 'age': 'low'}, ozoneData: {'networkID': '3048', 'dfpSiteID': 'd.thesun', 'sectionID': 'homepage', 'path': '/', 'sec_id': 'null', 'sec': 'sec', 'topics': 'null', 'kw': 'null', 'aid': 'null', 'search': 'null', 'article_type': 'null', 'hide_ads': '', 'article_slug': 'null'}, lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}], 'ThirdPartyAudience': [{'id': '123', 'name': 'Automobiles'}, {'id': '456', 'name': 'Ages: 30-39'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' + } +]; + +var validBidRequestsWithMediaTypes = [ + { + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', + bidId: '2899ec066a91ff8', + bidRequestsCount: 1, + bidder: 'ozone', + bidderRequestId: '1c1586b27a1b5c8', + crumbs: {pubcid: '203a0692-f728-4856-87f6-9a25a6b63715'}, + params: { publisherId: '9876abcd12-3', customData: {'gender': 'bart', 'age': 'low'}, ozoneData: {'networkID': '3048', 'dfpSiteID': 'd.thesun', 'sectionID': 'homepage', 'path': '/', 'sec_id': 'null', 'sec': 'sec', 'topics': 'null', 'kw': 'null', 'aid': 'null', 'search': 'null', 'article_type': 'null', 'hide_ads': '', 'article_slug': 'null'}, lotameData: {'Profile': {'tpid': 'c8ef27a0d4ba771a81159f0d2e792db4', 'Audiences': {'Audience': [{'id': '99999', 'abbr': 'sports'}, {'id': '88888', 'abbr': 'movie'}, {'id': '77777', 'abbr': 'blogger'}], 'ThirdPartyAudience': [{'id': '123', 'name': 'Automobiles'}, {'id': '456', 'name': 'Ages: 30-39'}]}}}, placementId: '1310000099', siteId: '1234567890', id: 'fea37168-78f1-4a23-a40e-88437a99377e', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', imp: [ { id: '2899ec066a91ff8', tagid: 'undefined', secure: 1, banner: { format: [{ w: 300, h: 250 }, { w: 300, h: 600 }], h: 250, topframe: 1, w: 300 } } ] }, + mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}, + transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' + } +]; var validBidderRequest = { auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -211,6 +240,18 @@ describe('ozone Adapter', function () { expect(spec.isBidRequestValid(xMissingPublisher)).to.equal(false); }); + var xMissingSiteId = { + bidder: BIDDER_CODE, + params: { + publisherId: '9876abcd12-3', + placementId: '1234567890', + } + }; + + it('should not validate missing sitetId', function () { + expect(spec.isBidRequestValid(xMissingSiteId)).to.equal(false); + }); + var xBadPublisherTooShort = { bidder: BIDDER_CODE, params: { @@ -419,6 +460,16 @@ describe('ozone Adapter', function () { const request = spec.buildRequests(validBidRequests, validBidderRequest); expect(request.bidderRequest.bids[0].bidder).to.equal(BIDDER_CODE); }); + + it('handles mediaTypes element correctly', function () { + const request = spec.buildRequests(validBidRequestsWithMediaTypes, validBidderRequest); + expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); + }); + + it('should not crash when there is no sizes element at all', function () { + const request = spec.buildRequests(validBidRequestsNoSizes, validBidderRequest); + expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); + }); }); describe('interpretResponse', function () {