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

Add adpod support to AppNexus adapter #3484

Merged
merged 26 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4dda86e
Add new context type
matthewlane Jan 23, 2019
564fdd1
Write request duplication test
matthewlane Jan 23, 2019
7643efb
Duplicate adpod placement for request
matthewlane Jan 25, 2019
93ee04e
Write requireExactDuration duplication test
matthewlane Jan 25, 2019
7c3fe33
Duplicate adpod placement when requireExactDuration is set
matthewlane Jan 25, 2019
b803679
Add brandCategoryExclusion config to request
matthewlane Jan 25, 2019
305c4dd
Add adpod fields to bid response object
matthewlane Jan 26, 2019
7136d47
Split large requests into batches
matthewlane Jan 29, 2019
ffb89be
Get context from correct object
matthewlane Jan 30, 2019
e8dfb44
Use util function to get request subsets
matthewlane Jan 31, 2019
bd1eb8b
Use correct mediaType.video configuration names
matthewlane Feb 4, 2019
0400567
Rename category prop to iabSubCatId
matthewlane Feb 4, 2019
07c58d5
Comment sub function usage
matthewlane Feb 5, 2019
4c21d19
Round down placements when config uneven
matthewlane Feb 6, 2019
c8ac854
Set max/min duration across tags when config numbers are uneven
matthewlane Feb 6, 2019
03aa09f
Account for multiple adpod adUnits
matthewlane Feb 6, 2019
5e5c511
Round durationSeconds up if remainder
matthewlane Feb 6, 2019
71071c0
Use adpod constant
matthewlane Feb 6, 2019
dbc475c
Update subCat usage comment
matthewlane Feb 6, 2019
be536fb
Update subCat usage comment
matthewlane Feb 11, 2019
635b02b
Change ceil to floor
matthewlane Feb 11, 2019
e424cf2
Merge branch 'master' into adpod-adapter-support
Feb 13, 2019
905bf8a
fix unit test
Feb 13, 2019
17a5176
correct flag name
Feb 19, 2019
0635c0f
uncomment todos
Feb 26, 2019
7bf163f
Merge branch 'master' of github.com:prebid/Prebid.js into adpod-adapt…
Feb 27, 2019
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
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