Skip to content

Commit

Permalink
Add adpod support to AppNexus adapter (prebid#3484)
Browse files Browse the repository at this point in the history
* Add new context type

* Write request duplication test

* Duplicate adpod placement for request

* Write requireExactDuration duplication test

* Duplicate adpod placement when requireExactDuration is set

* Add brandCategoryExclusion config to request

* Add adpod fields to bid response object

* Split large requests into batches

* Get context from correct object

* Use util function to get request subsets

* Use correct mediaType.video configuration names

* Rename category prop to iabSubCatId

* Comment sub function usage

* Round down placements when config uneven

* Set max/min duration across tags when config numbers are uneven

* Account for multiple adpod adUnits

* Round durationSeconds up if remainder

* Use adpod constant

* Update subCat usage comment

* Update subCat usage comment

* Change ceil to floor

* fix unit test

* correct flag name

* uncomment todos
  • Loading branch information
matthewlane authored and jacekburys-quantcast committed May 15, 2019
1 parent 3e90d2a commit 502b854
Show file tree
Hide file tree
Showing 3 changed files with 368 additions and 10 deletions.
114 changes: 105 additions & 9 deletions modules/appnexusBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Renderer } from '../src/Renderer';
import * as utils from '../src/utils';
import { config } from '../src/config';
import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory';
import { BANNER, NATIVE, VIDEO, ADPOD } from '../src/mediaTypes';
import find from 'core-js/library/fn/array/find';
Expand Down Expand Up @@ -32,6 +33,7 @@ const NATIVE_MAPPING = {
displayUrl: 'displayurl'
};
const SOURCE = 'pbjs';
const MAX_IMPS_PER_REQUEST = 15;
const mappingFileUrl = '//acdn.adnxs.com/prebid/appnexus-mapping/mappings.json';

export const spec = {
Expand Down Expand Up @@ -120,6 +122,7 @@ export const spec = {
version: '$prebid.version$'
}
};

if (member > 0) {
payload.member_id = member;
}
Expand All @@ -131,6 +134,10 @@ export const spec = {
payload.app = appIdObj;
}

if (config.getConfig('adpod.brandCategoryExclusion')) {
payload.brand_category_uniqueness = true;
}

if (debugObjParams.enabled) {
payload.debug = debugObjParams;
utils.logInfo('AppNexus Debug Auction Settings:\n\n' + JSON.stringify(debugObjParams, null, 4));
Expand All @@ -154,13 +161,18 @@ export const spec = {
payload.referrer_detection = refererinfo;
}

const payloadString = JSON.stringify(payload);
return {
method: 'POST',
url: URL,
data: payloadString,
bidderRequest
};
const hasAdPodBid = find(bidRequests, hasAdPod);
if (hasAdPodBid) {
bidRequests.filter(hasAdPod).forEach(adPodBid => {
const adPodTags = createAdPodRequest(tags, adPodBid);
// don't need the original adpod placement because it's in adPodTags
const nonPodTags = payload.tags.filter(tag => tag.uuid !== adPodBid.bidId);
payload.tags = [...nonPodTags, ...adPodTags];
});
}

const request = formatRequest(payload, bidderRequest);
return request;
},

/**
Expand Down Expand Up @@ -276,6 +288,35 @@ function deleteValues(keyPairObj) {
}
}

function formatRequest(payload, bidderRequest) {
let request = [];

if (payload.tags.length > MAX_IMPS_PER_REQUEST) {
const clonedPayload = utils.deepClone(payload);

utils.chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => {
clonedPayload.tags = tags;
const payloadString = JSON.stringify(clonedPayload);
request.push({
method: 'POST',
url: URL,
data: payloadString,
bidderRequest
});
});
} else {
const payloadString = JSON.stringify(payload);
request = {
method: 'POST',
url: URL,
data: payloadString,
bidderRequest
};
}

return request;
}

function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) {
const renderer = Renderer.install({
id: rtbBid.renderer_id,
Expand Down Expand Up @@ -339,14 +380,13 @@ function newBid(serverBid, rtbBid, bidderRequest) {
const videoContext = utils.deepAccess(bidRequest, 'mediaTypes.video.context');
if (videoContext === ADPOD) {
const iabSubCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id);

bid.meta = {
iabSubCatId
};

bid.video = {
context: ADPOD,
durationSeconds: Math.ceil(rtbBid.rtb.video.duration_ms / 1000),
durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000),
};
}

Expand Down Expand Up @@ -553,6 +593,62 @@ function hasDebug(bid) {
return !!bid.debug
}

function hasAdPod(bid) {
return (
bid.mediaTypes &&
bid.mediaTypes.video &&
bid.mediaTypes.video.context === ADPOD
);
}

/**
* Expand an adpod placement into a set of request objects according to the
* total adpod duration and the range of duration seconds. Sets minduration/
* maxduration video property according to requireExactDuration configuration
*/
function createAdPodRequest(tags, adPodBid) {
const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video;

const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video);
const maxDuration = utils.getMaxValueFromArray(durationRangeSec);

const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId);
let request = utils.fill(...tagToDuplicate, numberOfPlacements);

if (requireExactDuration) {
const divider = Math.ceil(numberOfPlacements / durationRangeSec.length);
const chunked = utils.chunk(request, divider);

// each configured duration is set as min/maxduration for a subset of requests
durationRangeSec.forEach((duration, index) => {
chunked[index].map(tag => {
setVideoProperty(tag, 'minduration', duration);
setVideoProperty(tag, 'maxduration', duration);
});
});
} else {
// all maxdurations should be the same
request.map(tag => setVideoProperty(tag, 'maxduration', maxDuration));
}

return request;
}

function getAdPodPlacementNumber(videoParams) {
const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams;
const minAllowedDuration = utils.getMinValueFromArray(durationRangeSec);
const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration);

return requireExactDuration
? Math.max(numberOfPlacements, durationRangeSec.length)
: numberOfPlacements;
}

function setVideoProperty(tag, key, value) {
if (utils.isEmpty(tag.video)) { tag.video = {}; }
tag.video[key] = value;
}

function getRtbBid(tag) {
return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb);
}
Expand Down
44 changes: 43 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,7 @@ export function getDefinedParams(object, params) {
*/
export function isValidMediaTypes(mediaTypes) {
const SUPPORTED_MEDIA_TYPES = ['banner', 'native', 'video'];
const SUPPORTED_STREAM_TYPES = ['instream', 'outstream'];
const SUPPORTED_STREAM_TYPES = ['instream', 'outstream', 'adpod'];

const types = Object.keys(mediaTypes);

Expand Down Expand Up @@ -1221,3 +1221,45 @@ export function hasLocalStorage() {
export function isArrayOfNums(val, size) {
return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v)));
}

/**
* Creates an array of n length and fills each item with the given value
*/
export function fill(value, length) {
let newArray = [];

for (let i = 0; i < length; i++) {
let valueToPush = isPlainObject(value) ? deepClone(value) : value;
newArray.push(valueToPush);
}

return newArray;
}

/**
* http://npm.im/chunk
* Returns an array with *size* chunks from given array
*
* Example:
* ['a', 'b', 'c', 'd', 'e'] chunked by 2 =>
* [['a', 'b'], ['c', 'd'], ['e']]
*/
export function chunk(array, size) {
let newArray = [];

for (let i = 0; i < Math.ceil(array.length / size); i++) {
let start = i * size;
let end = start + size;
newArray.push(array.slice(start, end));
}

return newArray;
}

export function getMinValueFromArray(array) {
return Math.min(...array);
}

export function getMaxValueFromArray(array) {
return Math.max(...array);
}
Loading

0 comments on commit 502b854

Please sign in to comment.