Skip to content

Commit

Permalink
PBJS-ORTB conversion library (#8738)
Browse files Browse the repository at this point in the history
* Automatically resolve libraries and their dependencies

* Move AnalyticsAdapter under libraries

* PBJS-ORTB conversion library

* Update ortbConverter README

* Improvements on request generation

* refactor openxOrtbBidAdapter

* Update README

* Filter out imps that have no id

* Fix nativeMapper logic

* Prebid core: simplify native ORTB logic

* Remove nativeMapper; do not mix ORTB request and response

* fix lint

* set bidRequest.nativeParams after ortb -> legacy conversion

* Fix native trackers for native ortb responses (including Prebid Server)

* Clean up native processors; add s2sConfig.ortbNative

* Make ext.prebid.floors.enabled respect first party data

* do not set placement for oustream video

* Improvements

* convert improvedigitalBidAdapter

* Update openxOrtbBidAdapter

* Improve video validation logic

* Move skip validation back to improvedigital

* Update README

* Override (not merge) context.nativeRequest

* Update improvedigital adapter

* Address PR feedback on improvedigital

* Fix tmax to s2sConfig.timeout for PBS adapter

* PBS adapter: set source.ext.schain with the most commonly used schain

* Remove dead code

* Use auctionId for source.tid

* Update package-lock

* Set source.tid to auctionId

* Update README

* Update README

* Update README
  • Loading branch information
dgirardi authored Oct 21, 2022
1 parent 0c69238 commit 21712ca
Show file tree
Hide file tree
Showing 55 changed files with 3,970 additions and 1,864 deletions.
378 changes: 378 additions & 0 deletions libraries/ortbConverter/README.md

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions libraries/ortbConverter/converter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {compose} from './lib/composer.js';
import {logError, memoize} from '../../src/utils.js';
import {DEFAULT_PROCESSORS} from './processors/default.js';
import {BID_RESPONSE, DEFAULT, getProcessors, IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js';
import {mergeProcessors} from './lib/mergeProcessors.js';

export function ortbConverter({
context: defaultContext = {},
processors = defaultProcessors,
overrides = {},
imp,
request,
bidResponse,
response,
} = {}) {
const REQ_CTX = new WeakMap();

function builder(slot, wrapperFn, builderFn, errorHandler) {
let build;
return function () {
if (build == null) {
build = (function () {
let delegate = builderFn.bind(this, compose(processors()[slot] || {}, overrides[slot] || {}));
if (wrapperFn) {
delegate = wrapperFn.bind(this, delegate);
}
return function () {
try {
return delegate.apply(this, arguments);
} catch (e) {
errorHandler.call(this, e, ...arguments);
}
}
})();
}
return build.apply(this, arguments);
}
}

const buildImp = builder(IMP, imp,
function (process, bidRequest, context) {
const imp = {};
process(imp, bidRequest, context);
return imp;
},
function (error, bidRequest, context) {
logError('Error while converting bidRequest to ORTB imp; request skipped.', {error, bidRequest, context});
}
);

const buildRequest = builder(REQUEST, request,
function (process, imps, bidderRequest, context) {
const ortbRequest = {imp: imps};
process(ortbRequest, bidderRequest, context);
return ortbRequest;
},
function (error, imps, bidderRequest, context) {
logError('Error while converting to ORTB request', {error, imps, bidderRequest, context});
throw error;
}
);

const buildBidResponse = builder(BID_RESPONSE, bidResponse,
function (process, bid, context) {
const bidResponse = {};
process(bidResponse, bid, context);
return bidResponse;
},
function (error, bid, context) {
logError('Error while converting ORTB seatbid.bid to bidResponse; bid skipped.', {error, bid, context});
}
);

const buildResponse = builder(RESPONSE, response,
function (process, bidResponses, ortbResponse, context) {
const response = {bids: bidResponses};
process(response, ortbResponse, context);
return response;
},
function (error, bidResponses, ortbResponse, context) {
logError('Error while converting from ORTB response', {error, bidResponses, ortbResponse, context});
throw error;
}
);

return {
toORTB({bidderRequest, bidRequests, context = {}}) {
bidRequests = bidRequests || bidderRequest.bids;
const ctx = {
req: Object.assign({bidRequests}, defaultContext, context),
imp: {}
}
const imps = bidRequests.map(bidRequest => {
const impContext = Object.assign({bidderRequest, reqContext: ctx.req}, defaultContext, context);
const result = buildImp(bidRequest, impContext);
if (result != null) {
if (result.hasOwnProperty('id')) {
impContext.bidRequest = bidRequest;
ctx.imp[result.id] = impContext;
return result;
}
logError('Converted ORTB imp does not specify an id, ignoring bid request', bidRequest, result);
}
}).filter(Boolean);

const request = buildRequest(imps, bidderRequest, ctx.req);
ctx.req.bidderRequest = bidderRequest;
if (request != null) {
REQ_CTX.set(request, ctx);
}
return request;
},
fromORTB({request, response}) {
const ctx = REQ_CTX.get(request);
if (ctx == null) {
throw new Error('ortbRequest passed to `fromORTB` must be the same object returned by `toORTB`')
}
function augmentContext(ctx, extraParams = {}) {
return Object.assign({ortbRequest: request}, extraParams, ctx);
}
const impsById = Object.fromEntries((request.imp || []).map(imp => [imp.id, imp]));
const bidResponses = (response.seatbid || []).flatMap(seatbid =>
(seatbid.bid || []).map((bid) => {
if (impsById.hasOwnProperty(bid.impid) && ctx.imp.hasOwnProperty(bid.impid)) {
return buildBidResponse(bid, augmentContext(ctx.imp[bid.impid], {imp: impsById[bid.impid], seatbid, ortbResponse: response}));
}
logError('ORTB response seatbid[].bid[].impid does not match any imp in request; ignoring bid', bid);
})
).filter(Boolean);
return buildResponse(bidResponses, response, augmentContext(ctx.req));
}
}
}

export const defaultProcessors = memoize(() => mergeProcessors(DEFAULT_PROCESSORS, getProcessors(DEFAULT)));
43 changes: 43 additions & 0 deletions libraries/ortbConverter/lib/composer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const SORTED = new WeakMap();

/**
* @typedef {Object} Component
* A component function, that can be composed with other compatible functions into one.
* Compatible functions take the same arguments; return values are ignored.
*
* @property {function} fn the component function;
* @property {number} priority determines the order in which this function will run when composed with others.
*/

/**
*
* @param {Object[string, Component]} components to compose
* @param {Object[string, function|boolean]} overrides a map from component name, to a function that should override that component.
* Override functions are replacements, except that they get the original function they are overriding as their first argument. If the override
* is `false`, the component is disabled.
*
* @return a function that will run all components in order of priority, with functions from `overrides` taking
* precedence over components that match names
*/
export function compose(components, overrides = {}) {
if (!SORTED.has(components)) {
const sorted = Object.entries(components);
sorted.sort((a, b) => {
a = a[1].priority || 0;
b = b[1].priority || 0;
return a === b ? 0 : a > b ? -1 : 1
});
SORTED.set(components, sorted.map(([name, cmp]) => [name, cmp.fn]))
}
const fns = SORTED.get(components)
.filter(([name]) => !overrides.hasOwnProperty(name) || overrides[name])
.map(function ([name, fn]) {
return overrides.hasOwnProperty(name) ? overrides[name].bind(this, fn) : fn;
});
return function () {
const args = Array.from(arguments);
fns.forEach(fn => {
fn.apply(this, args);
})
}
}
9 changes: 9 additions & 0 deletions libraries/ortbConverter/lib/mergeProcessors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {PROCESSOR_TYPES} from '../../../src/pbjsORTB.js';

export function mergeProcessors(...processors) {
const left = processors.shift();
const right = processors.length > 1 ? mergeProcessors(...processors) : processors[0];
return Object.fromEntries(
PROCESSOR_TYPES.map(type => [type, Object.assign({}, left[type], right[type])])
)
}
14 changes: 14 additions & 0 deletions libraries/ortbConverter/lib/sizes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {parseSizesInput} from '../../../src/utils.js';

export function sizesToFormat(sizes) {
sizes = parseSizesInput(sizes);

// get sizes in form [{ w: <int>, h: <int> }, ...]
return sizes.map(size => {
const [width, height] = size.split('x');
return {
w: parseInt(width, 10),
h: parseInt(height, 10)
};
});
}
40 changes: 40 additions & 0 deletions libraries/ortbConverter/processors/banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {createTrackPixelHtml, deepAccess, inIframe, mergeDeep} from '../../../src/utils.js';
import {BANNER} from '../../../src/mediaTypes.js';
import {sizesToFormat} from '../lib/sizes.js';

/**
* fill in a request `imp` with banner parameters from `bidRequest`.
*/
export function fillBannerImp(imp, bidRequest, context) {
if (context.mediaType && context.mediaType !== BANNER) return;

const bannerParams = deepAccess(bidRequest, 'mediaTypes.banner');
if (bannerParams) {
const banner = {
topframe: inIframe() === true ? 0 : 1
};
if (bannerParams.sizes) {
banner.format = sizesToFormat(bannerParams.sizes);
}
if (bannerParams.hasOwnProperty('pos')) {
banner.pos = bannerParams.pos;
}

imp.banner = mergeDeep(banner, imp.banner);
}
}

export function bannerResponseProcessor({createPixel = (url) => createTrackPixelHtml(decodeURIComponent(url))} = {}) {
return function fillBannerResponse(bidResponse, bid) {
if (bidResponse.mediaType === BANNER) {
if (bid.adm && bid.nurl) {
bidResponse.ad = bid.adm;
bidResponse.ad += createPixel(bid.nurl);
} else if (bid.adm) {
bidResponse.ad = bid.adm;
} else if (bid.nurl) {
bidResponse.adUrl = bid.nurl;
}
}
};
}
Loading

0 comments on commit 21712ca

Please sign in to comment.