Skip to content

Commit

Permalink
Prebid core: automatic dynamic loading of debugging module (prebid#8106)
Browse files Browse the repository at this point in the history
* WIP: move all debugging logic to the debugging module

* Dynamic loading of modules

* Move all debugging logic to the debugging module

* Automatically load debugging module when needed

* Build and dynamically load "standalone" version of debugging module

* add note to PR_REVIEW

* use loadExternalScript; only add debugging hooks when needed
  • Loading branch information
dgirardi authored and bwhisp committed Jul 13, 2022
1 parent b4e02c1 commit 697de49
Show file tree
Hide file tree
Showing 22 changed files with 697 additions and 448 deletions.
4 changes: 4 additions & 0 deletions PR_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ Follow steps above for general review process. In addition:
- Consider whether the kind of data the module is obtaining could have privacy implications. If so, make sure they're utilizing the `consent` data passed to them.
- Make sure there's a docs pull request

### Reviewing changes to the `debugging` module

The debugging module cannot import from core in the same way that other modules can. See this [warning](https://github.com/prebid/Prebid.js/blob/master/modules/debugging/WARNING.md) for more details.

## Ticket Coordinator

Each week, Prebid Org assigns one person to keep an eye on incoming issues and PRs. Every Monday morning a reminder is sent to the prebid-js slack channel with a link to the spreadsheet. If you're on rotation, please check that list each Monday to see if you're on-duty.
Expand Down
6 changes: 3 additions & 3 deletions babelConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function useLocal(module) {
})
}

module.exports = function (test = false) {
module.exports = function (options = {}) {
return {
'presets': [
[
Expand All @@ -18,12 +18,12 @@ module.exports = function (test = false) {
'useBuiltIns': 'entry',
'corejs': '3.13.0',
// a lot of tests use sinon.stub & others that stopped working on ES6 modules with webpack 5
'modules': test ? 'commonjs' : 'auto',
'modules': options.test ? 'commonjs' : 'auto',
}
]
],
'plugins': [
path.resolve(__dirname, './plugins/pbjsGlobals.js'),
[path.resolve(__dirname, './plugins/pbjsGlobals.js'), options],
useLocal('babel-plugin-transform-object-assign'),
],
}
Expand Down
9 changes: 9 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ function makeDevpackPkg() {
devtool: 'source-map',
mode: 'development'
})

const babelConfig = require('./babelConfig.js')({prebidDistUrlBase: '/build/dev/'});

// update babel config to set local dist url
cloned.module.rules
.flatMap((rule) => rule.use)
.filter((use) => use.loader === 'babel-loader')
.forEach((use) => use.options = Object.assign({}, use.options, babelConfig));

var externalModules = helpers.getArgModules();

const analyticsSources = helpers.getAnalyticsSources();
Expand Down
2 changes: 1 addition & 1 deletion karma.conf.maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function newWebpackConfig(codeCoverage) {
.flatMap((r) => r.use)
.filter((use) => use.loader === 'babel-loader')
.forEach((use) => {
use.options = babelConfig(true);
use.options = babelConfig({test: true});
});

if (codeCoverage) {
Expand Down
9 changes: 9 additions & 0 deletions modules/debugging/WARNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Warning

This module is also packaged as a "standalone" .js file and loaded dynamically by prebid-core when debugging configuration is passed to `setConfig` or loaded from session storage.

"Standalone" means that it does not have a compile-time dependency on `prebid-core.js` and can therefore work even if it was not built together with it (as would be the case when Prebid is pulled from npm).

Because of this, **this module cannot freely import symbols from core**: anything that depends on Prebid global state (which includes, but is not limited to, `config`, `auctionManager`, `adapterManager`, etc) would *not* work as expected.

Imports must be limited to logic that is stateless and free of side effects; symbols from `utils.js` are mostly OK, with the notable exception of logging functions (which have a dependency on `config`).
13 changes: 6 additions & 7 deletions modules/debugging/bidInterceptor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import {
deepClone,
deepEqual,
delayExecution,
prefixLog,
mergeDeep
} from '../../src/utils.js';
const { logMessage, logWarn, logError } = prefixLog('DEBUG:');

/**
* @typedef {Number|String|boolean|null|undefined} Scalar
*/

export function BidInterceptor(opts = {}) {
({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts);
this.logger = opts.logger;
this.rules = [];
}

Expand All @@ -22,10 +21,10 @@ Object.assign(BidInterceptor.prototype, {
delay: 0
},
serializeConfig(ruleDefs) {
function isSerializable(ruleDef, i) {
const isSerializable = (ruleDef, i) => {
const serializable = deepEqual(ruleDef, JSON.parse(JSON.stringify(ruleDef)), {checkTypes: true});
if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) {
logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef);
this.logger.logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef);
}
return serializable;
}
Expand Down Expand Up @@ -79,7 +78,7 @@ Object.assign(BidInterceptor.prototype, {
return matchDef;
}
if (typeof matchDef !== 'object') {
logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`);
this.logger.logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`);
return () => false;
}
function matches(candidate, {ref = matchDef, args = []}) {
Expand Down Expand Up @@ -119,7 +118,7 @@ Object.assign(BidInterceptor.prototype, {
if (typeof replDef === 'function') {
replFn = ({args}) => replDef(...args);
} else if (typeof replDef !== 'object') {
logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`);
this.logger.logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`);
replFn = () => ({});
} else {
replFn = ({args, ref = replDef}) => {
Expand Down Expand Up @@ -213,7 +212,7 @@ Object.assign(BidInterceptor.prototype, {
matches.forEach((match) => {
const mockResponse = match.rule.replace(match.bid, bidRequest);
const delay = match.rule.options.delay;
logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse)
this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse)
this.setTimeout(() => {
addBid(mockResponse, match.bid);
callDone();
Expand Down
109 changes: 109 additions & 0 deletions modules/debugging/debugging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {deepClone, delayExecution} from '../../src/utils.js';
import {BidInterceptor} from './bidInterceptor.js';
import {makePbsInterceptor} from './pbsInterceptor.js';
import {addHooks, removeHooks} from './legacy.js';

const interceptorHooks = [];
let bidInterceptor;
let enabled = false;

function enableDebugging(debugConfig, {fromSession = false, config, hook, logger}) {
config.setConfig({debug: true});
bidInterceptor.updateConfig(debugConfig);
resetHooks(true);
// also enable "legacy" overrides
removeHooks({hook});
addHooks(debugConfig, {hook, logger});
if (!enabled) {
enabled = true;
logger.logMessage(`Debug overrides enabled${fromSession ? ' from session' : ''}`);
}
}

export function disableDebugging({hook, logger}) {
bidInterceptor.updateConfig(({}));
resetHooks(false);
// also disable "legacy" overrides
removeHooks({hook});
if (enabled) {
enabled = false;
logger.logMessage('Debug overrides disabled');
}
}

function saveDebuggingConfig(debugConfig, {sessionStorage = window.sessionStorage, DEBUG_KEY} = {}) {
if (!debugConfig.enabled) {
try {
sessionStorage.removeItem(DEBUG_KEY);
} catch (e) {
}
} else {
if (debugConfig.intercept) {
debugConfig = deepClone(debugConfig);
debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept);
}
try {
sessionStorage.setItem(DEBUG_KEY, JSON.stringify(debugConfig));
} catch (e) {
}
}
}

export function getConfig(debugging, {sessionStorage = window.sessionStorage, DEBUG_KEY, config, hook, logger} = {}) {
if (debugging == null) return;
saveDebuggingConfig(debugging, {sessionStorage, DEBUG_KEY});
if (!debugging.enabled) {
disableDebugging({hook, logger});
} else {
enableDebugging(debugging, {config, hook, logger});
}
}

export function sessionLoader({DEBUG_KEY, storage, config, hook, logger}) {
let overrides;
try {
storage = storage || window.sessionStorage;
overrides = JSON.parse(storage.getItem(DEBUG_KEY));
} catch (e) {
}
if (overrides) {
enableDebugging(overrides, {fromSession: true, config, hook, logger});
}
}

function resetHooks(enable) {
interceptorHooks.forEach(([getHookFn, interceptor]) => {
getHookFn().getHooks({hook: interceptor}).remove();
});
if (enable) {
interceptorHooks.forEach(([getHookFn, interceptor]) => {
getHookFn().before(interceptor);
});
}
}

function registerBidInterceptor(getHookFn, interceptor) {
const interceptBids = (...args) => bidInterceptor.intercept(...args);
interceptorHooks.push([getHookFn, function (next, ...args) {
interceptor(next, interceptBids, ...args);
}]);
}

export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) {
const done = delayExecution(cbs.onCompletion, 2);
({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done}));
if (bids.length === 0) {
done();
} else {
next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done});
}
}

export function install({DEBUG_KEY, config, hook, createBid, logger}) {
bidInterceptor = new BidInterceptor({logger});
const pbsBidInterceptor = makePbsInterceptor({createBid});
registerBidInterceptor(() => hook.get('processBidderRequests'), bidderBidInterceptor);
registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor);
sessionLoader({DEBUG_KEY, config, hook, logger});
config.getConfig('debugging', ({debugging}) => getConfig(debugging, {DEBUG_KEY, config, hook, logger}), {init: true});
}
65 changes: 5 additions & 60 deletions modules/debugging/index.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,7 @@
import {deepClone, delayExecution} from '../../src/utils.js';
import {processBidderRequests} from '../../src/adapters/bidderFactory.js';
import {BidInterceptor} from './bidInterceptor.js';
import {config} from '../../src/config.js';
import {hook} from '../../src/hook.js';
import {pbsBidInterceptor} from './pbsInterceptor.js';
import {
onDisableOverrides,
onEnableOverrides,
saveDebuggingConfig
} from '../../src/debugging.js';
import {install} from './debugging.js';
import {prefixLog} from '../../src/utils.js';
import {createBid} from '../../src/bidfactory.js';

const interceptorHooks = [];
const bidInterceptor = new BidInterceptor();

saveDebuggingConfig.before(function (next, debugConfig, ...args) {
if (debugConfig.intercept) {
debugConfig = deepClone(debugConfig);
debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept);
}
next(debugConfig, ...args);
});

function resetHooks(enable) {
interceptorHooks.forEach(([getHookFn, interceptor]) => {
getHookFn().getHooks({hook: interceptor}).remove();
});
if (enable) {
interceptorHooks.forEach(([getHookFn, interceptor]) => {
getHookFn().before(interceptor);
})
}
}

onEnableOverrides.push((overrides) => {
bidInterceptor.updateConfig(overrides);
resetHooks(true);
});

onDisableOverrides.push(() => {
bidInterceptor.updateConfig({});
resetHooks(false);
})

function registerBidInterceptor(getHookFn, interceptor) {
const interceptBids = (...args) => bidInterceptor.intercept(...args);
interceptorHooks.push([getHookFn, function (next, ...args) {
interceptor(next, interceptBids, ...args)
}]);
}

export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) {
const done = delayExecution(cbs.onCompletion, 2);
({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done}));
if (bids.length === 0) {
done();
} else {
next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done});
}
}

registerBidInterceptor(() => processBidderRequests, bidderBidInterceptor);
registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor);
install({config, hook, createBid, logger: prefixLog('DEBUG:')});
Loading

0 comments on commit 697de49

Please sign in to comment.