Skip to content

Commit da46bf2

Browse files
authored
Prebid core: automatic dynamic loading of debugging module (prebid#8106)
* 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
1 parent c31d51a commit da46bf2

22 files changed

+697
-448
lines changed

PR_REVIEW.md

+4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ Follow steps above for general review process. In addition:
128128
- 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.
129129
- Make sure there's a docs pull request
130130

131+
### Reviewing changes to the `debugging` module
132+
133+
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.
134+
131135
## Ticket Coordinator
132136

133137
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.

babelConfig.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function useLocal(module) {
99
})
1010
}
1111

12-
module.exports = function (test = false) {
12+
module.exports = function (options = {}) {
1313
return {
1414
'presets': [
1515
[
@@ -18,12 +18,12 @@ module.exports = function (test = false) {
1818
'useBuiltIns': 'entry',
1919
'corejs': '3.13.0',
2020
// a lot of tests use sinon.stub & others that stopped working on ES6 modules with webpack 5
21-
'modules': test ? 'commonjs' : 'auto',
21+
'modules': options.test ? 'commonjs' : 'auto',
2222
}
2323
]
2424
],
2525
'plugins': [
26-
path.resolve(__dirname, './plugins/pbjsGlobals.js'),
26+
[path.resolve(__dirname, './plugins/pbjsGlobals.js'), options],
2727
useLocal('babel-plugin-transform-object-assign'),
2828
],
2929
}

gulpfile.js

+9
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ function makeDevpackPkg() {
119119
devtool: 'source-map',
120120
mode: 'development'
121121
})
122+
123+
const babelConfig = require('./babelConfig.js')({prebidDistUrlBase: '/build/dev/'});
124+
125+
// update babel config to set local dist url
126+
cloned.module.rules
127+
.flatMap((rule) => rule.use)
128+
.filter((use) => use.loader === 'babel-loader')
129+
.forEach((use) => use.options = Object.assign({}, use.options, babelConfig));
130+
122131
var externalModules = helpers.getArgModules();
123132

124133
const analyticsSources = helpers.getAnalyticsSources();

karma.conf.maker.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function newWebpackConfig(codeCoverage) {
2222
.flatMap((r) => r.use)
2323
.filter((use) => use.loader === 'babel-loader')
2424
.forEach((use) => {
25-
use.options = babelConfig(true);
25+
use.options = babelConfig({test: true});
2626
});
2727

2828
if (codeCoverage) {

modules/debugging/WARNING.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
## Warning
2+
3+
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.
4+
5+
"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).
6+
7+
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.
8+
9+
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`).

modules/debugging/bidInterceptor.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ import {
33
deepClone,
44
deepEqual,
55
delayExecution,
6-
prefixLog,
76
mergeDeep
87
} from '../../src/utils.js';
9-
const { logMessage, logWarn, logError } = prefixLog('DEBUG:');
108

119
/**
1210
* @typedef {Number|String|boolean|null|undefined} Scalar
1311
*/
1412

1513
export function BidInterceptor(opts = {}) {
1614
({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts);
15+
this.logger = opts.logger;
1716
this.rules = [];
1817
}
1918

@@ -22,10 +21,10 @@ Object.assign(BidInterceptor.prototype, {
2221
delay: 0
2322
},
2423
serializeConfig(ruleDefs) {
25-
function isSerializable(ruleDef, i) {
24+
const isSerializable = (ruleDef, i) => {
2625
const serializable = deepEqual(ruleDef, JSON.parse(JSON.stringify(ruleDef)), {checkTypes: true});
2726
if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) {
28-
logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef);
27+
this.logger.logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef);
2928
}
3029
return serializable;
3130
}
@@ -79,7 +78,7 @@ Object.assign(BidInterceptor.prototype, {
7978
return matchDef;
8079
}
8180
if (typeof matchDef !== 'object') {
82-
logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`);
81+
this.logger.logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`);
8382
return () => false;
8483
}
8584
function matches(candidate, {ref = matchDef, args = []}) {
@@ -119,7 +118,7 @@ Object.assign(BidInterceptor.prototype, {
119118
if (typeof replDef === 'function') {
120119
replFn = ({args}) => replDef(...args);
121120
} else if (typeof replDef !== 'object') {
122-
logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`);
121+
this.logger.logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`);
123122
replFn = () => ({});
124123
} else {
125124
replFn = ({args, ref = replDef}) => {
@@ -213,7 +212,7 @@ Object.assign(BidInterceptor.prototype, {
213212
matches.forEach((match) => {
214213
const mockResponse = match.rule.replace(match.bid, bidRequest);
215214
const delay = match.rule.options.delay;
216-
logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse)
215+
this.logger.logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse)
217216
this.setTimeout(() => {
218217
addBid(mockResponse, match.bid);
219218
callDone();

modules/debugging/debugging.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {deepClone, delayExecution} from '../../src/utils.js';
2+
import {BidInterceptor} from './bidInterceptor.js';
3+
import {makePbsInterceptor} from './pbsInterceptor.js';
4+
import {addHooks, removeHooks} from './legacy.js';
5+
6+
const interceptorHooks = [];
7+
let bidInterceptor;
8+
let enabled = false;
9+
10+
function enableDebugging(debugConfig, {fromSession = false, config, hook, logger}) {
11+
config.setConfig({debug: true});
12+
bidInterceptor.updateConfig(debugConfig);
13+
resetHooks(true);
14+
// also enable "legacy" overrides
15+
removeHooks({hook});
16+
addHooks(debugConfig, {hook, logger});
17+
if (!enabled) {
18+
enabled = true;
19+
logger.logMessage(`Debug overrides enabled${fromSession ? ' from session' : ''}`);
20+
}
21+
}
22+
23+
export function disableDebugging({hook, logger}) {
24+
bidInterceptor.updateConfig(({}));
25+
resetHooks(false);
26+
// also disable "legacy" overrides
27+
removeHooks({hook});
28+
if (enabled) {
29+
enabled = false;
30+
logger.logMessage('Debug overrides disabled');
31+
}
32+
}
33+
34+
function saveDebuggingConfig(debugConfig, {sessionStorage = window.sessionStorage, DEBUG_KEY} = {}) {
35+
if (!debugConfig.enabled) {
36+
try {
37+
sessionStorage.removeItem(DEBUG_KEY);
38+
} catch (e) {
39+
}
40+
} else {
41+
if (debugConfig.intercept) {
42+
debugConfig = deepClone(debugConfig);
43+
debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept);
44+
}
45+
try {
46+
sessionStorage.setItem(DEBUG_KEY, JSON.stringify(debugConfig));
47+
} catch (e) {
48+
}
49+
}
50+
}
51+
52+
export function getConfig(debugging, {sessionStorage = window.sessionStorage, DEBUG_KEY, config, hook, logger} = {}) {
53+
if (debugging == null) return;
54+
saveDebuggingConfig(debugging, {sessionStorage, DEBUG_KEY});
55+
if (!debugging.enabled) {
56+
disableDebugging({hook, logger});
57+
} else {
58+
enableDebugging(debugging, {config, hook, logger});
59+
}
60+
}
61+
62+
export function sessionLoader({DEBUG_KEY, storage, config, hook, logger}) {
63+
let overrides;
64+
try {
65+
storage = storage || window.sessionStorage;
66+
overrides = JSON.parse(storage.getItem(DEBUG_KEY));
67+
} catch (e) {
68+
}
69+
if (overrides) {
70+
enableDebugging(overrides, {fromSession: true, config, hook, logger});
71+
}
72+
}
73+
74+
function resetHooks(enable) {
75+
interceptorHooks.forEach(([getHookFn, interceptor]) => {
76+
getHookFn().getHooks({hook: interceptor}).remove();
77+
});
78+
if (enable) {
79+
interceptorHooks.forEach(([getHookFn, interceptor]) => {
80+
getHookFn().before(interceptor);
81+
});
82+
}
83+
}
84+
85+
function registerBidInterceptor(getHookFn, interceptor) {
86+
const interceptBids = (...args) => bidInterceptor.intercept(...args);
87+
interceptorHooks.push([getHookFn, function (next, ...args) {
88+
interceptor(next, interceptBids, ...args);
89+
}]);
90+
}
91+
92+
export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) {
93+
const done = delayExecution(cbs.onCompletion, 2);
94+
({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done}));
95+
if (bids.length === 0) {
96+
done();
97+
} else {
98+
next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done});
99+
}
100+
}
101+
102+
export function install({DEBUG_KEY, config, hook, createBid, logger}) {
103+
bidInterceptor = new BidInterceptor({logger});
104+
const pbsBidInterceptor = makePbsInterceptor({createBid});
105+
registerBidInterceptor(() => hook.get('processBidderRequests'), bidderBidInterceptor);
106+
registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor);
107+
sessionLoader({DEBUG_KEY, config, hook, logger});
108+
config.getConfig('debugging', ({debugging}) => getConfig(debugging, {DEBUG_KEY, config, hook, logger}), {init: true});
109+
}

modules/debugging/index.js

+5-60
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,7 @@
1-
import {deepClone, delayExecution} from '../../src/utils.js';
2-
import {processBidderRequests} from '../../src/adapters/bidderFactory.js';
3-
import {BidInterceptor} from './bidInterceptor.js';
1+
import {config} from '../../src/config.js';
42
import {hook} from '../../src/hook.js';
5-
import {pbsBidInterceptor} from './pbsInterceptor.js';
6-
import {
7-
onDisableOverrides,
8-
onEnableOverrides,
9-
saveDebuggingConfig
10-
} from '../../src/debugging.js';
3+
import {install} from './debugging.js';
4+
import {prefixLog} from '../../src/utils.js';
5+
import {createBid} from '../../src/bidfactory.js';
116

12-
const interceptorHooks = [];
13-
const bidInterceptor = new BidInterceptor();
14-
15-
saveDebuggingConfig.before(function (next, debugConfig, ...args) {
16-
if (debugConfig.intercept) {
17-
debugConfig = deepClone(debugConfig);
18-
debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept);
19-
}
20-
next(debugConfig, ...args);
21-
});
22-
23-
function resetHooks(enable) {
24-
interceptorHooks.forEach(([getHookFn, interceptor]) => {
25-
getHookFn().getHooks({hook: interceptor}).remove();
26-
});
27-
if (enable) {
28-
interceptorHooks.forEach(([getHookFn, interceptor]) => {
29-
getHookFn().before(interceptor);
30-
})
31-
}
32-
}
33-
34-
onEnableOverrides.push((overrides) => {
35-
bidInterceptor.updateConfig(overrides);
36-
resetHooks(true);
37-
});
38-
39-
onDisableOverrides.push(() => {
40-
bidInterceptor.updateConfig({});
41-
resetHooks(false);
42-
})
43-
44-
function registerBidInterceptor(getHookFn, interceptor) {
45-
const interceptBids = (...args) => bidInterceptor.intercept(...args);
46-
interceptorHooks.push([getHookFn, function (next, ...args) {
47-
interceptor(next, interceptBids, ...args)
48-
}]);
49-
}
50-
51-
export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) {
52-
const done = delayExecution(cbs.onCompletion, 2);
53-
({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done}));
54-
if (bids.length === 0) {
55-
done();
56-
} else {
57-
next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done});
58-
}
59-
}
60-
61-
registerBidInterceptor(() => processBidderRequests, bidderBidInterceptor);
62-
registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor);
7+
install({config, hook, createBid, logger: prefixLog('DEBUG:')});

0 commit comments

Comments
 (0)