diff --git a/modules/categoryTranslation.js b/modules/categoryTranslation.js index 57267095ff0..ba73aa01b85 100644 --- a/modules/categoryTranslation.js +++ b/modules/categoryTranslation.js @@ -34,13 +34,13 @@ export const registerAdserver = hook('async', function(adServer) { }, 'registerAdserver'); registerAdserver(); -export const getAdserverCategoryHook = timedBidResponseHook('categoryTranslation', function getAdserverCategoryHook(fn, adUnitCode, bid) { +export const getAdserverCategoryHook = timedBidResponseHook('categoryTranslation', function getAdserverCategoryHook(fn, adUnitCode, bid, reject) { if (!bid) { - return fn.call(this, adUnitCode); // if no bid, call original and let it display warnings + return fn.call(this, adUnitCode, bid, reject); // if no bid, call original and let it display warnings } if (!config.getConfig('adpod.brandCategoryExclusion')) { - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); } let localStorageKey = (config.getConfig('brandCategoryTranslation.translationFile')) ? DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB : DEFAULT_IAB_TO_FW_MAPPING_KEY; @@ -63,7 +63,7 @@ export const getAdserverCategoryHook = timedBidResponseHook('categoryTranslation logError('Translation mapping data not found in local storage'); } } - fn.call(this, adUnitCode, bid); + fn.call(this, adUnitCode, bid, reject); }); export function initTranslation(url, localStorageKey) { diff --git a/modules/currency.js b/modules/currency.js index f0ad6681afa..2c8070bcb08 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -1,10 +1,9 @@ -import { logInfo, logWarn, logError, logMessage } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { createBid } from '../src/bidfactory.js'; +import {logError, logInfo, logMessage, logWarn} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; @@ -181,9 +180,9 @@ function resetCurrency() { bidderCurrencyDefault = {}; } -export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid) { +export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) { if (!bid) { - return fn.call(this, adUnitCode); // if no bid, call original and let it display warnings + return fn.call(this, adUnitCode, bid, reject); // if no bid, call original and let it display warnings } let bidder = bid.bidderCode || bid.bidder; @@ -209,10 +208,10 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB // execute immediately if the bid is already in the desired currency if (bid.currency === adServerCurrency) { - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); } - bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid])); + bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject])); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); } else { @@ -239,7 +238,8 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('Returning NO_BID, getCurrencyConversion threw error: ', e); - params[1] = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); + // TODO: in v8, this should not continue with a "NO_BID" + params[1] = params[2](CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); } } return fn.apply(context, params); diff --git a/modules/dchain.js b/modules/dchain.js index 79e20520e93..daf97a7551f 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -109,7 +109,7 @@ function isValidDchain(bid) { } } -export const addBidResponseHook = timedBidResponseHook('dchain', function addBidResponseHook(fn, adUnitCode, bid) { +export const addBidResponseHook = timedBidResponseHook('dchain', function addBidResponseHook(fn, adUnitCode, bid, reject) { const basicDchain = { ver: '1.0', complete: 0, @@ -140,7 +140,7 @@ export const addBidResponseHook = timedBidResponseHook('dchain', function addBid bid.meta.dchain = basicDchain; } - fn(adUnitCode, bid); + fn(adUnitCode, bid, reject); }); export function init() { diff --git a/modules/debugging/legacy.js b/modules/debugging/legacy.js index 15b05cded64..e83b99c5194 100644 --- a/modules/debugging/legacy.js +++ b/modules/debugging/legacy.js @@ -54,7 +54,7 @@ export function applyBidOverrides(overrideObj, bidObj, bidType, logger) { }, bidObj); } -export function addBidResponseHook(next, adUnitCode, bid) { +export function addBidResponseHook(next, adUnitCode, bid, reject) { const {overrides, logger} = this; if (bidderExcluded(overrides.bidders, bid.bidderCode)) { @@ -70,7 +70,7 @@ export function addBidResponseHook(next, adUnitCode, bid) { }); } - next(adUnitCode, bid); + next(adUnitCode, bid, reject); } export function addBidderRequestsHook(next, bidderRequests) { diff --git a/modules/mass.js b/modules/mass.js index 6a01f06bd90..113fdce8d4f 100644 --- a/modules/mass.js +++ b/modules/mass.js @@ -78,7 +78,7 @@ export function updateRenderers() { /** * Before hook for 'addBidResponse'. */ -export const addBidResponseHook = timedBidResponseHook('mass', function addBidResponseHook(next, adUnitCode, bid, {index = auctionManager.index} = {}) { +export const addBidResponseHook = timedBidResponseHook('mass', function addBidResponseHook(next, adUnitCode, bid, reject, {index = auctionManager.index} = {}) { let renderer; for (let i = 0; i < renderers.length; i++) { if (renderers[i].match(bid)) { @@ -104,7 +104,7 @@ export const addBidResponseHook = timedBidResponseHook('mass', function addBidRe addListenerOnce(); } - next(adUnitCode, bid); + next(adUnitCode, bid, reject); }); /** diff --git a/modules/multibid/index.js b/modules/multibid/index.js index fb28be83fa8..76f4ede8f8e 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -99,7 +99,7 @@ export function adjustBidderRequestsHook(fn, bidderRequests) { * @param {String} ad unit code for bid * @param {Object} bid object */ -export const addBidResponseHook = timedBidResponseHook('multibid', function addBidResponseHook(fn, adUnitCode, bid) { +export const addBidResponseHook = timedBidResponseHook('multibid', function addBidResponseHook(fn, adUnitCode, bid, reject) { let floor = deepAccess(bid, 'floorData.floorValue'); if (!config.getConfig('multibid')) resetMultiConfig(); @@ -107,7 +107,7 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB // Else checks if multiconfig exists and bid bidderCode exists within config // Else continue with no modifications if (hasMultibid && multiConfig[bid.bidderCode] && deepAccess(bid, 'video.context') === 'adpod') { - fn.call(this, adUnitCode, bid); + fn.call(this, adUnitCode, bid, reject); } else if (hasMultibid && multiConfig[bid.bidderCode]) { // Set property multibidPrefix on bid if (multiConfig[bid.bidderCode].prefix) bid.multibidPrefix = multiConfig[bid.bidderCode].prefix; @@ -127,7 +127,7 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB if (multiConfig[bid.bidderCode].prefix) bid.targetingBidder = multiConfig[bid.bidderCode].prefix + length; if (length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; - fn.call(this, adUnitCode, bid); + fn.call(this, adUnitCode, bid, reject); } else { logWarn(`Filtering multibid received from bidder ${bid.bidderCode}: ` + ((multibidUnits[adUnitCode][bid.bidderCode].maxReached) ? `Maximum bid limit reached for ad unit code ${adUnitCode}` : 'Bid cpm under floors value.')); } @@ -137,10 +137,10 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB deepSetValue(multibidUnits, [adUnitCode, bid.bidderCode], {ads: [bid]}); if (multibidUnits[adUnitCode][bid.bidderCode].ads.length === multiConfig[bid.bidderCode].maxbids) multibidUnits[adUnitCode][bid.bidderCode].maxReached = true; - fn.call(this, adUnitCode, bid); + fn.call(this, adUnitCode, bid, reject); } } else { - fn.call(this, adUnitCode, bid); + fn.call(this, adUnitCode, bid, reject); } }); diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 5bdf9868d24..856b962e8a0 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -950,10 +950,6 @@ Object.assign(ORTB2.prototype, { (seatbid.bid || []).forEach(bid => { let bidRequest = this.getBidRequest(bid.impid, seatbid.seat); if (bidRequest == null) { - if (!s2sConfig.allowUnknownBidderCodes) { - logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`); - return; - } // for stored impression, a request was made with bidder code `null`. Pick it up here so that NO_BID, BID_WON, etc events // can work as expected (otherwise, the original request will always result in NO_BID). bidRequest = this.getBidRequest(bid.impid, null); @@ -968,6 +964,7 @@ Object.assign(ORTB2.prototype, { transactionId: this.adUnitsByImp[bid.impid].transactionId, auctionId: this.auctionId, }); + bidObject.requestBidder = bidRequest?.bidder; bidObject.requestTimestamp = this.requestTimestamp; bidObject.cpm = cpm; if (bid?.ext?.prebid?.meta?.adaptercode) { @@ -1158,8 +1155,15 @@ export function PrebidServer() { onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); metrics.checkpoint('addBidResponse'); - if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnit, bid))) { - addBidResponse(adUnit, bid); + if ((bid.requestId == null || bid.requestBidder == null) && !s2sBidRequest.s2sConfig.allowUnknownBidderCodes) { + logWarn(`PBS adapter received bid from unknown bidder (${bid.bidder}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`); + addBidResponse.reject(adUnit, bid, CONSTANTS.REJECTION_REASON.BIDDER_DISALLOWED); + } else { + if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnit, bid))) { + addBidResponse(adUnit, bid); + } else { + addBidResponse.reject(adUnit, bid, CONSTANTS.REJECTION_REASON.INVALID); + } } } }) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index e4203fd73d3..5aee87d474e 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -20,7 +20,6 @@ import {ajaxBuilder} from '../src/ajax.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {getHook} from '../src/hook.js'; -import {createBid} from '../src/bidfactory.js'; import {find} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {bidderSettings} from '../src/bidderSettings.js'; @@ -694,11 +693,11 @@ function shouldFloorBid(floorData, floorInfo, bid) { * @summary The main driving force of floors. On bidResponse we hook in and intercept bidResponses. * And if the rule we find determines a bid should be floored we will do so. */ -export const addBidResponseHook = timedBidResponseHook('priceFloors', function addBidResponseHook(fn, adUnitCode, bid) { +export const addBidResponseHook = timedBidResponseHook('priceFloors', function addBidResponseHook(fn, adUnitCode, bid, reject) { let floorData = _floorDataForAuction[bid.auctionId]; // if no floor data then bail if (!floorData || !bid || floorData.skipped) { - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); } const matchingBidRequest = auctionManager.index.getBidRequest(bid); @@ -708,7 +707,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a if (!floorInfo.matchingFloor) { logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); } // determine the base cpm to use based on if the currency matches the floor currency @@ -724,7 +723,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a adjustedCpm = getGlobal().convertCurrency(bid.cpm, bidResponseCurrency.toUpperCase(), floorCurrency); } catch (err) { logError(`${MODULE_NAME}: Unable do get currency conversion for bidResponse to Floor Currency. Do you have Currency module enabled? ${bid}`); - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); } } @@ -737,25 +736,12 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a // now do the compare! if (shouldFloorBid(floorData, floorInfo, bid)) { // bid fails floor -> throw it out - // create basic bid no-bid with necessary data fro analytics adapters - let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); - Object.assign(flooredBid, pick(bid, [ - 'floorData', - 'width', - 'height', - 'mediaType', - 'currency', - 'originalCpm', - 'originalCurrency', - 'getCpmInNewCurrency', - ])); - flooredBid.status = CONSTANTS.BID_STATUS.BID_REJECTED; - // if floor not met update bid with 0 cpm so it is not included downstream and marked as no-bid - flooredBid.cpm = 0; + // continue with a "NO_BID" bid, TODO: remove this in v8 + const flooredBid = reject(CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET); logWarn(`${MODULE_NAME}: ${flooredBid.bidderCode}'s Bid Response for ${adUnitCode} was rejected due to floor not met (adjusted cpm: ${bid?.floorData?.cpmAfterAdjustments}, floor: ${floorInfo?.matchingFloor})`, bid); - return fn.call(this, adUnitCode, flooredBid); + return fn.call(this, adUnitCode, flooredBid, reject); } - return fn.call(this, adUnitCode, bid); + return fn.call(this, adUnitCode, bid, reject); }); config.getConfig('floors', config => handleSetFloorsConfig(config.floors)); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index c60f4b8a015..d0b716d656d 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -202,6 +202,8 @@ export function newBidder(spec) { adUnitCodesHandled[adUnitCode] = true; if (metrics.measureTime('addBidResponse.validate', () => isValid(adUnitCode, bid))) { addBidResponse(adUnitCode, bid); + } else { + addBidResponse.reject(adUnitCode, bid, CONSTANTS.REJECTION_REASON.INVALID) } } @@ -262,6 +264,7 @@ export function newBidder(spec) { bid.adapterCode = bidRequest.bidder; if (isInvalidAlternateBidder(bid.bidderCode, bidRequest.bidder)) { logWarn(`${bid.bidderCode} is not a registered partner or known bidder of ${bidRequest.bidder}, hence continuing without bid. If you wish to support this bidder, please mark allowAlternateBidderCodes as true in bidderSettings.`); + addBidResponse.reject(bidRequest.adUnitCode, bid, CONSTANTS.REJECTION_REASON.BIDDER_DISALLOWED) return; } // creating a copy of original values as cpm and currency are modified later @@ -272,6 +275,7 @@ export function newBidder(spec) { addBidWithCode(bidRequest.adUnitCode, prebidBid); } else { logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); + addBidResponse.reject(null, bid, CONSTANTS.REJECTION_REASON.INVALID_REQUEST_ID); } }, onCompletion: afterAllResponses, @@ -541,24 +545,22 @@ export function getIabSubCategory(bidderCode, category) { // check that the bid has a width and height set function validBidSize(adUnitCode, bid, {index = auctionManager.index} = {}) { - if ((bid.width || parseInt(bid.width, 10) === 0) && (bid.height || parseInt(bid.height, 10) === 0)) { - bid.width = parseInt(bid.width, 10); - bid.height = parseInt(bid.height, 10); - return true; - } - const bidRequest = index.getBidRequest(bid); const mediaTypes = index.getMediaTypes(bid); const sizes = (bidRequest && bidRequest.sizes) || (mediaTypes && mediaTypes.banner && mediaTypes.banner.sizes); - const parsedSizes = parseSizesInput(sizes); + const parsedSizes = parseSizesInput(sizes).map(sz => sz.split('x').map(n => parseInt(n, 10))); + + if ((bid.width || parseInt(bid.width, 10) === 0) && (bid.height || parseInt(bid.height, 10) === 0)) { + bid.width = parseInt(bid.width, 10); + bid.height = parseInt(bid.height, 10); + return parsedSizes.length === 0 || parsedSizes.some(([w, h]) => bid.width === w && bid.height === h); + } // if a banner impression has one valid size, we assign that size to any bid // response that does not explicitly set width or height if (parsedSizes.length === 1) { - const [ width, height ] = parsedSizes[0].split('x'); - bid.width = parseInt(width, 10); - bid.height = parseInt(height, 10); + ([bid.width, bid.height] = parsedSizes[0]); return true; } @@ -600,7 +602,7 @@ export function isValid(adUnitCode, bid, {index = auctionManager.index} = {}) { return false; } if (bid.mediaType === 'banner' && !validBidSize(adUnitCode, bid, {index})) { - logError(errorMessage(`Banner bids require a width and height`)); + logError(errorMessage(`Banner bids require a width and height that match one of the requested sizes`)); return false; } diff --git a/src/auction.js b/src/auction.js index 81280e0833c..26561d87d71 100644 --- a/src/auction.js +++ b/src/auction.js @@ -58,26 +58,41 @@ */ import { - flatten, timestamp, adUnitsFilter, deepAccess, getValue, parseUrl, generateUUID, - logMessage, bind, logError, logInfo, logWarn, isEmpty, _each, isFn, isEmptyStr + _each, + adUnitsFilter, + bind, + deepAccess, + flatten, + generateUUID, + getValue, + isEmpty, + isEmptyStr, + isFn, + logError, + logInfo, + logMessage, + logWarn, + parseUrl, + timestamp } from './utils.js'; -import { getPriceBucketString } from './cpmBucketManager.js'; -import { getNativeTargeting } from './native.js'; -import { getCacheUrl, store } from './videoCache.js'; -import { Renderer } from './Renderer.js'; -import { config } from './config.js'; -import { userSync } from './userSync.js'; -import { hook } from './hook.js'; +import {getPriceBucketString} from './cpmBucketManager.js'; +import {getNativeTargeting} from './native.js'; +import {getCacheUrl, store} from './videoCache.js'; +import {Renderer} from './Renderer.js'; +import {config} from './config.js'; +import {userSync} from './userSync.js'; +import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; -import { OUTSTREAM } from './video.js'; -import { VIDEO } from './mediaTypes.js'; +import {OUTSTREAM} from './video.js'; +import {VIDEO} from './mediaTypes.js'; import {auctionManager} from './auctionManager.js'; import {bidderSettings} from './bidderSettings.js'; -import * as events from './events.js' +import * as events from './events.js'; import adapterManager from './adapterManager.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; +import {createBid} from './bidfactory.js'; const { syncUsers } = userSync; @@ -119,24 +134,26 @@ export function resetAuctionState() { */ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, auctionId, ortb2Fragments, metrics}) { metrics = useMetrics(metrics); - let _adUnits = adUnits; - let _labels = labels; - let _adUnitCodes = adUnitCodes; + const _adUnits = adUnits; + const _labels = labels; + const _adUnitCodes = adUnitCodes; + const _auctionId = auctionId || generateUUID(); + const _timeout = cbTimeout; + const _timelyBidders = new Set(); + let _bidsRejected = []; + let _callback = callback; let _bidderRequests = []; let _bidsReceived = []; let _noBids = []; + let _winningBids = []; let _auctionStart; let _auctionEnd; - let _auctionId = auctionId || generateUUID(); - let _auctionStatus; - let _callback = callback; let _timer; - let _timeout = cbTimeout; - let _winningBids = []; - let _timelyBidders = new Set(); + let _auctionStatus; function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests); } function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); } + function addBidRejected(bidsRejected) { _bidsRejected = _bidsRejected.concat(bidsRejected); } function addNoBid(noBid) { _noBids = _noBids.concat(noBid); } function getProperties() { @@ -151,6 +168,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a bidderRequests: _bidderRequests, noBids: _noBids, bidsReceived: _bidsReceived, + bidsRejected: _bidsRejected, winningBids: _winningBids, timeout: _timeout, metrics: metrics @@ -352,6 +370,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a return { addBidReceived, + addBidRejected, addNoBid, executeCallback, callBids, @@ -372,7 +391,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a }; } -export const addBidResponse = hook('sync', function(adUnitCode, bid) { +/** + * Hook into this to intercept bids before they are added to an auction. + * + * @param adUnitCode + * @param bid + * @param {function(String)} reject: a function that, when called, rejects `bid` with the given reason. + */ +export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { this.dispatch.call(null, adUnitCode, bid); }, 'addBidResponse'); @@ -425,20 +451,49 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM } } - function handleBidResponse(adUnitCode, bid) { + function handleBidResponse(adUnitCode, bid, handler) { bidResponseMap[bid.requestId] = true; - + addCommonResponseProperties(bid, adUnitCode) outstandingBidsAdded++; - let auctionId = auctionInstance.getAuctionId(); + return handler(afterBidAdded); + } - let bidResponse = getPreparedBidForAuction({adUnitCode, bid, auctionId}); + function acceptBidResponse(adUnitCode, bid) { + handleBidResponse(adUnitCode, bid, (done) => { + let bidResponse = getPreparedBidForAuction(bid); - if (bidResponse.mediaType === 'video') { - tryAddVideoBid(auctionInstance, bidResponse, afterBidAdded); - } else { - addBidToAuction(auctionInstance, bidResponse); - afterBidAdded(); - } + if (bidResponse.mediaType === 'video') { + tryAddVideoBid(auctionInstance, bidResponse, done); + } else { + addBidToAuction(auctionInstance, bidResponse); + done(); + } + }); + } + + function rejectBidResponse(adUnitCode, bid, reason) { + return handleBidResponse(adUnitCode, bid, (done) => { + // return a "NO_BID" replacement that the caller can decide to continue with + // TODO: remove this in v8; see https://github.com/prebid/Prebid.js/issues/8956 + const noBid = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); + Object.assign(noBid, Object.fromEntries(Object.entries(bid).filter(([k]) => !noBid.hasOwnProperty(k) && ![ + 'ad', + 'adUrl', + 'vastXml', + 'vastUrl', + 'native', + ].includes(k)))); + noBid.status = CONSTANTS.BID_STATUS.BID_REJECTED; + noBid.cpm = 0; + + bid.rejectionReason = reason; + logWarn(`Bid from ${bid.bidder || 'unknown bidder'} was rejected: ${reason}`, bid) + events.emit(CONSTANTS.EVENTS.BID_REJECTED, bid); + auctionInstance.addBidRejected(bid); + done(); + + return noBid; + }) } function adapterDone() { @@ -470,12 +525,24 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM } return { - addBidResponse: function (adUnit, bid) { - const bidderRequest = index.getBidderRequest(bid); - waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({ - dispatch: handleBidResponse, - }, adUnit, bid)); - }, + addBidResponse: (function () { + function addBid(adUnitCode, bid) { + const bidderRequest = index.getBidderRequest(bid); + waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({ + dispatch: acceptBidResponse, + }, adUnitCode, bid, (() => { + let rejection; + return (reason) => { + if (rejection == null) { + rejection = rejectBidResponse(adUnitCode, bid, reason); + } + return rejection; + } + })())); + } + addBid.reject = rejectBidResponse; + return addBid; + })(), adapterDone: function () { guard(this, adapterDone.bind(this)) } @@ -492,9 +559,7 @@ export function doCallbacksIfTimedout(auctionInstance, bidResponse) { export function addBidToAuction(auctionInstance, bidResponse) { setupBidTargeting(bidResponse); - const metrics = useMetrics(bidResponse.metrics); - metrics.timeSince('addBidResponse', 'addBidResponse.total'); - + useMetrics(bidResponse.metrics).timeSince('addBidResponse', 'addBidResponse.total'); events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); auctionInstance.addBidReceived(bidResponse); @@ -595,35 +660,41 @@ export const callPrebidCache = hook('async', function(auctionInstance, bidRespon batchAndStore(auctionInstance, bidResponse, afterBidAdded); }, 'callPrebidCache'); -// Postprocess the bids so that all the universal properties exist, no matter which bidder they came from. -// This should be called before addBidToAuction(). -function getPreparedBidForAuction({adUnitCode, bid, auctionId}, {index = auctionManager.index} = {}) { - const bidderRequest = index.getBidderRequest(bid); - const start = (bidderRequest && bidderRequest.start) || bid.requestTimestamp; - - let bidObject = Object.assign({}, bid, { - auctionId, - responseTimestamp: timestamp(), - requestTimestamp: start, - cpm: parseFloat(bid.cpm) || 0, - bidder: bid.bidderCode, +/** + * Augment `bidResponse` with properties that are common across all bids - including rejected bids. + * + */ +function addCommonResponseProperties(bidResponse, adUnitCode, {index = auctionManager.index} = {}) { + const bidderRequest = index.getBidderRequest(bidResponse); + const start = (bidderRequest && bidderRequest.start) || bidResponse.requestTimestamp; + + Object.assign(bidResponse, { + responseTimestamp: bidResponse.responseTimestamp || timestamp(), + requestTimestamp: bidResponse.requestTimestamp || start, + cpm: bidResponse.cpm || parseFloat(bidResponse.cpm) || 0, + bidder: bidResponse.bidder || bidResponse.bidderCode, adUnitCode }); - bidObject.timeToRespond = bidObject.responseTimestamp - bidObject.requestTimestamp; + bidResponse.timeToRespond = bidResponse.responseTimestamp - bidResponse.requestTimestamp; +} +/** + * Add additional bid response properties that are universal for all _accepted_ bids. + */ +function getPreparedBidForAuction(bid, {index = auctionManager.index} = {}) { // Let listeners know that now is the time to adjust the bid, if they want to. // // CAREFUL: Publishers rely on certain bid properties to be available (like cpm), // but others to not be set yet (like priceStrings). See #1372 and #1389. - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); + events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bid); // a publisher-defined renderer can be used to render bids - const adUnitRenderer = index.getAdUnit(bidObject).renderer; + const adUnitRenderer = index.getAdUnit(bid).renderer; // a publisher can also define a renderer for a mediaType - const bidObjectMediaType = bidObject.mediaType; - const mediaTypes = index.getMediaTypes(bidObject); + const bidObjectMediaType = bid.mediaType; + const mediaTypes = index.getMediaTypes(bid); const bidMediaType = mediaTypes && mediaTypes[bidObjectMediaType]; var mediaTypeRenderer = bidMediaType && bidMediaType.renderer; @@ -639,25 +710,25 @@ function getPreparedBidForAuction({adUnitCode, bid, auctionId}, {index = auction if (renderer) { // be aware, an adapter could already have installed the bidder, in which case this overwrite's the existing adapter - bidObject.renderer = Renderer.install({ url: renderer.url, config: renderer.options });// rename options to config, to make it consistent? - bidObject.renderer.setRender(renderer.render); + bid.renderer = Renderer.install({ url: renderer.url, config: renderer.options });// rename options to config, to make it consistent? + bid.renderer.setRender(renderer.render); } // Use the config value 'mediaTypeGranularity' if it has been defined for mediaType, else use 'customPriceBucket' const mediaTypeGranularity = getMediaTypeGranularity(bid.mediaType, mediaTypes, config.getConfig('mediaTypePriceGranularity')); const priceStringsObj = getPriceBucketString( - bidObject.cpm, + bid.cpm, (typeof mediaTypeGranularity === 'object') ? mediaTypeGranularity : config.getConfig('customPriceBucket'), config.getConfig('currency.granularityMultiplier') ); - bidObject.pbLg = priceStringsObj.low; - bidObject.pbMg = priceStringsObj.med; - bidObject.pbHg = priceStringsObj.high; - bidObject.pbAg = priceStringsObj.auto; - bidObject.pbDg = priceStringsObj.dense; - bidObject.pbCg = priceStringsObj.custom; - - return bidObject; + bid.pbLg = priceStringsObj.low; + bid.pbMg = priceStringsObj.med; + bid.pbHg = priceStringsObj.high; + bid.pbAg = priceStringsObj.auto; + bid.pbDg = priceStringsObj.dense; + bid.pbCg = priceStringsObj.custom; + + return bid; } function setupBidTargeting(bidObject) { diff --git a/src/constants.json b/src/constants.json index ad57fb45c4f..30ade5fccbe 100644 --- a/src/constants.json +++ b/src/constants.json @@ -29,6 +29,7 @@ "BID_TIMEOUT": "bidTimeout", "BID_REQUESTED": "bidRequested", "BID_RESPONSE": "bidResponse", + "BID_REJECTED": "bidRejected", "NO_BID": "noBid", "BID_WON": "bidWon", "BIDDER_DONE": "bidderDone", @@ -119,6 +120,13 @@ "RENDERED": "rendered", "BID_REJECTED": "bidRejected" }, + "REJECTION_REASON": { + "INVALID": "Bid has missing or invalid properties", + "INVALID_REQUEST_ID": "Invalid request ID", + "BIDDER_DISALLOWED": "Bidder code is not allowed by allowedAlternateBidderCodes / allowUnknownBidderCodes", + "FLOOR_NOT_MET": "Bid does not meet price floor", + "CANNOT_CONVERT_CURRENCY": "Unable to convert currency" + }, "PREBID_NATIVE_DATA_KEYS_TO_ORTB": { "body": "desc", "body2": "desc2", diff --git a/src/prebid.js b/src/prebid.js index 18826fc7205..a0981cd387a 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -36,7 +36,7 @@ import {listenMessagesFromCreative} from './secureCreatives.js'; import {userSync} from './userSync.js'; import {config} from './config.js'; import {auctionManager} from './auctionManager.js'; -import {filters, targeting} from './targeting.js'; +import {isBidUsable, targeting} from './targeting.js'; import {hook, wrapHook} from './hook.js'; import {loadSession} from './debugging.js'; import {includes} from './polyfill.js'; @@ -296,8 +296,7 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode = function (adunitCode) { if (adunitCode) { const bid = auctionManager.getAllBidsForAdUnitCode(adunitCode) - .filter(filters.isUnusedBid) - .filter(filters.isBidNotExpired) + .filter(isBidUsable) return bid.length ? bid.reduce(getHighestCpm) : {} } else { diff --git a/src/targeting.js b/src/targeting.js index d2b3e9f8470..a427867e890 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,15 +1,28 @@ import { - uniques, isGptPubadsDefined, getHighestCpm, getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, timestamp, - deepAccess, deepClone, logError, logWarn, logInfo, isFn, isArray, logMessage, isStr, + deepAccess, + deepClone, + getHighestCpm, + getOldestHighestCpmBid, + groupBy, + isAdUnitCodeMatchingSlot, + isArray, + isFn, + isGptPubadsDefined, + isStr, + logError, + logInfo, + logMessage, + logWarn, + timestamp, + uniques, } from './utils.js'; -import { config } from './config.js'; -import { NATIVE_TARGETING_KEYS } from './native.js'; -import { auctionManager } from './auctionManager.js'; -import { sizeSupported } from './sizeMapping.js'; -import { ADPOD } from './mediaTypes.js'; -import { hook } from './hook.js'; -import { bidderSettings } from './bidderSettings.js'; -import {includes, find} from './polyfill.js'; +import {config} from './config.js'; +import {NATIVE_TARGETING_KEYS} from './native.js'; +import {auctionManager} from './auctionManager.js'; +import {ADPOD} from './mediaTypes.js'; +import {hook} from './hook.js'; +import {bidderSettings} from './bidderSettings.js'; +import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; var pbTargetingKeys = []; @@ -32,10 +45,17 @@ const isBidNotExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 - TTL_B const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); export let filters = { + isActualBid(bid) { + return bid.getStatusCode() === CONSTANTS.STATUS.GOOD + }, isBidNotExpired, isUnusedBid }; +export function isBidUsable(bid) { + return !Object.values(filters).some((predicate) => !predicate(bid)); +} + // If two bids are found for same adUnitCode, we will use the highest one to take part in auction // This can happen in case of concurrent auctions // If adUnitBidLimit is set above 0 return top N number of bids @@ -449,10 +469,7 @@ export function newTargeting(auctionManager) { bidsReceived = bidsReceived .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) - .filter(bid => bid.mediaType !== 'banner' || sizeSupported([bid.width, bid.height])) - .filter(filters.isUnusedBid) - .filter(filters.isBidNotExpired) - ; + .filter(isBidUsable); return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); } diff --git a/test/fixtures/fixtures.js b/test/fixtures/fixtures.js index b0fbd7da806..b66d885c113 100644 --- a/test/fixtures/fixtures.js +++ b/test/fixtures/fixtures.js @@ -1,5 +1,6 @@ // jscs:disable import CONSTANTS from 'src/constants.json'; +import {createBid} from '../../src/bidfactory.js'; const utils = require('src/utils.js'); function convertTargetingsFromOldToNew(targetings) { @@ -1268,7 +1269,7 @@ export function createBidReceived({bidder, cpm, auctionId, responseTimestamp, ad if (typeof status !== 'undefined') { bid.status = status; } - return bid; + return Object.assign(createBid(CONSTANTS.STATUS.GOOD), bid); } export function getServerTestingsAds() { diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 49ae13c43cc..105401a62a4 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -21,6 +21,7 @@ import {auctionManager} from '../../src/auctionManager.js'; import 'modules/debugging/index.js' // some tests look for debugging side effects import {AuctionIndex} from '../../src/auctionIndex.js'; import {expect} from 'chai'; +import {deepClone} from '../../src/utils.js'; var assert = require('assert'); @@ -57,7 +58,16 @@ function mockBid(opts) { 'currency': 'USD', 'netRevenue': true, 'ttl': 360, - getSize: () => '300x250' + getSize: () => '300x250', + getIdentifiers() { + return { + src: this.source, + bidder: this.bidderCode, + bidId: this.requestId, + transactionId: this.transactionId, + auctionId: this.auctionId + } + } }; } @@ -1323,6 +1333,7 @@ describe('auctionmanager.js', function () { getAdUnits: () => getBidRequests().flatMap(br => br.bids).map(br => ({ code: br.adUnitCode, transactionId: br.transactionId, mediaTypes: br.mediaTypes })), getAuctionId: () => '1', addBidReceived: () => true, + addBidRejected: () => true, getTimeout: () => 1000, getAuctionStart: () => start, } @@ -1382,26 +1393,32 @@ describe('auctionmanager.js', function () { bidRequests = null; }); - it('should call auction done after bid is added to auction for mediaType banner', function () { - let ADUNIT_CODE2 = 'adUnitCode2'; - let BIDDER_CODE2 = 'sampleBidder2'; - - let bids1 = [mockBid({ bidderCode: BIDDER_CODE1, transactionId: ADUNIT_CODE1 })]; - let bids2 = [mockBid({ bidderCode: BIDDER_CODE2, transactionId: ADUNIT_CODE2 })]; - bidRequests = [ - mockBidRequest(bids[0]), - mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), - mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE2 }) - ]; - let cbs = auctionCallbacks(doneSpy, auction); - cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE, bids[0]); - cbs.adapterDone.call(bidRequests[0]); - cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids1[0]); - cbs.adapterDone.call(bidRequests[1]); - cbs.addBidResponse.call(bidRequests[2], ADUNIT_CODE2, bids2[0]); - cbs.adapterDone.call(bidRequests[2]); - assert.equal(doneSpy.callCount, 1); - }); + Object.entries({ + 'added to': (cbs) => cbs.addBidResponse, + 'rejected from': (cbs) => cbs.addBidResponse.reject, + }).forEach(([t, getMethod]) => { + it(`should call auction done after bid is ${t} auction for mediaType banner`, function () { + let ADUNIT_CODE2 = 'adUnitCode2'; + let BIDDER_CODE2 = 'sampleBidder2'; + + let bids1 = [mockBid({ bidderCode: BIDDER_CODE1, transactionId: ADUNIT_CODE1 })]; + let bids2 = [mockBid({ bidderCode: BIDDER_CODE2, transactionId: ADUNIT_CODE2 })]; + bidRequests = [ + mockBidRequest(bids[0]), + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + mockBidRequest(bids2[0], { adUnitCode: ADUNIT_CODE2 }) + ]; + let cbs = auctionCallbacks(doneSpy, auction); + const method = getMethod(cbs); + method(ADUNIT_CODE, bids[0]); + cbs.adapterDone.call(bidRequests[0]); + method(ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[1]); + method(ADUNIT_CODE2, bids2[0]); + cbs.adapterDone.call(bidRequests[2]); + assert.equal(doneSpy.callCount, 1); + }); + }) it('should call auction done after prebid cache is complete for mediaType video', function() { bids[0].mediaType = 'video'; @@ -1543,6 +1560,77 @@ describe('auctionmanager.js', function () { }); }) }); + + describe('when bids are rejected', () => { + let cbs, bid, expectedRejection; + const onBidRejected = sinon.stub(); + const REJECTION_REASON = 'Bid rejected'; + const AU_CODE = 'au'; + + function rejectHook(fn, adUnitCode, bid, reject) { + reject(REJECTION_REASON); + reject(REJECTION_REASON); // second call should do nothing + } + + before(() => { + addBidResponse.before(rejectHook, 999); + events.on(CONSTANTS.EVENTS.BID_REJECTED, onBidRejected); + }); + + after(() => { + addBidResponse.getHooks({hook: rejectHook}).remove(); + events.off(CONSTANTS.EVENTS.BID_REJECTED, onBidRejected); + }); + + beforeEach(() => { + onBidRejected.reset(); + bid = mockBid({bidderCode: BIDDER_CODE}); + bidRequests = [ + mockBidRequest(bid), + ]; + cbs = auctionCallbacks(doneSpy, auction); + expectedRejection = sinon.match(Object.assign({}, bid, { + rejectionReason: REJECTION_REASON, + adUnitCode: AU_CODE + })); + auction.addBidRejected = sinon.stub(); + }); + + Object.entries({ + 'with addBidResponse.reject': () => cbs.addBidResponse.reject(AU_CODE, deepClone(bid), REJECTION_REASON), + 'from addBidResponse hooks': () => cbs.addBidResponse(AU_CODE, deepClone(bid)) + }).forEach(([t, rejectBid]) => { + describe(t, () => { + it('should emit a BID_REJECTED event', () => { + rejectBid(); + sinon.assert.calledWith(onBidRejected, expectedRejection); + }); + + it('should pass bid to auction.addBidRejected', () => { + rejectBid(); + sinon.assert.calledWith(auction.addBidRejected, expectedRejection); + }); + }) + }); + + it('should return a NO_BID replacement', () => { + const noBid = cbs.addBidResponse.reject(AU_CODE, {...bid, statusMessage: 'Bid available', status: CONSTANTS.BID_STATUS.RENDERED}, 'Rejected'); + sinon.assert.match(noBid, { + status: CONSTANTS.BID_STATUS.BID_REJECTED, + statusMessage: 'Bid returned empty or error response', + cpm: 0, + requestId: bid.requestId, + auctionId: bid.auctionId, + adUnitCode: AU_CODE, + rejectionReason: undefined, + }); + }); + + it('addBidResponse hooks should not be able to reject the same bid twice', () => { + cbs.addBidResponse(AU_CODE, bid); + expect(auction.addBidRejected.calledOnce).to.be.true; + }); + }) }); describe('auctionOptions', function() { diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 6eb4f929a42..b674cb5976d 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -351,6 +351,11 @@ describe('currency', function () { }); describe('currency.addBidResponseDecorator', function () { + let reject; + beforeEach(() => { + reject = sinon.stub().returns({status: 'rejected'}); + }); + it('should leave bid at 1 when currency support is not enabled and fromCurrency is USD', function () { setConfig({}); var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); @@ -368,8 +373,9 @@ describe('currency', function () { var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; - }, 'elementId', bid); - expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }, 'elementId', bid, reject); + expect(innerBid.status).to.equal('rejected'); + expect(reject.calledOnce).to.be.true; }); it('should not buffer bid when currency is already in desired currency', function () { @@ -395,8 +401,9 @@ describe('currency', function () { var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; - }, 'elementId', bid); - expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }, 'elementId', bid, reject); + expect(innerBid.status).to.equal('rejected'); + expect(reject.calledOnce).to.be.true; }); it('should result in NO_BID when adServerCurrency is not supported in file', function () { @@ -407,8 +414,9 @@ describe('currency', function () { var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; - }, 'elementId', bid); - expect(innerBid.statusMessage).to.equal('Bid returned empty or error response'); + }, 'elementId', bid, reject); + expect(innerBid.status).to.equal('rejected'); + expect(reject.calledOnce).to.be.true; }); it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', function () { diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index c3ca3261193..b5209e5be12 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -536,6 +536,8 @@ describe('S2S Adapter', function () { addBidResponse = sinon.spy(), done = sinon.spy(); + addBidResponse.reject = sinon.spy(); + function prepRequest(req) { req.ad_units.forEach((adUnit) => { delete adUnit.nativeParams @@ -595,6 +597,7 @@ describe('S2S Adapter', function () { afterEach(function () { addBidResponse.resetHistory(); + addBidResponse.reject = sinon.spy(); done.resetHistory(); }); @@ -2903,6 +2906,16 @@ describe('S2S Adapter', function () { }); } + it('should reject invalid bids', () => { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + Object.assign(response.seatbid[0].bid[0], {w: null, h: null}); + server.requests[0].respond(200, {}, JSON.stringify(response)); + expect(addBidResponse.reject.calledOnce).to.be.true; + expect(addBidResponse.called).to.be.false; + }); + it('does not (by default) allow bids that were not requested', function () { config.setConfig({ s2sConfig: CONFIG }); adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); @@ -2911,6 +2924,7 @@ describe('S2S Adapter', function () { server.requests[0].respond(200, {}, JSON.stringify(response)); expect(addBidResponse.called).to.be.false; + expect(addBidResponse.reject.calledOnce).to.be.true; }); it('allows unrequested bids if config.allowUnknownBidderCodes', function () { @@ -2924,17 +2938,35 @@ describe('S2S Adapter', function () { expect(addBidResponse.calledWith(sinon.match.any, sinon.match({ bidderCode: 'unknown' }))).to.be.true; }); - it('uses "null" request\'s ID for all responses, when a null request is present', function () { - const cfg = {...CONFIG, allowUnknownBidderCodes: true}; - config.setConfig({s2sConfig: cfg}); - const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}]}]}; - const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} - adapter.callBids(req, [bidReq], addBidResponse, done, ajax); - const response = deepClone(RESPONSE_OPENRTB); - response.seatbid[0].seat = 'storedImpression'; - server.requests[0].respond(200, {}, JSON.stringify(response)); - sinon.assert.calledWith(addBidResponse, sinon.match.any, sinon.match({bidderCode: 'storedImpression', requestId: 'testId'})) - }); + describe('stored impressions', () => { + let bidReq, response; + + function mks2sReq(s2sConfig = CONFIG) { + return {...REQUEST, s2sConfig, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}]}]}; + } + + beforeEach(() => { + bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} + response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'storedImpression'; + }) + + it('uses "null" request\'s ID for all responses, when a null request is present', function () { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); + adapter.callBids(mks2sReq(cfg), [bidReq], addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(response)); + sinon.assert.calledWith(addBidResponse, sinon.match.any, sinon.match({bidderCode: 'storedImpression', requestId: 'testId'})) + }); + + it('does not allow null requests (= stored impressions) if allowUnknownBidderCodes is not set', () => { + config.setConfig({s2sConfig: CONFIG}); + adapter.callBids(mks2sReq(), [bidReq], addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(response)); + expect(addBidResponse.called).to.be.false; + expect(addBidResponse.reject.calledOnce).to.be.true; + }); + }) it('copies ortb2Imp to response when there is only a null bid', () => { const cfg = {...CONFIG}; diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index cbdd4d179e3..b7d771814d0 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -1665,7 +1665,7 @@ describe('the price floors module', function () { }); describe('bidResponseHook tests', function () { const AUCTION_ID = '123456'; - let returnedBidResponse, indexStub; + let returnedBidResponse, indexStub, reject; let adUnit = { transactionId: 'au', code: 'test_div_1' @@ -1681,6 +1681,7 @@ describe('the price floors module', function () { }; beforeEach(function () { returnedBidResponse = {}; + reject = sinon.stub().returns({status: 'rejected'}); indexStub = sinon.stub(auctionManager, 'index'); indexStub.get(() => stubAuctionIndex({adUnits: [adUnit]})); }); @@ -1693,7 +1694,7 @@ describe('the price floors module', function () { let next = (adUnitCode, bid) => { returnedBidResponse = bid; }; - addBidResponseHook(next, bidResp.adUnitCode, Object.assign(createBid(CONSTANTS.STATUS.GOOD, {auctionId: AUCTION_ID}), bidResp)); + addBidResponseHook(next, bidResp.adUnitCode, Object.assign(createBid(CONSTANTS.STATUS.GOOD, {auctionId: AUCTION_ID}), bidResp), reject); }; it('continues with the auction if not floors data is present without any flooring', function () { runBidResponse(); @@ -1710,9 +1711,8 @@ describe('the price floors module', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; runBidResponse(); - expect(returnedBidResponse).to.haveOwnProperty('floorData'); - expect(returnedBidResponse.status).to.equal(CONSTANTS.BID_STATUS.BID_REJECTED); - expect(returnedBidResponse.cpm).to.equal(0); + expect(reject.calledOnce).to.be.true; + expect(returnedBidResponse.status).to.equal('rejected'); }); it('if it finds a rule and does not floor should update the bid accordingly', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 7164e468e71..fa6cf7e6bb5 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -1,4 +1,4 @@ -import { newBidder, registerBidder, preloadBidderMappingFile, storage } from 'src/adapters/bidderFactory.js'; +import {newBidder, registerBidder, preloadBidderMappingFile, storage, isValid} from 'src/adapters/bidderFactory.js'; import adapterManager from 'src/adapterManager.js'; import * as ajax from 'src/ajax.js'; import { expect } from 'chai'; @@ -62,6 +62,7 @@ describe('bidders created by newBidder', function () { }; addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); doneStub = sinon.stub(); }); @@ -508,7 +509,7 @@ describe('bidders created by newBidder', function () { expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); - it('should logError when required bid response params are missing', function () { + it('should logError and reject bid when required bid response params are missing', function () { const bidder = newBidder(spec); const bid = { @@ -532,9 +533,10 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - it('should logError when required bid response params are undefined', function () { + it('should logError and reject bid when required response params are undefined', function () { const bidder = newBidder(spec); const bid = { @@ -562,6 +564,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); it('should require requestId from interpretResponse', () => { @@ -586,6 +589,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(addBidResponseStub.called).to.be.false; + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); }); @@ -856,6 +860,7 @@ describe('validate bid response: ', function () { }); addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); doneStub = sinon.stub(); ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { const fakeResponse = sinon.stub(); @@ -954,7 +959,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); }); } @@ -1074,7 +1080,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; expect(logWarnSpy.callCount).to.equal(1); }); @@ -1085,7 +1092,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { @@ -1096,7 +1104,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; expect(logWarnSpy.callCount).to.equal(1); }); @@ -1147,7 +1156,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; expect(logWarnSpy.callCount).to.equal(1); }); @@ -1159,7 +1169,7 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.called).to.equal(true); expect(logWarnSpy.callCount).to.equal(0); expect(logErrorSpy.callCount).to.equal(0); }); @@ -1172,8 +1182,9 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { @@ -1183,7 +1194,8 @@ describe('validate bid response: ', function () { spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; expect(logWarnSpy.callCount).to.equal(1); }); }); @@ -1385,3 +1397,46 @@ describe('preload mapping url hook', function() { clock.restore(); }); }); + +describe('bid response isValid', () => { + describe('size check', () => { + let req, index; + + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2], [3, 4]] + } + } + } + }); + + function mkResponse(width, height) { + return { + requestId: req.bidId, + width, + height, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + mediaType: 'banner', + } + } + + function checkValid(bid) { + return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); + } + + it('should succeed when response has a size that was in request', () => { + expect(checkValid(mkResponse(3, 4))).to.be.true; + }); + + it('should fail when response has a size that was not in request', () => { + expect(checkValid(mkResponse(10, 11))).to.be.false; + }); + }) +}); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 4585ddbfaaf..448f1b36e3a 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -6,8 +6,14 @@ import CONSTANTS from 'src/constants.json'; import { auctionManager } from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; +import {createBid} from '../../../../src/bidfactory.js'; +import {hook} from '../../../../src/hook.js'; -const bid1 = { +function mkBid(bid, status = CONSTANTS.STATUS.GOOD) { + return Object.assign(createBid(status), bid); +} + +const sampleBid = { 'bidderCode': 'rubicon', 'width': '300', 'height': '250', @@ -39,7 +45,9 @@ const bid1 = { 'ttl': 300 }; -const bid2 = { +const bid1 = mkBid(sampleBid); + +const bid2 = mkBid({ 'bidderCode': 'rubicon', 'width': '300', 'height': '250', @@ -67,9 +75,9 @@ const bid2 = { 'netRevenue': true, 'currency': 'USD', 'ttl': 300 -}; +}); -const bid3 = { +const bid3 = mkBid({ 'bidderCode': 'rubicon', 'width': '300', 'height': '600', @@ -97,9 +105,9 @@ const bid3 = { 'netRevenue': true, 'currency': 'USD', 'ttl': 300 -}; +}); -const nativeBid1 = { +const nativeBid1 = mkBid({ 'bidderCode': 'appnexus', 'width': 0, 'height': 0, @@ -165,8 +173,9 @@ const nativeBid1 = { [CONSTANTS.NATIVE_KEYS.image]: 'http://vcdn.adnxs.com/p/creative-image/94/22/cd/0f/9422cd0f-f400-45d3-80f5-2b92629d9257.jpg', [CONSTANTS.NATIVE_KEYS.icon]: 'http://vcdn.adnxs.com/p/creative-image/bd/59/a6/c6/bd59a6c6-0851-411d-a16d-031475a51312.png' } -}; -const nativeBid2 = { +}); + +const nativeBid2 = mkBid({ 'bidderCode': 'dgads', 'width': 0, 'height': 0, @@ -222,7 +231,7 @@ const nativeBid2 = { [CONSTANTS.NATIVE_KEYS.sponsoredBy]: 'test.com', [CONSTANTS.NATIVE_KEYS.clickUrl]: 'http://prebid.org/' } -}; +}); describe('targeting tests', function () { let sandbox; @@ -231,6 +240,10 @@ describe('targeting tests', function () { let bidCacheFilterFunction; let undef; + before(() => { + hook.ready(); + }); + beforeEach(function() { sandbox = sinon.sandbox.create(); @@ -287,6 +300,12 @@ describe('targeting tests', function () { bidExpiryStub.restore(); }); + it('should filter out NO_BID bids', () => { + bidsReceived = [mkBid(sampleBid, CONSTANTS.STATUS.NO_BID)]; + const tg = targetingInstance.getAllTargeting(); + expect(tg[bidsReceived[0].adUnitCode]).to.eql({}); + }); + describe('when handling different adunit targeting value types', function () { const adUnitCode = '/123456/header-bid-tag-0'; const adServerTargeting = {}; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 6757ef42094..aac36ee76a1 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -200,6 +200,7 @@ describe('Unit: Prebid Module', function () { hook.ready(); $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); resetDebugging(); + sinon.stub(filters, 'isActualBid').returns(true); // stub this out so that we can use vanilla objects as bids }); beforeEach(function () { @@ -216,6 +217,7 @@ describe('Unit: Prebid Module', function () { after(function() { auctionManager.clearAllAuctions(); + filters.isActualBid.restore(); }); describe('and global adUnits', () => { @@ -504,8 +506,8 @@ describe('Unit: Prebid Module', function () { 'client_initiated_ad_counting': true, 'rtb': { 'banner': { - 'width': 728, - 'height': 90, + 'width': 300, + 'height': 250, 'content': '' }, 'trackers': [{