Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PBS adapter: partial support of sizeMapping (and sizeMappingV2) #8084

Merged
merged 4 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ Object.assign(ORTB2.prototype, {
// transform ad unit into array of OpenRTB impression objects
let impIds = new Set();
adUnits.forEach(adUnit => {
// TODO: support labels / conditional bids
// for now, just warn about them
adUnit.bids.forEach((bid) => {
if (bid.mediaTypes != null) {
logWarn(`Prebid Server adapter does not (yet) support bidder-specific mediaTypes for the same adUnit. Size mapping configuration will be ignored for adUnit: ${adUnit.code}, bidder: ${bid.bidder}`);
}
})

Comment on lines +520 to +527
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please provided an example Ad unit setup where we use bidder-specific mediaTypes? I don't understand the meaning of this warning message.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fawke - this is the 'relevantMediaTypes feature described in https://docs.prebid.org/dev-docs/modules/sizeMappingV2.html

// in case there is a duplicate imp.id, add '-2' suffix to the second imp.id.
// e.g. if there are 2 adUnits (case of twin adUnit codes) with code 'test',
// first imp will have id 'test' and second imp will have id 'test-2'
Expand Down
245 changes: 90 additions & 155 deletions modules/sizeMappingV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@
* rendering. Read full API documentation on Prebid.org, http://prebid.org/dev-docs/modules/sizeMappingV2.html
*/

import { isArray, logError, isArrayOfNums, deepClone, logWarn, getWindowTop, deepEqual, logInfo, isValidMediaTypes, deepAccess, getDefinedParams, getUniqueIdentifierStr, flatten } from '../src/utils.js';
import { adunitCounter } from '../src/adUnits.js';
import {
isArray,
logError,
isArrayOfNums,
deepClone,
logWarn,
getWindowTop,
logInfo,
isValidMediaTypes
} from '../src/utils.js';
import includes from 'core-js-pure/features/array/includes.js';
import { getHook } from '../src/hook.js';
import { adUnitSetupChecks } from '../src/prebid.js';
Expand All @@ -20,61 +28,33 @@ export const internal = {
isLabelActivated
};

/*
'sizeMappingInternalStore' contains information on, whether a particular auction is using size mapping V2 (the new size mapping spec),
and it also contains additional information on each adUnit, such as, mediaTypes, activeViewport, etc. This information is required by
the 'getBids' function.
*/

export const sizeMappingInternalStore = createSizeMappingInternalStore();

function createSizeMappingInternalStore() {
const sizeMappingInternalStore = {};

return {
initializeStore: function (auctionId, isUsingSizeMappingBool) {
sizeMappingInternalStore[auctionId] = {
usingSizeMappingV2: isUsingSizeMappingBool,
adUnits: []
};
},
getAuctionDetail: function (auctionId) {
return sizeMappingInternalStore[auctionId];
},
setAuctionDetail: function (auctionId, adUnitDetail) {
sizeMappingInternalStore[auctionId].adUnits.push(adUnitDetail);
}
}
}
const V2_ADUNITS = new WeakMap();

/*
Returns "true" if at least one of the adUnits in the adUnits array is using an Ad Unit and/or Bidder level sizeConfig,
otherwise, returns "false."
*/
export function isUsingNewSizeMapping(adUnits) {
let isUsingSizeMappingBool = false;
adUnits.forEach(adUnit => {
return !!adUnits.find(adUnit => {
if (V2_ADUNITS.has(adUnit)) return V2_ADUNITS.get(adUnit);
if (adUnit.mediaTypes) {
// checks for the presence of sizeConfig property at the adUnit.mediaTypes object
Object.keys(adUnit.mediaTypes).forEach(mediaType => {
for (let mediaType of Object.keys(adUnit.mediaTypes)) {
if (adUnit.mediaTypes[mediaType].sizeConfig) {
if (isUsingSizeMappingBool === false) {
isUsingSizeMappingBool = true;
}
V2_ADUNITS.set(adUnit, true);
return true;
}
});

// checks for the presence of sizeConfig property at the adUnit.bids[].bidder object
adUnit.bids && isArray(adUnit.bids) && adUnit.bids.forEach(bidder => {
if (bidder.sizeConfig) {
if (isUsingSizeMappingBool === false) {
isUsingSizeMappingBool = true;
}
}
for (let bid of adUnit.bids && isArray(adUnit.bids) ? adUnit.bids : []) {
if (bid.sizeConfig) {
V2_ADUNITS.set(adUnit, true);
return true;
}
});
}
V2_ADUNITS.set(adUnit, false);
return false;
}
});
return isUsingSizeMappingBool;
}

/**
Expand Down Expand Up @@ -289,23 +269,11 @@ export function checkBidderSizeConfigFormat(sizeConfig) {
return didCheckPass;
}

getHook('getBids').before(function (fn, bidderInfo) {
// check if the adUnit is using sizeMappingV2 specs and store the result in _sizeMappingUsageMap.
if (typeof sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId) === 'undefined') {
const isUsingSizeMappingBool = isUsingNewSizeMapping(bidderInfo.adUnits);

// initialize sizeMappingInternalStore for the first time for a particular auction
sizeMappingInternalStore.initializeStore(bidderInfo.auctionId, isUsingSizeMappingBool);
}
if (sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId).usingSizeMappingV2) {
// if adUnit is found using sizeMappingV2 specs, run the getBids function which processes the sizeConfig object
// and returns the bids array for a particular bidder.

const bids = getBids(bidderInfo);
return fn.bail(bids);
getHook('setupAdUnitMediaTypes').before(function (fn, adUnits, labels) {
if (isUsingNewSizeMapping(adUnits)) {
return fn.bail(setupAdUnitMediaTypes(adUnits, labels));
} else {
// if not using sizeMappingV2, default back to the getBids function defined in adapterManager.
return fn.call(this, bidderInfo);
return fn.call(this, adUnits, labels);
}
});

Expand Down Expand Up @@ -423,8 +391,8 @@ export function getFilteredMediaTypes(mediaTypes) {
return sizeBucketToSizeMap;
}, {});

return { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes };
};
return { sizeBucketToSizeMap, activeViewport, transformedMediaTypes };
}

/**
* Evaluates the given sizeConfig object and checks for various properties to determine if the sizeConfig is active or not. For example,
Expand Down Expand Up @@ -475,120 +443,87 @@ export function getActiveSizeBucket(sizeConfig, activeViewport) {
}

export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) {
const mediaTypes = new Set();
if (internal.checkBidderSizeConfigFormat(sizeConfig)) {
const activeSizeBucket = internal.getActiveSizeBucket(sizeConfig, activeViewport);
return sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes'];
sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes'].forEach((mt) => mediaTypes.add(mt));
}
return [];
return mediaTypes;
}

// sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function
// returns adUnit details object.
export function getAdUnitDetail(auctionId, adUnit, labels) {
// fetch all adUnits for an auction from the sizeMappingInternalStore
const adUnitsForAuction = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits;

// check if the adUnit exists already in the sizeMappingInterStore (check for equivalence of 'code' && 'mediaTypes' properties)
const adUnitDetail = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code && deepEqual(adUnitDetail.mediaTypes, adUnit.mediaTypes));

if (adUnitDetail.length > 0) {
adUnitDetail[0].cacheHits++;
return adUnitDetail[0];
} else {
const identicalAdUnit = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code);
const adUnitInstance = identicalAdUnit.length > 0 && typeof identicalAdUnit[0].instance === 'number' ? identicalAdUnit[identicalAdUnit.length - 1].instance + 1 : 1;
const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance);
const { mediaTypes = adUnit.mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes);

const adUnitDetail = {
adUnitCode: adUnit.code,
mediaTypes,
sizeBucketToSizeMap,
activeViewport,
transformedMediaTypes,
instance: adUnitInstance,
isLabelActivated,
cacheHits: 0
};

// set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'.
sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail);
isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap);

return adUnitDetail;
}
export function getAdUnitDetail(adUnit, labels, adUnitInstance) {
const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance);
const { sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes);
isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap);
return {
activeViewport,
transformedMediaTypes,
isLabelActivated,
};
}

export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src }) {
export function setupAdUnitMediaTypes(adUnits, labels) {
const duplCounter = {};
return adUnits.reduce((result, adUnit) => {
const instance = (() => {
if (!duplCounter.hasOwnProperty(adUnit.code)) {
duplCounter[adUnit.code] = 1;
}
return duplCounter[adUnit.code]++;
})();
if (adUnit.mediaTypes && isValidMediaTypes(adUnit.mediaTypes)) {
const { activeViewport, transformedMediaTypes, instance: adUnitInstance, isLabelActivated, cacheHits } = internal.getAdUnitDetail(auctionId, adUnit, labels);
const { activeViewport, transformedMediaTypes, isLabelActivated } = internal.getAdUnitDetail(adUnit, labels, instance);
if (isLabelActivated) {
// check if adUnit has any active media types remaining, if not drop the adUnit from auction,
// else proceed to evaluate the bids object.
if (Object.keys(transformedMediaTypes).length === 0) {
cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`);
return result;
}
result
.push(adUnit.bids.filter(bid => bid.bidder === bidderCode)
.reduce((bids, bid) => {
if (internal.isLabelActivated(bid, labels, adUnit.code, adUnitInstance)) {
// handle native params

bid = Object.assign({}, bid, getDefinedParams(adUnit, ['mediaType', 'renderer', 'nativeParams']));

if (bid.sizeConfig) {
const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport);
if (relevantMediaTypes.length === 0) {
logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bidderCode} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`);
bid = Object.assign({}, bid);
} else if (relevantMediaTypes[0] !== 'none') {
const bidderMediaTypes = Object
.keys(transformedMediaTypes)
.filter(mt => relevantMediaTypes.indexOf(mt) > -1)
.reduce((mediaTypes, mediaType) => {
mediaTypes[mediaType] = transformedMediaTypes[mediaType];
return mediaTypes;
}, {});

if (Object.keys(bidderMediaTypes).length > 0) {
bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes });
} else {
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`);
return bids;
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`);
} else {
adUnit.mediaTypes = transformedMediaTypes;
adUnit.bids = adUnit.bids.reduce((bids, bid) => {
if (internal.isLabelActivated(bid, labels, adUnit.code, instance)) {
if (bid.sizeConfig) {
const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport);
if (relevantMediaTypes.size === 0) {
logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remain active.`);
bids.push(bid);
} else if (!relevantMediaTypes.has('none')) {
let modified = false;
const bidderMediaTypes = Object.fromEntries(
Object.entries(transformedMediaTypes)
.filter(([key, val]) => {
if (!relevantMediaTypes.has(key)) {
modified = true;
return false;
}
return true;
})
);
if (Object.keys(bidderMediaTypes).length > 0) {
if (modified) {
bid.mediaTypes = bidderMediaTypes;
}
bids.push(bid);
} else {
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`);
return bids;
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`);
}
} else {
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`);
}
bids.push(Object.assign({}, bid, {
adUnitCode: adUnit.code,
transactionId: adUnit.transactionId,
sizes: deepAccess(transformedMediaTypes, 'banner.sizes') || deepAccess(transformedMediaTypes, 'video.playerSize') || [],
mediaTypes: bid.mediaTypes || transformedMediaTypes,
bidId: bid.bid_id || getUniqueIdentifierStr(),
bidderRequestId,
auctionId,
src,
bidRequestsCount: adunitCounter.getRequestsCounter(adUnit.code),
bidderRequestsCount: adunitCounter.getBidderRequestsCounter(adUnit.code, bid.bidder),
bidderWinsCount: adunitCounter.getBidderWinsCounter(adUnit.code, bid.bidder)
}));
return bids;
} else {
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`);
return bids;
bids.push(bid);
}
}, []));
} else {
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`);
}
return bids;
}, []);
result.push(adUnit);
}
} else {
cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit is disabled due to failing label check.`);
logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit is disabled due to failing label check.`);
}
} else {
logWarn(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`);
return result;
}
return result;
}, []).reduce(flatten, []).filter(val => val !== '');
}, [])
}
Loading