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

Prebid core: automatic dynamic loading of debugging module #8106

Merged
merged 10 commits into from
Jun 14, 2022
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 @@ -118,6 +118,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`).
Comment on lines +7 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no import guards that can be setup at build time, then this may be something to mention in the PR_REVIEW doc for others to be on the look out for.

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