diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 406bffdf4df..c5d9f16ef58 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -2,14 +2,20 @@ import * as utils from '../src/utils'; import { registerBidder } from '../src/adapters/bidderFactory'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes'; import {config} from '../src/config'; +import {getPriceBucketString} from '../src/cpmBucketManager'; +import { Renderer } from '../src/Renderer' const BIDDER_CODE = 'ozone'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; +const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js' + const OZONECOOKIESYNC = 'https://elb.the-ozone-project.com/static/load-cookie.html'; -const OZONEVERSION = '1.4.7'; +const OZONEVERSION = '2.0.0'; + export const spec = { code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], /** * Basic check to see whether required parameters are in the request. @@ -63,19 +69,26 @@ export const spec = { return false; } } + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + if (!bid.mediaTypes.video.hasOwnProperty('context')) { + utils.logInfo('OZONE: [WARNING] No context key/value in bid. Rejecting bid: ', ozoneBidRequest); + return false; + } + if (bid.mediaTypes.video.context !== 'outstream') { + utils.logInfo('OZONE: [WARNING] Only outstream video is supported. Rejecting bid: ', ozoneBidRequest); + return false; + } + } return true; }, + buildRequests(validBidRequests, bidderRequest) { utils.logInfo('OZONE: ozone v' + OZONEVERSION + ' validBidRequests', validBidRequests, 'bidderRequest', bidderRequest); - utils.logInfo('OZONE: buildRequests setting auctionId', bidderRequest.auctionId); let singleRequest = config.getConfig('ozone.singleRequest'); - singleRequest = singleRequest !== false; // undefined & true will be true utils.logInfo('OZONE: 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(); - delete ozoneRequest.test; // don't allow test to be set in the config - ONLY use $_GET['pbjs_debug'] if (bidderRequest.gdprConsent) { utils.logInfo('OZONE: ADDING GDPR info'); @@ -92,8 +105,7 @@ export const spec = { ozoneRequest.device = {'w': window.innerWidth, 'h': window.innerHeight}; let tosendtags = validBidRequests.map(ozoneBidRequest => { var obj = {}; - 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.id = ozoneBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder ozone made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = (ozoneBidRequest.params.placementId).toString(); obj.secure = window.location.protocol === 'https:' ? 1 : 0; // is there a banner (or nothing declared, so banner is the default)? @@ -141,6 +153,7 @@ export const spec = { 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 + obj.ext.ozone.oz_pb_v = OZONEVERSION; if (ozoneBidRequest.params.hasOwnProperty('customData')) { obj.ext.ozone.customData = ozoneBidRequest.params.customData; } @@ -150,16 +163,14 @@ export const spec = { if (ozoneBidRequest.params.hasOwnProperty('lotameData')) { obj.ext.ozone.lotameData = ozoneBidRequest.params.lotameData; } - if (ozoneBidRequest.hasOwnProperty('crumbs') && ozoneBidRequest.crumbs.hasOwnProperty('pubcid')) { + if (utils.deepAccess(ozoneBidRequest, 'crumbs.pubcid')) { obj.ext.ozone.pubcid = ozoneBidRequest.crumbs.pubcid; } return obj; }); - utils.logInfo('tosendtags = ', tosendtags); ozoneRequest.site = {'publisher': {'id': htmlParams.publisherId}, 'page': document.location.href}; ozoneRequest.test = parseInt(getTestQuerystringValue()); // will be 1 or 0 - // utils.logInfo('_ozoneInternal is', _ozoneInternal); // return the single request object OR the array: if (singleRequest) { utils.logInfo('OZONE: buildRequests starting to generate response for a single request'); @@ -177,7 +188,6 @@ export const spec = { utils.logInfo('OZONE: 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('OZONE: buildRequests starting to generate non-single response, working on imp : ', imp); @@ -201,67 +211,65 @@ export const spec = { /** * 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 + * + * Updated April 2019 to return all bids, not just the one we decide is the 'winner' + * * @param serverResponse * @param request * @returns {*} */ interpretResponse(serverResponse, request) { - utils.logInfo('OZONE: version' + 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('OZONE: 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('OZONE: 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('OZONE: 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); + if (!serverResponse.hasOwnProperty('seatbid')) { return []; } + if (typeof serverResponse.seatbid !== 'object') { return []; } + let arrAllBids = []; + serverResponse.seatbid = injectAdIdsIntoAllBidResponses(serverResponse.seatbid); // we now make sure that each bid in the bidresponse has a unique (within page) adId attribute. + for (let i = 0; i < serverResponse.seatbid.length; i++) { + let sb = serverResponse.seatbid[i]; + const {defaultWidth, defaultHeight} = defaultSize(request.bidderRequest.bids[i]); + for (let j = 0; j < sb.bid.length; j++) { + let thisBid = ozoneAddStandardProperties(sb.bid[j], defaultWidth, defaultHeight); - winningBid.adserverTargeting = adserverTargeting; - utils.logInfo('OZONE: winner is', winningBid); - arrWinners.push(winningBid); - utils.logInfo('OZONE: arrWinners is', arrWinners); + // from https://github.com/prebid/Prebid.js/pull/1082 + if (utils.deepAccess(thisBid, 'ext.prebid.type') === VIDEO) { + utils.logInfo('OZONE: going to attach a renderer to:', j); + let renderConf = createObjectForInternalVideoRender(thisBid); + thisBid.renderer = Renderer.install(renderConf); + } else { + utils.logInfo('OZONE: bid is not a video, will not attach a renderer: ', j); } - let winnersClean = arrWinners.filter(w => { - return (w.bidId); // will be cast to boolean + + let ozoneInternalKey = thisBid.bidId; + let adserverTargeting = {}; + // all keys for all bidders for this bid have to be added to all objects returned, else some keys will not be sent to ads? + let allBidsForThisBidid = ozoneGetAllBidsForBidId(ozoneInternalKey, serverResponse.seatbid); + // add all the winning & non-winning bids for this bidId: + utils.logInfo('OZONE: Going to iterate allBidsForThisBidId', allBidsForThisBidid); + Object.keys(allBidsForThisBidid).forEach(function(bidderName, index, ar2) { + 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); + adserverTargeting['oz_' + bidderName + '_adId'] = String(allBidsForThisBidid[bidderName].adId); + adserverTargeting['oz_' + bidderName + '_pb_r'] = getRoundedBid(allBidsForThisBidid[bidderName].price, allBidsForThisBidid[bidderName].ext.prebid.type); + if (allBidsForThisBidid[bidderName].hasOwnProperty('dealid')) { + adserverTargeting['oz_' + bidderName + '_dealid'] = String(allBidsForThisBidid[bidderName].dealid); + } }); - utils.logInfo('OZONE: going to return winnersClean:', winnersClean); - return winnersClean; - } else { - return []; + // also add in the winning bid, to be sent to dfp + let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(ozoneInternalKey, serverResponse.seatbid); + 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); + adserverTargeting['oz_pb_v'] = OZONEVERSION; + thisBid.adserverTargeting = adserverTargeting; + arrAllBids.push(thisBid); } - } else { - return []; } + return arrAllBids; }, getUserSyncs(optionsType, serverResponse) { if (!serverResponse || serverResponse.length === 0) { @@ -275,6 +283,21 @@ export const spec = { } } } +/** + * add a page-level-unique adId element to all server response bids. + * NOTE that this is distructive - it mutates the serverResponse object sent in as a parameter + * @param seatbid object (serverResponse.seatbid) + * @returns seatbid object + */ +export function injectAdIdsIntoAllBidResponses(seatbid) { + for (let i = 0; i < seatbid.length; i++) { + let sb = seatbid[i]; + for (let j = 0; j < sb.bid.length; j++) { + sb.bid[j]['adId'] = sb.bid[j]['impid'] + '-' + i; // modify the bidId per-bid, so each bid has a unique adId within this response, and dfp can select one. + } + } + return seatbid; +} export function checkDeepArray(Arr) { if (Array.isArray(Arr)) { if (Array.isArray(Arr[0])) { @@ -300,15 +323,14 @@ export function defaultSize(thebidObj) { * @param serverResponseSeatBid * @returns {*} bid object */ -export function ozoneGetWinnerForRequestBid(requestBid, serverResponseSeatBid) { +export function ozoneGetWinnerForRequestBid(requestBidId, 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 (theseBids[k].impid === requestBidId) { // we've found a matching server response bid for this request bid if ((thisBidWinner == null) || (thisBidWinner.price < theseBids[k].price)) { thisBidWinner = theseBids[k]; winningSeat = thisSeat; @@ -327,22 +349,88 @@ export function ozoneGetWinnerForRequestBid(requestBid, serverResponseSeatBid) { * @returns {} = {ozone:{obj}, appnexus:{obj}, ... } */ export function ozoneGetAllBidsForBidId(matchBidId, serverResponseSeatBid) { - utils.logInfo('OZONE: 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('OZONE: ozoneGetAllBidsForBidId - going to return: ', objBids); return objBids; } +/** + * Round the bid price down according to the granularity + * @param price + * @param mediaType = video, banner or native + */ +export function getRoundedBid(price, mediaType) { + const mediaTypeGranularity = config.getConfig(`mediaTypePriceGranularity.${mediaType}`); // might be string or object or nothing; if set then this takes precedence over 'priceGranularity' + let objBuckets = config.getConfig('customPriceBucket'); // this is always an object - {} if strBuckets is not 'custom' + let strBuckets = config.getConfig('priceGranularity'); // priceGranularity value, always a string ** if priceGranularity is set to an object then it's always 'custom' + let theConfigObject = getGranularityObject(mediaType, mediaTypeGranularity, strBuckets, objBuckets); + let theConfigKey = getGranularityKeyName(mediaType, mediaTypeGranularity, strBuckets); + + utils.logInfo('getRoundedBid. price:', price, 'mediaType:', mediaType, 'configkey:', theConfigKey, 'configObject:', theConfigObject, 'mediaTypeGranularity:', mediaTypeGranularity, 'strBuckets:', strBuckets); + + let priceStringsObj = getPriceBucketString( + price, + theConfigObject, + config.getConfig('currency.granularityMultiplier') + ); + utils.logInfo('priceStringsObj', priceStringsObj); + // by default, without any custom granularity set, you get granularity name : 'medium' + let granularityNamePriceStringsKeyMapping = { + 'medium': 'med', + 'custom': 'custom', + 'high': 'high', + 'low': 'low', + 'dense': 'dense' + }; + if (granularityNamePriceStringsKeyMapping.hasOwnProperty(theConfigKey)) { + let priceStringsKey = granularityNamePriceStringsKeyMapping[theConfigKey]; + utils.logInfo('OZONE: looking for priceStringsKey:', priceStringsKey); + return priceStringsObj[priceStringsKey]; + } + return priceStringsObj['auto']; +} + +/** + * return the key to use to get the value out of the priceStrings object, taking into account anything at + * config.priceGranularity level or config.mediaType.xxx level + * I've noticed that the key specified by prebid core : config.getConfig('priceGranularity') does not properly + * take into account the 2-levels of config + */ +export function getGranularityKeyName(mediaType, mediaTypeGranularity, strBuckets) { + if (typeof mediaTypeGranularity === 'string') { + return mediaTypeGranularity; + } + if (typeof mediaTypeGranularity === 'object') { + return 'custom'; + } + if (typeof strBuckets === 'string') { + return strBuckets; + } + return 'auto'; // fall back to a default key - should literally never be needed though. +} + +/** + * return the object to use to create the custom value of the priceStrings object, taking into account anything at + * config.priceGranularity level or config.mediaType.xxx level + */ +export function getGranularityObject(mediaType, mediaTypeGranularity, strBuckets, objBuckets) { + if (typeof mediaTypeGranularity === 'object') { + return mediaTypeGranularity; + } + if (strBuckets === 'custom') { + return objBuckets; + } + return ''; +} + /** * We expect to be able to find a standard set of properties on winning bid objects; add them here. * @param seatBid @@ -379,5 +467,37 @@ export function getTestQuerystringValue() { return 0; } +/** + * Generate a random number per ad; I'll use the current ms timestamp, then append 8 random alpha/numeric characters + * Randomness : 1 in 208 billion random combinations per-millisecond, non-repeating sequence. + * + * @returns {*} + */ +export function pgGuid() { + return new Date().getTime() + 'xxxxxxxx'.replace(/x/g, function(c) { + return Math.round((Math.random() * 36)).toString(36); + }); +} + +function createObjectForInternalVideoRender(bid) { + let obj = { + url: OZONE_RENDERER_URL, + callback: () => onOutstreamRendererLoaded(bid) + } + return obj; +} + +function onOutstreamRendererLoaded(bid) { + try { + bid.renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err) + } +} + +function outstreamRender(bid) { + window.ozoneVideo.outstreamRender(bid); +} + registerBidder(spec); utils.logInfo('OZONE: ozoneBidAdapter ended'); diff --git a/modules/ozoneBidAdapter.md b/modules/ozoneBidAdapter.md index c013a4d558c..89760697088 100644 --- a/modules/ozoneBidAdapter.md +++ b/modules/ozoneBidAdapter.md @@ -1,4 +1,4 @@ - + # Overview ``` @@ -12,7 +12,7 @@ Maintainer: engineering@ozoneproject.com Module that connects to the Ozone Project's demand source(s). -The Ozone Project bid adapter supports Banner mediaTypes ONLY. +The Ozone Project bid adapter supports Banner and Outstream Video mediaTypes ONLY. # Test Parameters @@ -36,9 +36,43 @@ adUnits = [{ publisherId: 'OZONENUK0001', /* an ID to identify the publisher account - required */ siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ placementId: '0420420421', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ - customData: {"key1": "value1", "key2": "value2}, /* optional JSON placeholder for passing publisher key-values for targeting. */ + customData": [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"],}}] /* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ + ozoneData: {"key1": "value1", "key2": "value2"}, /* optional JSON placeholder for for passing ozone project key-values for targeting. */ + lotameData: {"key1": "value1", "key2": "value2"} /* optional JSON placeholder for passing Lotame DMP data */ + } + }] + }]; +``` + + +``` + +//Outstream Video adUnit + +adUnits = [{ + code: 'id-of-your-video-div', + mediaTypes: { + video: { + playerSize: [640, 480], + mimes: ['video/mp4'], + context: 'outstream', + } + }, + bids: [{ + bidder: 'ozone', + params: { + publisherId: 'OZONENUK0001', /* an ID to identify the publisher account - required */ + siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ + customData: [{"settings": {}, "targeting": { "key": "value", "key2": ["value1", "value2"]}}] + placementId: '0440440442', /* an ID used to identify the piece of inventory - required - for unruly test use 0440440442. */ + customData": [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"],}}] /* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ ozoneData: {"key1": "value1", "key2": "value2"}, /* optional JSON placeholder for for passing ozone project key-values for targeting. */ - lotameData: {"key1": "value1", "key2": "value2} /* optional JSON placeholder for passing Lotame DMP data */ + lotameData: {"key1": "value1", "key2": "value2"}, /* optional JSON placeholder for passing Lotame DMP data */ + video: { + skippable: true, /* optional */ + playback_method: ['auto_play_sound_off'], /* optional */ + targetDiv: 'some-different-div-id-to-my-adunitcode' /* optional */ + } } }] }]; diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index a910ef391b0..b0f252d4469 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/ozoneBidAdapter'; import { config } from 'src/config'; +import {Renderer} from '../../../src/Renderer'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; /* @@ -63,7 +64,7 @@ var validBidRequestsWithBannerMediaType = [ transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; -var validBidRequestsWithNonBannerMediaTypes = [ +var validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', auctionId: '27dcb421-95c6-4024-a624-3c03816c5f99', @@ -72,8 +73,8 @@ var validBidRequestsWithNonBannerMediaTypes = [ 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: {video: {info: 'dummy data'}, native: {info: 'dummy data'}}, + 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, video: {skippable: true, playback_method: ['auto_play_sound_off'], targetDiv: 'some-different-div-id-to-my-adunitcode'} } ] }, + mediaTypes: {video: {mimes: ['video/mp4'], 'context': 'outstream'}, native: {info: 'dummy data'}}, transactionId: '2e63c0ed-b10c-4008-aed5-84582cecfe87' } ]; @@ -154,6 +155,67 @@ var validResponse = { } }, 'headers': {} +}; +var validOutstreamResponse = { + 'body': { + 'id': 'd6198807-7a53-4141-b2db-d2cb754d68ba', + 'seatbid': [ + { + 'bid': [ + { + 'id': '677903815252395017', + 'impid': '2899ec066a91ff8', + 'price': 0.5, + 'adm': '', + 'adid': '98493581', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://fra1-ib.adnxs.com/cr?id=98493581', + 'cid': '9325', + 'crid': '98493581', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 600, + 'ext': { + 'prebid': { + 'type': 'video' + }, + 'bidder': { + 'unruly': { + 'renderer': { + 'config': { + 'targetingUUID': 'aafd3388-afaf-41f4-b271-0ac8e0325a7f', + 'siteId': 1052815, + 'featureOverrides': {} + }, + 'url': 'https://video.unrulymedia.com/native/native-loader.js#supplyMode=prebid?cb=6284685353877994', + 'id': 'unruly_inarticle' + }, + 'vast_url': 'data:text/xml;base64,PD94bWwgdmVyc2lvbj0i' + } + } + } + } + ], + 'seat': 'unruly' + } + ], + 'ext': { + 'responsetimemillis': { + 'appnexus': 47, + 'openx': 30 + } + }, + 'timing': { + 'start': 1536848078.089177, + 'end': 1536848078.142203, + 'TimeTaken': 0.05302619934082031 + } + }, + 'headers': {} } describe('ozone Adapter', function () { @@ -463,6 +525,43 @@ describe('ozone Adapter', function () { it('should not validate lotameData being sent', function () { expect(spec.isBidRequestValid(xBadLotame)).to.equal(false); }); + + var xBadVideoContext = { + bidder: BIDDER_CODE, + params: { + 'placementId': '1234567890', + 'publisherId': '9876abcd12-3', + 'lotameData': 'this should be an object', + siteId: '1234567890' + }, + mediaTypes: { + video: { + mimes: ['video/mp4'], + 'context': 'instream'}, + } + }; + + it('should not validate video instream being sent', function () { + expect(spec.isBidRequestValid(xBadVideoContext)).to.equal(false); + }); + + let validVideoBidReq = { + bidder: BIDDER_CODE, + params: { + placementId: '1310000099', + publisherId: '9876abcd12-3', + siteId: '1234567890' + }, + mediaTypes: { + video: { + mimes: ['video/mp4'], + 'context': 'outstream'}, + } + }; + + it('should not validate video instream being sent', function () { + expect(spec.isBidRequestValid(validVideoBidReq)).to.equal(true); + }); }); describe('buildRequests', function () { @@ -509,8 +608,8 @@ describe('ozone Adapter', function () { expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); - it('handles missing banner mediaType element correctly', function () { - const request = spec.buildRequests(validBidRequestsWithNonBannerMediaTypes, validBidderRequest); + it('handles video mediaType element correctly, with outstream video', function () { + const request = spec.buildRequests(validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo, validBidderRequest); expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); }); @@ -584,6 +683,13 @@ describe('ozone Adapter', function () { expect(result).to.be.an('array'); expect(result).to.be.empty; }); + + it('should have video renderer', function () { + const request = spec.buildRequests(validBidRequestsWithNonBannerMediaTypesAndValidOutstreamVideo, validBidderRequest); + const result = spec.interpretResponse(validOutstreamResponse, request); + const bid = result[0]; + expect(bid.renderer).to.be.an.instanceOf(Renderer); + }); }); describe('userSyncs', function () {