Skip to content

Commit

Permalink
BaseAdapter for the Prebid 0.x -> 1.x transition (prebid#1494)
Browse files Browse the repository at this point in the history
* Added a base adapter for single-request adapters, and ported the appnexusAst adapter onto it

* Renamed the SingleRequestBidder to BidderFactory. Updated it to handle multi-request architecture adapters.

* Added a unit test for the delayExecution function.

* Made newBidder a default import. Added some unit tests.

* Added more tests.

* Added more tests, and fixed a few bugs.

* Changed an error to a log message. Fixed a small bug.

* Did the no-brainer improvements from PR comments.

* Added spec-level support for aliases and mediaTypes. Aliases may still be broken.

* Added support for aliases. Added more tests

* Cleaned up some unnecessary code.

* Removed the GET/POST constants. Fixed some typos, and renamed some Request-related properties for consistency with the native object.

* Re-added some code for outstream rendering, which was apparently lost in the merges somewhere.

* Removed confusing use of this

* Fixed lint error

* Moved JSON parsing into the bidderFactory, and moved the JSDocs to the top.

* Removed placementCode from everywhere I could.
  • Loading branch information
dbemiller authored and dluxemburg committed Jul 17, 2018
1 parent 758ba75 commit 168372f
Show file tree
Hide file tree
Showing 8 changed files with 1,086 additions and 332 deletions.
636 changes: 308 additions & 328 deletions modules/appnexusAstBidAdapter.js

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/Renderer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { loadScript } from './adloader';
import * as utils from './utils';

/**
* @typedef {object} Renderer
*
* A Renderer stores some functions which are used to render a particular Bid.
* These are used in Outstream Video Bids, returned on the Bid by the adapter, and will
* be used to render that bid unless the Publisher overrides them.
*/

export function Renderer(options) {
const { url, config, id, callback, loaded } = options;
this.url = url;
Expand Down
302 changes: 302 additions & 0 deletions src/adapters/bidderFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
import Adapter from 'src/adapter';
import adaptermanager from 'src/adaptermanager';
import { ajax } from 'src/ajax';
import bidmanager from 'src/bidmanager';
import bidfactory from 'src/bidfactory';
import { STATUS } from 'src/constants';

import { logWarn, logError, parseQueryStringParameters, delayExecution } from 'src/utils';

/**
* This file aims to support Adapters during the Prebid 0.x -> 1.x transition.
*
* Prebid 1.x and Prebid 0.x will be in separate branches--perhaps for a long time.
* This function defines an API for adapter construction which is compatible with both versions.
* Adapters which use it can maintain their code in master, and only this file will need to change
* in the 1.x branch.
*
* Typical usage looks something like:
*
* const adapter = registerBidder({
* code: 'myBidderCode',
* aliases: ['alias1', 'alias2'],
* supportedMediaTypes: ['video', 'native'],
* areParamsValid: function(paramsObject) { return true/false },
* buildRequests: function(bidRequests) { return some ServerRequest(s) },
* interpretResponse: function(oneServerResponse) { return some Bids, or throw an error. }
* });
*
* @see BidderSpec for the full API and more thorough descriptions.
*/

/**
* @typedef {object} BidderSpec An object containing the adapter-specific functions needed to
* make a Bidder.
*
* @property {string} code A code which will be used to uniquely identify this bidder. This should be the same
* one as is used in the call to registerBidAdapter
* @property {string[]} [aliases] A list of aliases which should also resolve to this bidder.
* @property {MediaType[]} [supportedMediaTypes]: A list of Media Types which the adapter supports.
* @property {function(object): boolean} areParamsValid Determines whether or not the given object has all the params
* needed to make a valid request.
* @property {function(BidRequest[]): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server which
* requests Bids for the given array of Requests. Each BidRequest in the argument array is guaranteed to have
* a "params" property which has passed the areParamsValid() test
* @property {function(*): Bid[]} interpretResponse Given a successful response from the Server, interpret it
* and return the Bid objects. This function will be run inside a try/catch. If it throws any errors, your
* bids will be discarded.
* @property {function(SyncOptions, Array): UserSync[]} [getUserSyncs] Given an array of all the responses
* from the server, determine which user syncs should occur. The argument array will contain every element
* which has been sent through to interpretResponse. The order of syncs in this array matters. The most
* important ones should come first, since publishers may limit how many are dropped on their page.
*/

/**
* @typedef {object} BidRequest
*
* @property {string} bidId A string which uniquely identifies this BidRequest in the current Auction.
* @property {object} params Any bidder-specific params which the publisher used in their bid request.
* This is guaranteed to have passed the spec.areParamsValid() test.
*/

/**
* @typedef {object} ServerRequest
*
* @property {('GET'|'POST')} method The type of request which this is.
* @property {string} url The endpoint for the request. For example, "//bids.example.com".
* @property {string|object} data Data to be sent in the request.
* If this is a GET request, they'll become query params. If it's a POST request, they'll be added to the body.
* Strings will be added as-is. Objects will be unpacked into query params based on key/value mappings, or
* JSON-serialized into the Request body.
*/

/**
* @typedef {object} Bid
*
* @property {string} requestId The specific BidRequest which this bid is aimed at.
* This should correspond to one of the
* @property {string} ad A URL which can be used to load this ad, if it's chosen by the publisher.
* @property {number} cpm The bid price, in US cents per thousand impressions.
* @property {number} height The height of the ad, in pixels.
* @property {number} width The width of the ad, in pixels.
*
* @property [Renderer] renderer A Renderer which can be used as a default for this bid,
* if the publisher doesn't override it. This is only relevant for Outstream Video bids.
*/

/**
* @typedef {Object} SyncOptions
*
* An object containing information about usersyncs which the adapter should obey.
*
* @property {boolean} iframeEnabled True if iframe usersyncs are allowed, and false otherwise
* @property {boolean} pixelEnabled True if image usersyncs are allowed, and false otherwise
*/

/**
* TODO: Move this to the UserSync module after that PR is merged.
*
* @typedef {object} UserSync
*
* @property {('image'|'iframe')} type The type of user sync to be done.
* @property {string} url The URL which makes the sync happen.
*/

/**
* Register a bidder with prebid, using the given spec.
*
* If possible, Adapter modules should use this function instead of adaptermanager.registerBidAdapter().
*
* @param {BidderSpec} spec An object containing the bare-bones functions we need to make a Bidder.
*/
export function registerBidder(spec) {
const mediaTypes = Array.isArray(spec.supportedMediaTypes)
? { supportedMediaTypes: spec.supportedMediaTypes }
: undefined;
function putBidder(spec) {
const bidder = newBidder(spec);
adaptermanager.registerBidAdapter(bidder, spec.code, mediaTypes);
}

putBidder(spec);
if (Array.isArray(spec.aliases)) {
spec.aliases.forEach(alias => {
putBidder(Object.assign({}, spec, { code: alias }));
});
}
}

/**
* Make a new bidder from the given spec. This is exported mainly for testing.
* Adapters will probably find it more convenient to use registerBidder instead.
*
* @param {BidderSpec} spec
*/
export function newBidder(spec) {
return Object.assign(new Adapter(spec.code), {
callBids: function(bidderRequest) {
if (!Array.isArray(bidderRequest.bids)) {
return;
}

// callBids must add a NO_BID response for _every_ AdUnit code, in order for the auction to
// end properly. This map stores placement codes which we've made _real_ bids on.
//
// As we add _real_ bids to the bidmanager, we'll log the ad unit codes here too. Once all the real
// bids have been added, fillNoBids() can be called to add NO_BID bids for any extra ad units, which
// will end the auction.
//
// In Prebid 1.0, this will be simplified to use the `addBidResponse` and `done` callbacks.
const adUnitCodesHandled = {};
function addBidWithCode(adUnitCode, bid) {
adUnitCodesHandled[adUnitCode] = true;
bidmanager.addBidResponse(adUnitCode, bid);
}
function fillNoBids() {
bidderRequest.bids
.map(bidRequest => bidRequest.placementCode)
.forEach(adUnitCode => {
if (adUnitCode && !adUnitCodesHandled[adUnitCode]) {
bidmanager.addBidResponse(adUnitCode, newEmptyBid());
}
});
}

const bidRequests = bidderRequest.bids.filter(filterAndWarn);
if (bidRequests.length === 0) {
fillNoBids();
return;
}
const bidRequestMap = {};
bidRequests.forEach(bid => {
bidRequestMap[bid.bidId] = bid;
});

let requests = spec.buildRequests(bidRequests);
if (!requests || requests.length === 0) {
fillNoBids();
return;
}
if (!Array.isArray(requests)) {
requests = [requests];
}

// After all the responses have come back, fill up the "no bid" bids and
// register any required usersync pixels.
const responses = [];
function afterAllResponses() {
fillNoBids();

if (spec.getUserSyncs) {
// TODO: Before merge, replace this empty object with the real config values.
// Then register them with the UserSync pool. This is waiting on the UserSync PR
// to be merged first, though.
spec.getUserSyncs({ }, responses);
}
}

// Callbacks don't compose as nicely as Promises. We should call fillNoBids() once _all_ the
// Server requests have returned and been processed. Since `ajax` accepts a single callback,
// we need to rig up a function which only executes after all the requests have been responded.
const onResponse = delayExecution(afterAllResponses, requests.length)
requests.forEach(processRequest);

function processRequest(request) {
switch (request.method) {
case 'GET':
ajax(
`${request.url}?${parseQueryStringParameters(request.data)}`,
{
success: onSuccess,
error: onFailure
},
undefined,
{
method: 'GET',
withCredentials: true
}
);
break;
case 'POST':
ajax(
request.url,
{
success: onSuccess,
error: onFailure
},
typeof request.data === 'string' ? request.data : JSON.stringify(request.data),
{
method: 'POST',
contentType: 'text/plain',
withCredentials: true
}
);
break;
default:
logWarn(`Skipping invalid request from ${spec.code}. Request type ${request.type} must be GET or POST`);
onResponse();
}
}

// If the server responds successfully, use the adapter code to unpack the Bids from it.
// If the adapter code fails, no bids should be added. After all the bids have been added, make
// sure to call the `onResponse` function so that we're one step closer to calling fillNoBids().
function onSuccess(response) {
try {
response = JSON.parse(response);
} catch (e) { /* response might not be JSON... that's ok. */ }
responses.push(response);

let bids;
try {
bids = spec.interpretResponse(response);
} catch (err) {
logError(`Bidder ${spec.code} failed to interpret the server's response. Continuing without bids`, null, err);
onResponse();
return;
}

if (bids) {
if (bids.forEach) {
bids.forEach(addBidUsingRequestMap);
} else {
addBidUsingRequestMap(bids);
}
}
onResponse();

function addBidUsingRequestMap(bid) {
const bidRequest = bidRequestMap[bid.requestId];
if (bidRequest) {
const prebidBid = Object.assign(bidfactory.createBid(STATUS.GOOD, bidRequest), bid);
addBidWithCode(bidRequest.placementCode, prebidBid);
} else {
logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`);
}
}
}

// If the server responds with an error, there's not much we can do. Log it, and make sure to
// call onResponse() so that we're one step closer to calling fillNoBids().
function onFailure(err) {
logError(`Server call for ${spec.code} failed: ${err}. Continuing without bids.`);
onResponse();
}
}
});

function filterAndWarn(bid) {
if (!spec.areParamsValid(bid.params)) {
logWarn(`Invalid bid sent to bidder ${spec.code}: ${JSON.stringify(bid)}`);
return false;
}
return true;
}

function newEmptyBid() {
const bid = bidfactory.createBid(STATUS.NO_BID);
bid.code = spec.code;
bid.bidderCode = spec.code;
return bid;
}
}
17 changes: 17 additions & 0 deletions src/mediaTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* This file contains the valid Media Types in Prebid.
*
* All adapters are assumed to support banner ads. Other media types are specified by Adapters when they
* register themselves with prebid-core.
*/

/**
* @typedef {('native'|'video'|'banner')} MediaType
*/

/** @type MediaType */
export const NATIVE = 'native';
/** @type MediaType */
export const VIDEO = 'video';
/** @type MediaType */
export const BANNER = 'banner';
24 changes: 24 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,31 @@ export function getBidderRequest(bidder, adUnitCode) {
}

/**
* Given a function, return a function which only executes the original after
* it's been called numRequiredCalls times.
*
* Note that the arguments from the previous calls will *not* be forwarded to the original function.
* Only the final call's arguments matter.
*
* @param {function} func The function which should be executed, once the returned function has been executed
* numRequiredCalls times.
* @param {int} numRequiredCalls The number of times which the returned function needs to be called before
* func is.
*/
export function delayExecution(func, numRequiredCalls) {
if (numRequiredCalls < 1) {
throw new Error(`numRequiredCalls must be a positive number. Got ${numRequiredCalls}`);
}
let numCalls = 0;
return function () {
numCalls++;
if (numCalls === numRequiredCalls) {
func.apply(null, arguments);
}
}
}

/**
* https://stackoverflow.com/a/34890276/428704
* @export
* @param {array} xs
Expand Down
7 changes: 3 additions & 4 deletions test/spec/modules/appnexusAstBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import Adapter from 'modules/appnexusAstBidAdapter';
import { spec } from 'modules/appnexusAstBidAdapter';
import { newBidder } from 'src/adapters/bidderFactory';
import bidmanager from 'src/bidmanager';

const ENDPOINT = '//ib.adnxs.com/ut/v3/prebid';
Expand Down Expand Up @@ -61,9 +62,7 @@ const RESPONSE = {
};

describe('AppNexusAdapter', () => {
let adapter;

beforeEach(() => adapter = new Adapter());
const adapter = newBidder(spec);

describe('request function', () => {
let xhr;
Expand Down
Loading

0 comments on commit 168372f

Please sign in to comment.