repro-nest 0.0.214 → 0.0.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +60 -6
- package/dist/index.js +258 -26
- package/docs/tracing.md +71 -4
- package/package.json +1 -1
- package/src/index.ts +386 -38
- package/tracer/runtime.js +12 -5
package/dist/index.d.ts
CHANGED
|
@@ -42,23 +42,35 @@ type TracerApi = {
|
|
|
42
42
|
setFunctionLogsEnabled?: (enabled: boolean) => void;
|
|
43
43
|
};
|
|
44
44
|
type TraceEventPhase = 'enter' | 'exit';
|
|
45
|
-
export type TraceRulePattern = string | RegExp | Array<string | RegExp>;
|
|
45
|
+
export type TraceRulePattern = string | number | RegExp | Array<string | number | RegExp>;
|
|
46
46
|
export type TraceEventForFilter = {
|
|
47
47
|
type: TraceEventPhase;
|
|
48
48
|
eventType: TraceEventPhase;
|
|
49
49
|
functionType?: string | null;
|
|
50
50
|
fn?: string;
|
|
51
|
+
wrapperClass?: string | null;
|
|
51
52
|
file?: string | null;
|
|
53
|
+
line?: number | null;
|
|
52
54
|
depth?: number;
|
|
53
55
|
library?: string | null;
|
|
54
56
|
};
|
|
55
57
|
export type HeaderRule = string | RegExp;
|
|
56
58
|
export type HeaderCaptureOptions = {
|
|
57
|
-
/** When true, sensitive headers such as Authorization are kept; default
|
|
59
|
+
/** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
|
|
58
60
|
allowSensitiveHeaders?: boolean;
|
|
59
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Header names (string or RegExp) to mask.
|
|
63
|
+
* `dropHeaders` is kept for backward-compatibility and treated as an alias for `maskHeaders`.
|
|
64
|
+
*/
|
|
65
|
+
maskHeaders?: HeaderRule | HeaderRule[];
|
|
66
|
+
/** @deprecated Alias for {@link maskHeaders}. */
|
|
60
67
|
dropHeaders?: HeaderRule | HeaderRule[];
|
|
61
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Header names (string or RegExp) to keep unmasked, overriding defaults and `maskHeaders`.
|
|
70
|
+
* `keepHeaders` is kept for backward-compatibility and treated as an alias for `unmaskHeaders`.
|
|
71
|
+
*/
|
|
72
|
+
unmaskHeaders?: HeaderRule | HeaderRule[];
|
|
73
|
+
/** @deprecated Alias for {@link unmaskHeaders}. */
|
|
62
74
|
keepHeaders?: HeaderRule | HeaderRule[];
|
|
63
75
|
};
|
|
64
76
|
/** Lightweight helper to disable every trace emitted from specific files. */
|
|
@@ -78,8 +90,21 @@ export type DisableFunctionTraceRule = {
|
|
|
78
90
|
fn?: TraceRulePattern;
|
|
79
91
|
/** Function name (e.g. `"findOne"`, `/^UserService\./`). */
|
|
80
92
|
functionName?: TraceRulePattern;
|
|
81
|
-
/**
|
|
93
|
+
/** Shortcut for {@link wrapperClass}. */
|
|
94
|
+
wrapper?: TraceRulePattern;
|
|
95
|
+
/**
|
|
96
|
+
* Wrapper/owner name derived from {@link functionName} (e.g. `"UserService"` in `"UserService.create"`).
|
|
97
|
+
* Useful when multiple functions share the same method name.
|
|
98
|
+
*/
|
|
99
|
+
wrapperClass?: TraceRulePattern;
|
|
100
|
+
/** Alias for {@link wrapperClass}. */
|
|
101
|
+
className?: TraceRulePattern;
|
|
102
|
+
/** Alias for {@link wrapperClass}. */
|
|
103
|
+
owner?: TraceRulePattern;
|
|
104
|
+
/** Source filename reported by the trace event. */
|
|
82
105
|
file?: TraceRulePattern;
|
|
106
|
+
/** Line number reported by the trace event. */
|
|
107
|
+
line?: TraceRulePattern;
|
|
83
108
|
/** Shortcut for {@link library}. */
|
|
84
109
|
lib?: TraceRulePattern;
|
|
85
110
|
/** Library/package name inferred from the file path (e.g. `"mongoose"`). */
|
|
@@ -95,6 +120,33 @@ export type DisableFunctionTraceRule = {
|
|
|
95
120
|
};
|
|
96
121
|
export type DisableFunctionTracePredicate = (event: TraceEventForFilter) => boolean;
|
|
97
122
|
export type DisableFunctionTraceConfig = DisableFunctionTraceRule | DisableFunctionTracePredicate;
|
|
123
|
+
export type ReproMaskTarget = 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body' | 'trace.args' | 'trace.returnValue' | 'trace.error';
|
|
124
|
+
export type ReproMaskWhen = DisableFunctionTraceRule & {
|
|
125
|
+
/** Match HTTP method (e.g. `"GET"`, `/^post$/i`). */
|
|
126
|
+
method?: TraceRulePattern;
|
|
127
|
+
/** Match request path without query string (e.g. `"/api/auth/login"`). */
|
|
128
|
+
path?: TraceRulePattern;
|
|
129
|
+
/** Match normalized endpoint key (e.g. `"POST /api/auth/login"`). */
|
|
130
|
+
key?: TraceRulePattern;
|
|
131
|
+
};
|
|
132
|
+
export type ReproMaskRule = {
|
|
133
|
+
when?: ReproMaskWhen;
|
|
134
|
+
target: ReproMaskTarget | ReproMaskTarget[];
|
|
135
|
+
/**
|
|
136
|
+
* Dot/bracket paths to mask (supports `*`, `[0]`, `[*]`).
|
|
137
|
+
* Examples: `"password"`, `"user.token"`, `"items[*].serial"`, `"0.password"` (for trace args arrays).
|
|
138
|
+
*/
|
|
139
|
+
paths?: string | string[];
|
|
140
|
+
/** Key name patterns to mask anywhere in the payload. */
|
|
141
|
+
keys?: TraceRulePattern;
|
|
142
|
+
/** Override replacement value for this rule (defaults to config replacement / `"[REDACTED]"`). */
|
|
143
|
+
replacement?: any;
|
|
144
|
+
};
|
|
145
|
+
export type ReproMaskingConfig = {
|
|
146
|
+
/** Default replacement value (defaults to `"[REDACTED]"`). */
|
|
147
|
+
replacement?: any;
|
|
148
|
+
rules?: ReproMaskRule[] | null;
|
|
149
|
+
};
|
|
98
150
|
export declare function setDisabledFunctionTraces(rules?: DisableFunctionTraceConfig[] | null): void;
|
|
99
151
|
export declare function setDisabledFunctionTypes(patterns?: TraceRulePattern | null): void;
|
|
100
152
|
export declare function setDisabledTraceFiles(config?: DisableTraceFileConfig | DisableTraceFileConfig[] | null): void;
|
|
@@ -146,8 +198,10 @@ export type ReproMiddlewareConfig = {
|
|
|
146
198
|
tenantId: string;
|
|
147
199
|
appSecret: string;
|
|
148
200
|
apiBase: string;
|
|
149
|
-
/** Configure header capture/
|
|
201
|
+
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
150
202
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
203
|
+
/** Optional masking rules for request/response payloads and function traces. */
|
|
204
|
+
masking?: ReproMaskingConfig;
|
|
151
205
|
};
|
|
152
206
|
export declare function reproMiddleware(cfg: ReproMiddlewareConfig): (req: Request, res: Response, next: NextFunction) => void;
|
|
153
207
|
export declare function reproMongoosePlugin(cfg: {
|
package/dist/index.js
CHANGED
|
@@ -298,12 +298,35 @@ function inferLibraryNameFromFile(file) {
|
|
|
298
298
|
}
|
|
299
299
|
return segments[0] || null;
|
|
300
300
|
}
|
|
301
|
+
function inferWrapperClassFromFn(fn) {
|
|
302
|
+
if (!fn)
|
|
303
|
+
return null;
|
|
304
|
+
const raw = String(fn);
|
|
305
|
+
if (!raw)
|
|
306
|
+
return null;
|
|
307
|
+
const hashIdx = raw.indexOf('#');
|
|
308
|
+
if (hashIdx > 0) {
|
|
309
|
+
const left = raw.slice(0, hashIdx).trim();
|
|
310
|
+
return left || null;
|
|
311
|
+
}
|
|
312
|
+
const dotIdx = raw.lastIndexOf('.');
|
|
313
|
+
if (dotIdx > 0) {
|
|
314
|
+
const left = raw.slice(0, dotIdx).trim();
|
|
315
|
+
return left || null;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
301
319
|
function matchesRule(rule, event) {
|
|
302
320
|
const namePattern = rule.fn ?? rule.functionName;
|
|
303
321
|
if (!matchesPattern(event.fn, namePattern))
|
|
304
322
|
return false;
|
|
323
|
+
const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
|
|
324
|
+
if (!matchesPattern(event.wrapperClass, wrapperPattern))
|
|
325
|
+
return false;
|
|
305
326
|
if (!matchesPattern(event.file, rule.file))
|
|
306
327
|
return false;
|
|
328
|
+
if (!matchesPattern(event.line == null ? null : String(event.line), rule.line))
|
|
329
|
+
return false;
|
|
307
330
|
const libPattern = rule.lib ?? rule.library;
|
|
308
331
|
if (!matchesPattern(event.library, libPattern))
|
|
309
332
|
return false;
|
|
@@ -364,7 +387,7 @@ function flattenTraceFilePatterns(config) {
|
|
|
364
387
|
if (Array.isArray(config)) {
|
|
365
388
|
return config.flatMap(entry => flattenTraceFilePatterns(entry));
|
|
366
389
|
}
|
|
367
|
-
if (config instanceof RegExp || typeof config === 'string') {
|
|
390
|
+
if (config instanceof RegExp || typeof config === 'string' || typeof config === 'number') {
|
|
368
391
|
return [config];
|
|
369
392
|
}
|
|
370
393
|
if (typeof config === 'object' && 'file' in config) {
|
|
@@ -1197,6 +1220,7 @@ const DEFAULT_SENSITIVE_HEADERS = [
|
|
|
1197
1220
|
'cookie',
|
|
1198
1221
|
'set-cookie',
|
|
1199
1222
|
];
|
|
1223
|
+
const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
|
|
1200
1224
|
function normalizeHeaderRules(rules) {
|
|
1201
1225
|
return normalizePatternArray(rules || []);
|
|
1202
1226
|
}
|
|
@@ -1218,14 +1242,14 @@ function matchesHeaderRule(name, rules) {
|
|
|
1218
1242
|
}
|
|
1219
1243
|
function normalizeHeaderCaptureConfig(raw) {
|
|
1220
1244
|
if (raw === false) {
|
|
1221
|
-
return { enabled: false, allowSensitive: false,
|
|
1245
|
+
return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
|
|
1222
1246
|
}
|
|
1223
1247
|
const opts = raw && raw !== true ? raw : {};
|
|
1224
1248
|
return {
|
|
1225
1249
|
enabled: true,
|
|
1226
1250
|
allowSensitive: opts.allowSensitiveHeaders === true,
|
|
1227
|
-
|
|
1228
|
-
|
|
1251
|
+
mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
|
|
1252
|
+
unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
|
|
1229
1253
|
};
|
|
1230
1254
|
}
|
|
1231
1255
|
function sanitizeHeaderValue(value) {
|
|
@@ -1248,24 +1272,209 @@ function sanitizeHeaders(headers, rawCfg) {
|
|
|
1248
1272
|
const cfg = normalizeHeaderCaptureConfig(rawCfg);
|
|
1249
1273
|
if (!cfg.enabled)
|
|
1250
1274
|
return {};
|
|
1251
|
-
const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
|
|
1252
1275
|
const out = {};
|
|
1253
1276
|
for (const [rawKey, rawVal] of Object.entries(headers || {})) {
|
|
1254
1277
|
const key = String(rawKey || '').toLowerCase();
|
|
1255
1278
|
if (!key)
|
|
1256
1279
|
continue;
|
|
1257
|
-
const shouldDrop = matchesHeaderRule(key, dropList);
|
|
1258
|
-
const keep = matchesHeaderRule(key, cfg.keep);
|
|
1259
|
-
if (shouldDrop && !keep)
|
|
1260
|
-
continue;
|
|
1261
1280
|
const sanitizedValue = sanitizeHeaderValue(rawVal);
|
|
1262
1281
|
if (sanitizedValue !== undefined) {
|
|
1263
1282
|
out[key] = sanitizedValue;
|
|
1264
1283
|
}
|
|
1265
1284
|
}
|
|
1285
|
+
const maskList = cfg.allowSensitive ? cfg.mask : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.mask];
|
|
1286
|
+
Object.keys(out).forEach((key) => {
|
|
1287
|
+
if (!matchesHeaderRule(key, maskList))
|
|
1288
|
+
return;
|
|
1289
|
+
if (matchesHeaderRule(key, cfg.unmask))
|
|
1290
|
+
return;
|
|
1291
|
+
const current = out[key];
|
|
1292
|
+
if (Array.isArray(current)) {
|
|
1293
|
+
out[key] = current.map(() => DEFAULT_MASK_REPLACEMENT);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
out[key] = DEFAULT_MASK_REPLACEMENT;
|
|
1297
|
+
});
|
|
1266
1298
|
return out;
|
|
1267
1299
|
}
|
|
1300
|
+
function normalizeMaskTargets(target) {
|
|
1301
|
+
const out = Array.isArray(target) ? target : [target];
|
|
1302
|
+
return out.filter((t) => typeof t === 'string' && t.length > 0);
|
|
1303
|
+
}
|
|
1304
|
+
function parseMaskPath(raw) {
|
|
1305
|
+
if (!raw)
|
|
1306
|
+
return [];
|
|
1307
|
+
let path = String(raw).trim();
|
|
1308
|
+
if (!path)
|
|
1309
|
+
return [];
|
|
1310
|
+
if (path.startsWith('$.'))
|
|
1311
|
+
path = path.slice(2);
|
|
1312
|
+
if (path.startsWith('.'))
|
|
1313
|
+
path = path.slice(1);
|
|
1314
|
+
path = path.replace(/\[(\d+|\*)\]/g, '.$1');
|
|
1315
|
+
return path.split('.').map(s => s.trim()).filter(Boolean);
|
|
1316
|
+
}
|
|
1317
|
+
function normalizeMaskPaths(paths) {
|
|
1318
|
+
if (!paths)
|
|
1319
|
+
return [];
|
|
1320
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1321
|
+
return list
|
|
1322
|
+
.map(p => parseMaskPath(p))
|
|
1323
|
+
.filter(parts => parts.length > 0);
|
|
1324
|
+
}
|
|
1325
|
+
function normalizeMaskingConfig(raw) {
|
|
1326
|
+
const rules = raw?.rules;
|
|
1327
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
1328
|
+
return null;
|
|
1329
|
+
const replacement = raw?.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1330
|
+
const normalized = [];
|
|
1331
|
+
for (const rule of rules) {
|
|
1332
|
+
if (!rule)
|
|
1333
|
+
continue;
|
|
1334
|
+
const targets = normalizeMaskTargets(rule.target);
|
|
1335
|
+
if (!targets.length)
|
|
1336
|
+
continue;
|
|
1337
|
+
const paths = normalizeMaskPaths(rule.paths);
|
|
1338
|
+
const keys = rule.keys ?? undefined;
|
|
1339
|
+
if (!paths.length && !keys)
|
|
1340
|
+
continue;
|
|
1341
|
+
normalized.push({
|
|
1342
|
+
when: rule.when,
|
|
1343
|
+
targets,
|
|
1344
|
+
paths,
|
|
1345
|
+
keys,
|
|
1346
|
+
replacement: rule.replacement ?? replacement,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
return normalized.length ? { replacement, rules: normalized } : null;
|
|
1350
|
+
}
|
|
1351
|
+
function maskWhenRequiresTrace(when) {
|
|
1352
|
+
return Boolean(when.fn ||
|
|
1353
|
+
when.functionName ||
|
|
1354
|
+
when.wrapper ||
|
|
1355
|
+
when.wrapperClass ||
|
|
1356
|
+
when.className ||
|
|
1357
|
+
when.owner ||
|
|
1358
|
+
when.file ||
|
|
1359
|
+
when.lib ||
|
|
1360
|
+
when.library ||
|
|
1361
|
+
when.type ||
|
|
1362
|
+
when.functionType ||
|
|
1363
|
+
when.event ||
|
|
1364
|
+
when.eventType);
|
|
1365
|
+
}
|
|
1366
|
+
function matchesMaskWhen(when, req, trace) {
|
|
1367
|
+
if (!when)
|
|
1368
|
+
return true;
|
|
1369
|
+
if (!matchesPattern(req.method, when.method))
|
|
1370
|
+
return false;
|
|
1371
|
+
if (!matchesPattern(req.path, when.path))
|
|
1372
|
+
return false;
|
|
1373
|
+
if (!matchesPattern(req.key, when.key))
|
|
1374
|
+
return false;
|
|
1375
|
+
if (maskWhenRequiresTrace(when)) {
|
|
1376
|
+
if (!trace)
|
|
1377
|
+
return false;
|
|
1378
|
+
if (!matchesRule(when, trace))
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
function maskKeysInPlace(node, keys, replacement) {
|
|
1384
|
+
if (!node)
|
|
1385
|
+
return;
|
|
1386
|
+
if (Array.isArray(node)) {
|
|
1387
|
+
node.forEach(item => maskKeysInPlace(item, keys, replacement));
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (typeof node !== 'object')
|
|
1391
|
+
return;
|
|
1392
|
+
Object.keys(node).forEach((key) => {
|
|
1393
|
+
if (matchesPattern(key, keys, false)) {
|
|
1394
|
+
try {
|
|
1395
|
+
node[key] = replacement;
|
|
1396
|
+
}
|
|
1397
|
+
catch { }
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
maskKeysInPlace(node[key], keys, replacement);
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function maskPathInPlace(node, pathParts, replacement, depth = 0) {
|
|
1404
|
+
if (!node)
|
|
1405
|
+
return;
|
|
1406
|
+
if (depth >= pathParts.length)
|
|
1407
|
+
return;
|
|
1408
|
+
const part = pathParts[depth];
|
|
1409
|
+
const isLast = depth === pathParts.length - 1;
|
|
1410
|
+
const applyAt = (container, key) => {
|
|
1411
|
+
if (!container)
|
|
1412
|
+
return;
|
|
1413
|
+
try {
|
|
1414
|
+
container[key] = replacement;
|
|
1415
|
+
}
|
|
1416
|
+
catch { }
|
|
1417
|
+
};
|
|
1418
|
+
if (part === '*') {
|
|
1419
|
+
if (Array.isArray(node)) {
|
|
1420
|
+
for (let i = 0; i < node.length; i++) {
|
|
1421
|
+
if (isLast)
|
|
1422
|
+
applyAt(node, i);
|
|
1423
|
+
else
|
|
1424
|
+
maskPathInPlace(node[i], pathParts, replacement, depth + 1);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else if (typeof node === 'object') {
|
|
1428
|
+
for (const key of Object.keys(node)) {
|
|
1429
|
+
if (isLast)
|
|
1430
|
+
applyAt(node, key);
|
|
1431
|
+
else
|
|
1432
|
+
maskPathInPlace(node[key], pathParts, replacement, depth + 1);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const index = Number(part);
|
|
1438
|
+
const isIndex = Number.isInteger(index) && String(index) === part;
|
|
1439
|
+
if (Array.isArray(node) && isIndex) {
|
|
1440
|
+
if (index < 0 || index >= node.length)
|
|
1441
|
+
return;
|
|
1442
|
+
if (isLast)
|
|
1443
|
+
applyAt(node, index);
|
|
1444
|
+
else
|
|
1445
|
+
maskPathInPlace(node[index], pathParts, replacement, depth + 1);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (typeof node !== 'object')
|
|
1449
|
+
return;
|
|
1450
|
+
if (!Object.prototype.hasOwnProperty.call(node, part))
|
|
1451
|
+
return;
|
|
1452
|
+
if (isLast)
|
|
1453
|
+
applyAt(node, part);
|
|
1454
|
+
else
|
|
1455
|
+
maskPathInPlace(node[part], pathParts, replacement, depth + 1);
|
|
1456
|
+
}
|
|
1457
|
+
function applyMasking(target, value, req, trace, masking) {
|
|
1458
|
+
if (!masking || !masking.rules.length)
|
|
1459
|
+
return value;
|
|
1460
|
+
if (value === undefined)
|
|
1461
|
+
return value;
|
|
1462
|
+
for (const rule of masking.rules) {
|
|
1463
|
+
if (!rule.targets.includes(target))
|
|
1464
|
+
continue;
|
|
1465
|
+
if (!matchesMaskWhen(rule.when, req, trace))
|
|
1466
|
+
continue;
|
|
1467
|
+
const replacement = rule.replacement ?? masking.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1468
|
+
if (rule.keys)
|
|
1469
|
+
maskKeysInPlace(value, rule.keys, replacement);
|
|
1470
|
+
if (rule.paths.length) {
|
|
1471
|
+
rule.paths.forEach(parts => maskPathInPlace(value, parts, replacement, 0));
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return value;
|
|
1475
|
+
}
|
|
1268
1476
|
function reproMiddleware(cfg) {
|
|
1477
|
+
const masking = normalizeMaskingConfig(cfg.masking);
|
|
1269
1478
|
return function (req, res, next) {
|
|
1270
1479
|
const sid = req.headers['x-bug-session-id'] || '';
|
|
1271
1480
|
const aid = req.headers['x-bug-action-id'] || '';
|
|
@@ -1278,8 +1487,10 @@ function reproMiddleware(cfg) {
|
|
|
1278
1487
|
const rid = nextRequestId(requestEpochMs);
|
|
1279
1488
|
const t0 = requestStartRaw;
|
|
1280
1489
|
const url = req.originalUrl || req.url || '/';
|
|
1490
|
+
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
1281
1491
|
const path = url; // back-compat
|
|
1282
1492
|
const key = normalizeRouteKey(req.method, url);
|
|
1493
|
+
const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
1283
1494
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
1284
1495
|
beginSessionRequest(sid);
|
|
1285
1496
|
// ---- response body capture (unchanged) ----
|
|
@@ -1426,6 +1637,9 @@ function reproMiddleware(cfg) {
|
|
|
1426
1637
|
unsubscribe = __TRACER__.tracer.on((ev) => {
|
|
1427
1638
|
if (!ev || ev.traceId !== tidNow)
|
|
1428
1639
|
return;
|
|
1640
|
+
const sourceFileForLibrary = typeof ev.sourceFile === 'string' && ev.sourceFile
|
|
1641
|
+
? String(ev.sourceFile)
|
|
1642
|
+
: null;
|
|
1429
1643
|
const evt = {
|
|
1430
1644
|
t: alignTimestamp(ev.t),
|
|
1431
1645
|
type: ev.type,
|
|
@@ -1439,14 +1653,25 @@ function reproMiddleware(cfg) {
|
|
|
1439
1653
|
if (ev.functionType !== undefined) {
|
|
1440
1654
|
evt.functionType = ev.functionType;
|
|
1441
1655
|
}
|
|
1656
|
+
const candidate = {
|
|
1657
|
+
type: evt.type,
|
|
1658
|
+
eventType: evt.type,
|
|
1659
|
+
functionType: evt.functionType ?? null,
|
|
1660
|
+
fn: evt.fn,
|
|
1661
|
+
wrapperClass: inferWrapperClassFromFn(evt.fn),
|
|
1662
|
+
file: evt.file ?? null,
|
|
1663
|
+
line: evt.line ?? null,
|
|
1664
|
+
depth: evt.depth,
|
|
1665
|
+
library: inferLibraryNameFromFile(sourceFileForLibrary ?? evt.file),
|
|
1666
|
+
};
|
|
1442
1667
|
if (ev.args !== undefined) {
|
|
1443
|
-
evt.args = sanitizeTraceArgs(ev.args);
|
|
1668
|
+
evt.args = applyMasking('trace.args', sanitizeTraceArgs(ev.args), maskReq, candidate, masking);
|
|
1444
1669
|
}
|
|
1445
1670
|
if (ev.returnValue !== undefined) {
|
|
1446
|
-
evt.returnValue = sanitizeTraceValue(ev.returnValue);
|
|
1671
|
+
evt.returnValue = applyMasking('trace.returnValue', sanitizeTraceValue(ev.returnValue), maskReq, candidate, masking);
|
|
1447
1672
|
}
|
|
1448
1673
|
if (ev.error !== undefined) {
|
|
1449
|
-
evt.error = sanitizeTraceValue(ev.error);
|
|
1674
|
+
evt.error = applyMasking('trace.error', sanitizeTraceValue(ev.error), maskReq, candidate, masking);
|
|
1450
1675
|
}
|
|
1451
1676
|
if (ev.threw !== undefined) {
|
|
1452
1677
|
evt.threw = Boolean(ev.threw);
|
|
@@ -1454,15 +1679,6 @@ function reproMiddleware(cfg) {
|
|
|
1454
1679
|
if (ev.unawaited !== undefined) {
|
|
1455
1680
|
evt.unawaited = ev.unawaited === true;
|
|
1456
1681
|
}
|
|
1457
|
-
const candidate = {
|
|
1458
|
-
type: evt.type,
|
|
1459
|
-
eventType: evt.type,
|
|
1460
|
-
functionType: evt.functionType ?? null,
|
|
1461
|
-
fn: evt.fn,
|
|
1462
|
-
file: evt.file ?? null,
|
|
1463
|
-
depth: evt.depth,
|
|
1464
|
-
library: inferLibraryNameFromFile(evt.file),
|
|
1465
|
-
};
|
|
1466
1682
|
const dropEvent = shouldDropTraceEvent(candidate);
|
|
1467
1683
|
const spanKey = normalizeSpanId(evt.spanId);
|
|
1468
1684
|
if (evt.type === 'enter') {
|
|
@@ -1546,9 +1762,25 @@ function reproMiddleware(cfg) {
|
|
|
1546
1762
|
?? firstAppTrace
|
|
1547
1763
|
?? { fn: null, file: null, line: null, functionType: null };
|
|
1548
1764
|
const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1765
|
+
const endpointTraceCtx = (() => {
|
|
1766
|
+
if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
|
|
1767
|
+
return null;
|
|
1768
|
+
return {
|
|
1769
|
+
type: 'enter',
|
|
1770
|
+
eventType: 'enter',
|
|
1771
|
+
fn: chosenEndpoint.fn ?? undefined,
|
|
1772
|
+
wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
|
|
1773
|
+
file: chosenEndpoint.file ?? null,
|
|
1774
|
+
line: chosenEndpoint.line ?? null,
|
|
1775
|
+
functionType: chosenEndpoint.functionType ?? null,
|
|
1776
|
+
library: inferLibraryNameFromFile(chosenEndpoint.file),
|
|
1777
|
+
};
|
|
1778
|
+
})();
|
|
1779
|
+
const requestBody = applyMasking('request.body', sanitizeRequestSnapshot(req.body), maskReq, endpointTraceCtx, masking);
|
|
1780
|
+
const requestParams = applyMasking('request.params', sanitizeRequestSnapshot(req.params), maskReq, endpointTraceCtx, masking);
|
|
1781
|
+
const requestQuery = applyMasking('request.query', sanitizeRequestSnapshot(req.query), maskReq, endpointTraceCtx, masking);
|
|
1782
|
+
const maskedHeaders = applyMasking('request.headers', requestHeaders, maskReq, endpointTraceCtx, masking);
|
|
1783
|
+
const responseBody = applyMasking('response.body', capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody), maskReq, endpointTraceCtx, masking);
|
|
1552
1784
|
const requestPayload = {
|
|
1553
1785
|
rid,
|
|
1554
1786
|
method: req.method,
|
|
@@ -1556,9 +1788,9 @@ function reproMiddleware(cfg) {
|
|
|
1556
1788
|
path,
|
|
1557
1789
|
status: res.statusCode,
|
|
1558
1790
|
durMs: Date.now() - t0,
|
|
1559
|
-
headers:
|
|
1791
|
+
headers: maskedHeaders,
|
|
1560
1792
|
key,
|
|
1561
|
-
respBody:
|
|
1793
|
+
respBody: responseBody,
|
|
1562
1794
|
trace: traceBatches.length ? undefined : '[]',
|
|
1563
1795
|
};
|
|
1564
1796
|
if (requestBody !== undefined)
|
package/docs/tracing.md
CHANGED
|
@@ -30,7 +30,7 @@ or getters. Provide a string, regular expression, or array of them and every
|
|
|
30
30
|
matching trace event will be ignored, regardless of library or filename.
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
import { setDisabledFunctionTypes } from '
|
|
33
|
+
import { setDisabledFunctionTypes } from 'repro-nest';
|
|
34
34
|
|
|
35
35
|
setDisabledFunctionTypes(['constructor']);
|
|
36
36
|
```
|
|
@@ -50,7 +50,9 @@ captured in the session payload.
|
|
|
50
50
|
| Property | Description |
|
|
51
51
|
| --------------- | ------------------------------------------------------------------------------------------------- |
|
|
52
52
|
| `fn`/`functionName` | Match against the instrumented function name (substring or RegExp). |
|
|
53
|
-
| `
|
|
53
|
+
| `wrapper`/`wrapperClass`/`className`/`owner` | Match the wrapper/owner inferred from the function name (e.g. `"UserService"` in `"UserService.create"`). |
|
|
54
|
+
| `file` | Match the filename reported by the trace event (callsite for wrapped calls; definition file for body-traced app functions). |
|
|
55
|
+
| `line` | Match the line number reported by the trace event. |
|
|
54
56
|
| `lib`/`library` | Match the npm package inferred from the file path (e.g. `"mongoose"`). |
|
|
55
57
|
| `type`/`functionType` | Match the detected function kind (e.g. `"constructor"`, `"method"`, `"arrow"`). |
|
|
56
58
|
| `event`/`eventType` | Match the trace phase (`"enter"` or `"exit"`) to suppress only specific edges of a call. |
|
|
@@ -64,7 +66,7 @@ Provide a function that receives the raw trace event and returns `true` when it
|
|
|
64
66
|
should be discarded. Use this form for complex, stateful, or cross-field logic.
|
|
65
67
|
|
|
66
68
|
```ts
|
|
67
|
-
import { setDisabledFunctionTraces } from '
|
|
69
|
+
import { setDisabledFunctionTraces } from 'repro-nest';
|
|
68
70
|
|
|
69
71
|
setDisabledFunctionTraces([
|
|
70
72
|
(event) => event.library === 'mongoose' && event.functionType === 'constructor',
|
|
@@ -74,6 +76,71 @@ setDisabledFunctionTraces([
|
|
|
74
76
|
Pass `null` or an empty array to `initReproTracing` or `setDisabledFunctionTraces`
|
|
75
77
|
to remove previously configured rules.
|
|
76
78
|
|
|
79
|
+
## Payload masking (`reproMiddleware`)
|
|
80
|
+
|
|
81
|
+
`reproMiddleware` can mask request/response payloads, request headers, and traced
|
|
82
|
+
function inputs/outputs before they are persisted.
|
|
83
|
+
|
|
84
|
+
### Targets
|
|
85
|
+
|
|
86
|
+
- `request.headers`
|
|
87
|
+
- `request.body`
|
|
88
|
+
- `request.params`
|
|
89
|
+
- `request.query`
|
|
90
|
+
- `response.body`
|
|
91
|
+
- `trace.args`
|
|
92
|
+
- `trace.returnValue`
|
|
93
|
+
- `trace.error`
|
|
94
|
+
|
|
95
|
+
### Rules
|
|
96
|
+
|
|
97
|
+
- `when.method` / `when.path` / `when.key` scope rules by endpoint (`key` is `"METHOD /path"` without query string).
|
|
98
|
+
- For function-specific rules, `when` supports the same fields as `disableFunctionTraces` (`fn`, `wrapperClass`, `file`, `line`, etc). This is useful when multiple functions share the same name.
|
|
99
|
+
- `paths` uses dot/bracket syntax and supports `*`, `[0]`, `[*]` (example: `"items[*].token"` or `"0.password"` for trace args arrays).
|
|
100
|
+
- `keys` masks matching key names anywhere in the payload (string/RegExp/array).
|
|
101
|
+
- `replacement` overrides the default replacement value (defaults to `"[REDACTED]"`).
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { reproMiddleware } from 'repro-nest';
|
|
105
|
+
|
|
106
|
+
app.use(reproMiddleware({
|
|
107
|
+
appId,
|
|
108
|
+
tenantId,
|
|
109
|
+
appSecret,
|
|
110
|
+
apiBase,
|
|
111
|
+
masking: {
|
|
112
|
+
rules: [
|
|
113
|
+
{
|
|
114
|
+
when: { key: 'POST /api/auth/login' },
|
|
115
|
+
target: 'request.body',
|
|
116
|
+
paths: ['password'],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
when: { wrapperClass: 'AuthService', functionName: 'login', file: '/app/src/auth/auth.service.ts' },
|
|
120
|
+
target: ['trace.args', 'trace.returnValue', 'trace.error'],
|
|
121
|
+
keys: [/token/i],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
target: 'request.headers',
|
|
125
|
+
keys: [/authorization/i, /cookie/i],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Header capture/masking (`captureHeaders`)
|
|
133
|
+
|
|
134
|
+
Headers are captured by default with sensitive values masked. Configure `captureHeaders`
|
|
135
|
+
to change the behavior:
|
|
136
|
+
|
|
137
|
+
- `captureHeaders: false` disables header capture entirely.
|
|
138
|
+
- `captureHeaders: true` (or omitted) captures headers and masks sensitive ones.
|
|
139
|
+
- `captureHeaders.allowSensitiveHeaders: true` keeps default sensitive headers unmasked (use with care).
|
|
140
|
+
- `captureHeaders.maskHeaders` adds additional header names to mask.
|
|
141
|
+
- `captureHeaders.unmaskHeaders` keeps specific header names unmasked (overrides defaults and `maskHeaders`).
|
|
142
|
+
- `captureHeaders.dropHeaders` / `captureHeaders.keepHeaders` are legacy aliases for `maskHeaders` / `unmaskHeaders`.
|
|
143
|
+
|
|
77
144
|
## `logFunctionCalls`
|
|
78
145
|
|
|
79
146
|
Set to `true` to enable verbose console logging of function entry/exit events at
|
|
@@ -81,7 +148,7 @@ runtime, or to `false` to silence them. The value is forwarded to
|
|
|
81
148
|
`setReproTraceLogsEnabled`, so you can also toggle logging later in the process:
|
|
82
149
|
|
|
83
150
|
```ts
|
|
84
|
-
import { enableReproTraceLogs, disableReproTraceLogs } from '
|
|
151
|
+
import { enableReproTraceLogs, disableReproTraceLogs } from 'repro-nest';
|
|
85
152
|
|
|
86
153
|
enableReproTraceLogs();
|
|
87
154
|
// ...
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -229,14 +229,16 @@ let __TRACER__: TracerApi | null = null;
|
|
|
229
229
|
let __TRACER_READY = false;
|
|
230
230
|
|
|
231
231
|
type TraceEventPhase = 'enter' | 'exit';
|
|
232
|
-
export type TraceRulePattern = string | RegExp | Array<string | RegExp>;
|
|
232
|
+
export type TraceRulePattern = string | number | RegExp | Array<string | number | RegExp>;
|
|
233
233
|
|
|
234
234
|
export type TraceEventForFilter = {
|
|
235
235
|
type: TraceEventPhase; // legacy alias for eventType
|
|
236
236
|
eventType: TraceEventPhase;
|
|
237
237
|
functionType?: string | null;
|
|
238
238
|
fn?: string;
|
|
239
|
+
wrapperClass?: string | null;
|
|
239
240
|
file?: string | null;
|
|
241
|
+
line?: number | null;
|
|
240
242
|
depth?: number;
|
|
241
243
|
library?: string | null;
|
|
242
244
|
};
|
|
@@ -267,19 +269,29 @@ type EndpointTraceInfo = {
|
|
|
267
269
|
|
|
268
270
|
export type HeaderRule = string | RegExp;
|
|
269
271
|
export type HeaderCaptureOptions = {
|
|
270
|
-
/** When true, sensitive headers such as Authorization are kept; default
|
|
272
|
+
/** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
|
|
271
273
|
allowSensitiveHeaders?: boolean;
|
|
272
|
-
/**
|
|
274
|
+
/**
|
|
275
|
+
* Header names (string or RegExp) to mask.
|
|
276
|
+
* `dropHeaders` is kept for backward-compatibility and treated as an alias for `maskHeaders`.
|
|
277
|
+
*/
|
|
278
|
+
maskHeaders?: HeaderRule | HeaderRule[];
|
|
279
|
+
/** @deprecated Alias for {@link maskHeaders}. */
|
|
273
280
|
dropHeaders?: HeaderRule | HeaderRule[];
|
|
274
|
-
/**
|
|
281
|
+
/**
|
|
282
|
+
* Header names (string or RegExp) to keep unmasked, overriding defaults and `maskHeaders`.
|
|
283
|
+
* `keepHeaders` is kept for backward-compatibility and treated as an alias for `unmaskHeaders`.
|
|
284
|
+
*/
|
|
285
|
+
unmaskHeaders?: HeaderRule | HeaderRule[];
|
|
286
|
+
/** @deprecated Alias for {@link unmaskHeaders}. */
|
|
275
287
|
keepHeaders?: HeaderRule | HeaderRule[];
|
|
276
288
|
};
|
|
277
289
|
|
|
278
290
|
type NormalizedHeaderCapture = {
|
|
279
291
|
enabled: boolean;
|
|
280
292
|
allowSensitive: boolean;
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
mask: HeaderRule[];
|
|
294
|
+
unmask: HeaderRule[];
|
|
283
295
|
};
|
|
284
296
|
|
|
285
297
|
/** Lightweight helper to disable every trace emitted from specific files. */
|
|
@@ -301,8 +313,21 @@ export type DisableFunctionTraceRule = {
|
|
|
301
313
|
fn?: TraceRulePattern;
|
|
302
314
|
/** Function name (e.g. `"findOne"`, `/^UserService\./`). */
|
|
303
315
|
functionName?: TraceRulePattern;
|
|
304
|
-
/**
|
|
316
|
+
/** Shortcut for {@link wrapperClass}. */
|
|
317
|
+
wrapper?: TraceRulePattern;
|
|
318
|
+
/**
|
|
319
|
+
* Wrapper/owner name derived from {@link functionName} (e.g. `"UserService"` in `"UserService.create"`).
|
|
320
|
+
* Useful when multiple functions share the same method name.
|
|
321
|
+
*/
|
|
322
|
+
wrapperClass?: TraceRulePattern;
|
|
323
|
+
/** Alias for {@link wrapperClass}. */
|
|
324
|
+
className?: TraceRulePattern;
|
|
325
|
+
/** Alias for {@link wrapperClass}. */
|
|
326
|
+
owner?: TraceRulePattern;
|
|
327
|
+
/** Source filename reported by the trace event. */
|
|
305
328
|
file?: TraceRulePattern;
|
|
329
|
+
/** Line number reported by the trace event. */
|
|
330
|
+
line?: TraceRulePattern;
|
|
306
331
|
/** Shortcut for {@link library}. */
|
|
307
332
|
lib?: TraceRulePattern;
|
|
308
333
|
/** Library/package name inferred from the file path (e.g. `"mongoose"`). */
|
|
@@ -323,6 +348,45 @@ export type DisableFunctionTraceConfig =
|
|
|
323
348
|
| DisableFunctionTraceRule
|
|
324
349
|
| DisableFunctionTracePredicate;
|
|
325
350
|
|
|
351
|
+
export type ReproMaskTarget =
|
|
352
|
+
| 'request.headers'
|
|
353
|
+
| 'request.body'
|
|
354
|
+
| 'request.params'
|
|
355
|
+
| 'request.query'
|
|
356
|
+
| 'response.body'
|
|
357
|
+
| 'trace.args'
|
|
358
|
+
| 'trace.returnValue'
|
|
359
|
+
| 'trace.error';
|
|
360
|
+
|
|
361
|
+
export type ReproMaskWhen = DisableFunctionTraceRule & {
|
|
362
|
+
/** Match HTTP method (e.g. `"GET"`, `/^post$/i`). */
|
|
363
|
+
method?: TraceRulePattern;
|
|
364
|
+
/** Match request path without query string (e.g. `"/api/auth/login"`). */
|
|
365
|
+
path?: TraceRulePattern;
|
|
366
|
+
/** Match normalized endpoint key (e.g. `"POST /api/auth/login"`). */
|
|
367
|
+
key?: TraceRulePattern;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export type ReproMaskRule = {
|
|
371
|
+
when?: ReproMaskWhen;
|
|
372
|
+
target: ReproMaskTarget | ReproMaskTarget[];
|
|
373
|
+
/**
|
|
374
|
+
* Dot/bracket paths to mask (supports `*`, `[0]`, `[*]`).
|
|
375
|
+
* Examples: `"password"`, `"user.token"`, `"items[*].serial"`, `"0.password"` (for trace args arrays).
|
|
376
|
+
*/
|
|
377
|
+
paths?: string | string[];
|
|
378
|
+
/** Key name patterns to mask anywhere in the payload. */
|
|
379
|
+
keys?: TraceRulePattern;
|
|
380
|
+
/** Override replacement value for this rule (defaults to config replacement / `"[REDACTED]"`). */
|
|
381
|
+
replacement?: any;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
export type ReproMaskingConfig = {
|
|
385
|
+
/** Default replacement value (defaults to `"[REDACTED]"`). */
|
|
386
|
+
replacement?: any;
|
|
387
|
+
rules?: ReproMaskRule[] | null;
|
|
388
|
+
};
|
|
389
|
+
|
|
326
390
|
const DEFAULT_INTERCEPTOR_TRACE_RULES: DisableFunctionTraceConfig[] = [
|
|
327
391
|
{ fn: /switchToHttp$/i },
|
|
328
392
|
{ fn: /intercept$/i },
|
|
@@ -348,8 +412,8 @@ function refreshDisabledFunctionTraceRules() {
|
|
|
348
412
|
}
|
|
349
413
|
|
|
350
414
|
let disabledFunctionTraceRules: DisableFunctionTraceConfig[] = computeDisabledFunctionTraceRules();
|
|
351
|
-
let disabledFunctionTypePatterns: Array<string | RegExp> = [];
|
|
352
|
-
let disabledTraceFilePatterns: Array<string | RegExp> = [];
|
|
415
|
+
let disabledFunctionTypePatterns: Array<string | number | RegExp> = [];
|
|
416
|
+
let disabledTraceFilePatterns: Array<string | number | RegExp> = [];
|
|
353
417
|
let __TRACE_LOG_PREF: boolean | null = null;
|
|
354
418
|
|
|
355
419
|
function setInterceptorTracingEnabled(enabled: boolean) {
|
|
@@ -403,11 +467,32 @@ function inferLibraryNameFromFile(file?: string | null): string | null {
|
|
|
403
467
|
return segments[0] || null;
|
|
404
468
|
}
|
|
405
469
|
|
|
470
|
+
function inferWrapperClassFromFn(fn?: string | null): string | null {
|
|
471
|
+
if (!fn) return null;
|
|
472
|
+
const raw = String(fn);
|
|
473
|
+
if (!raw) return null;
|
|
474
|
+
const hashIdx = raw.indexOf('#');
|
|
475
|
+
if (hashIdx > 0) {
|
|
476
|
+
const left = raw.slice(0, hashIdx).trim();
|
|
477
|
+
return left || null;
|
|
478
|
+
}
|
|
479
|
+
const dotIdx = raw.lastIndexOf('.');
|
|
480
|
+
if (dotIdx > 0) {
|
|
481
|
+
const left = raw.slice(0, dotIdx).trim();
|
|
482
|
+
return left || null;
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
406
487
|
function matchesRule(rule: DisableFunctionTraceRule, event: TraceEventForFilter): boolean {
|
|
407
488
|
const namePattern = rule.fn ?? rule.functionName;
|
|
408
489
|
if (!matchesPattern(event.fn, namePattern)) return false;
|
|
409
490
|
|
|
491
|
+
const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
|
|
492
|
+
if (!matchesPattern(event.wrapperClass, wrapperPattern)) return false;
|
|
493
|
+
|
|
410
494
|
if (!matchesPattern(event.file, rule.file)) return false;
|
|
495
|
+
if (!matchesPattern(event.line == null ? null : String(event.line), rule.line)) return false;
|
|
411
496
|
|
|
412
497
|
const libPattern = rule.lib ?? rule.library;
|
|
413
498
|
if (!matchesPattern(event.library, libPattern)) return false;
|
|
@@ -460,12 +545,12 @@ export function setDisabledFunctionTypes(patterns?: TraceRulePattern | null) {
|
|
|
460
545
|
disabledFunctionTypePatterns = normalizePatternArray(patterns);
|
|
461
546
|
}
|
|
462
547
|
|
|
463
|
-
function flattenTraceFilePatterns(config: DisableTraceFileConfig): Array<string | RegExp> {
|
|
548
|
+
function flattenTraceFilePatterns(config: DisableTraceFileConfig): Array<string | number | RegExp> {
|
|
464
549
|
if (config === null || config === undefined) return [];
|
|
465
550
|
if (Array.isArray(config)) {
|
|
466
551
|
return config.flatMap(entry => flattenTraceFilePatterns(entry));
|
|
467
552
|
}
|
|
468
|
-
if (config instanceof RegExp || typeof config === 'string') {
|
|
553
|
+
if (config instanceof RegExp || typeof config === 'string' || typeof config === 'number') {
|
|
469
554
|
return [config];
|
|
470
555
|
}
|
|
471
556
|
if (typeof config === 'object' && 'file' in config) {
|
|
@@ -1421,6 +1506,8 @@ const DEFAULT_SENSITIVE_HEADERS: Array<string | RegExp> = [
|
|
|
1421
1506
|
'set-cookie',
|
|
1422
1507
|
];
|
|
1423
1508
|
|
|
1509
|
+
const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
|
|
1510
|
+
|
|
1424
1511
|
function normalizeHeaderRules(rules?: HeaderRule | HeaderRule[] | null): HeaderRule[] {
|
|
1425
1512
|
return normalizePatternArray<HeaderRule>(rules || []);
|
|
1426
1513
|
}
|
|
@@ -1438,14 +1525,14 @@ function matchesHeaderRule(name: string, rules: HeaderRule[]): boolean {
|
|
|
1438
1525
|
|
|
1439
1526
|
function normalizeHeaderCaptureConfig(raw?: boolean | HeaderCaptureOptions): NormalizedHeaderCapture {
|
|
1440
1527
|
if (raw === false) {
|
|
1441
|
-
return { enabled: false, allowSensitive: false,
|
|
1528
|
+
return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
|
|
1442
1529
|
}
|
|
1443
1530
|
const opts: HeaderCaptureOptions = raw && raw !== true ? raw : {};
|
|
1444
1531
|
return {
|
|
1445
1532
|
enabled: true,
|
|
1446
1533
|
allowSensitive: opts.allowSensitiveHeaders === true,
|
|
1447
|
-
|
|
1448
|
-
|
|
1534
|
+
mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
|
|
1535
|
+
unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
|
|
1449
1536
|
};
|
|
1450
1537
|
}
|
|
1451
1538
|
|
|
@@ -1466,24 +1553,210 @@ function sanitizeHeaders(headers: any, rawCfg?: boolean | HeaderCaptureOptions)
|
|
|
1466
1553
|
const cfg = normalizeHeaderCaptureConfig(rawCfg);
|
|
1467
1554
|
if (!cfg.enabled) return {};
|
|
1468
1555
|
|
|
1469
|
-
const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
|
|
1470
1556
|
const out: Record<string, any> = {};
|
|
1471
1557
|
|
|
1472
1558
|
for (const [rawKey, rawVal] of Object.entries(headers || {})) {
|
|
1473
1559
|
const key = String(rawKey || '').toLowerCase();
|
|
1474
1560
|
if (!key) continue;
|
|
1475
|
-
const shouldDrop = matchesHeaderRule(key, dropList);
|
|
1476
|
-
const keep = matchesHeaderRule(key, cfg.keep);
|
|
1477
|
-
if (shouldDrop && !keep) continue;
|
|
1478
|
-
|
|
1479
1561
|
const sanitizedValue = sanitizeHeaderValue(rawVal);
|
|
1480
1562
|
if (sanitizedValue !== undefined) {
|
|
1481
1563
|
out[key] = sanitizedValue;
|
|
1482
1564
|
}
|
|
1483
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
const maskList = cfg.allowSensitive ? cfg.mask : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.mask];
|
|
1568
|
+
Object.keys(out).forEach((key) => {
|
|
1569
|
+
if (!matchesHeaderRule(key, maskList)) return;
|
|
1570
|
+
if (matchesHeaderRule(key, cfg.unmask)) return;
|
|
1571
|
+
const current = out[key];
|
|
1572
|
+
if (Array.isArray(current)) {
|
|
1573
|
+
out[key] = current.map(() => DEFAULT_MASK_REPLACEMENT);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
out[key] = DEFAULT_MASK_REPLACEMENT;
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1484
1579
|
return out;
|
|
1485
1580
|
}
|
|
1486
1581
|
|
|
1582
|
+
// ===================================================================
|
|
1583
|
+
// Masking (request/response payloads + trace args/returns/errors)
|
|
1584
|
+
// ===================================================================
|
|
1585
|
+
type NormalizedMaskRule = {
|
|
1586
|
+
when?: ReproMaskWhen;
|
|
1587
|
+
targets: ReproMaskTarget[];
|
|
1588
|
+
paths: string[][];
|
|
1589
|
+
keys?: TraceRulePattern;
|
|
1590
|
+
replacement: any;
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
type NormalizedMaskingConfig = {
|
|
1594
|
+
replacement: any;
|
|
1595
|
+
rules: NormalizedMaskRule[];
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
type MaskRequestContext = {
|
|
1599
|
+
method: string;
|
|
1600
|
+
path: string;
|
|
1601
|
+
key: string;
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
function normalizeMaskTargets(target: ReproMaskTarget | ReproMaskTarget[]): ReproMaskTarget[] {
|
|
1605
|
+
const out = Array.isArray(target) ? target : [target];
|
|
1606
|
+
return out.filter((t): t is ReproMaskTarget => typeof t === 'string' && t.length > 0);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function parseMaskPath(raw: string): string[] {
|
|
1610
|
+
if (!raw) return [];
|
|
1611
|
+
let path = String(raw).trim();
|
|
1612
|
+
if (!path) return [];
|
|
1613
|
+
if (path.startsWith('$.')) path = path.slice(2);
|
|
1614
|
+
if (path.startsWith('.')) path = path.slice(1);
|
|
1615
|
+
path = path.replace(/\[(\d+|\*)\]/g, '.$1');
|
|
1616
|
+
return path.split('.').map(s => s.trim()).filter(Boolean);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function normalizeMaskPaths(paths?: string | string[]): string[][] {
|
|
1620
|
+
if (!paths) return [];
|
|
1621
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1622
|
+
return list
|
|
1623
|
+
.map(p => parseMaskPath(p))
|
|
1624
|
+
.filter(parts => parts.length > 0);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function normalizeMaskingConfig(raw?: ReproMaskingConfig): NormalizedMaskingConfig | null {
|
|
1628
|
+
const rules = raw?.rules;
|
|
1629
|
+
if (!Array.isArray(rules) || rules.length === 0) return null;
|
|
1630
|
+
const replacement = raw?.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1631
|
+
const normalized: NormalizedMaskRule[] = [];
|
|
1632
|
+
for (const rule of rules) {
|
|
1633
|
+
if (!rule) continue;
|
|
1634
|
+
const targets = normalizeMaskTargets(rule.target);
|
|
1635
|
+
if (!targets.length) continue;
|
|
1636
|
+
const paths = normalizeMaskPaths(rule.paths);
|
|
1637
|
+
const keys = rule.keys ?? undefined;
|
|
1638
|
+
if (!paths.length && !keys) continue;
|
|
1639
|
+
normalized.push({
|
|
1640
|
+
when: rule.when,
|
|
1641
|
+
targets,
|
|
1642
|
+
paths,
|
|
1643
|
+
keys,
|
|
1644
|
+
replacement: rule.replacement ?? replacement,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
return normalized.length ? { replacement, rules: normalized } : null;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function maskWhenRequiresTrace(when: ReproMaskWhen): boolean {
|
|
1651
|
+
return Boolean(
|
|
1652
|
+
when.fn ||
|
|
1653
|
+
when.functionName ||
|
|
1654
|
+
when.wrapper ||
|
|
1655
|
+
when.wrapperClass ||
|
|
1656
|
+
when.className ||
|
|
1657
|
+
when.owner ||
|
|
1658
|
+
when.file ||
|
|
1659
|
+
when.lib ||
|
|
1660
|
+
when.library ||
|
|
1661
|
+
when.type ||
|
|
1662
|
+
when.functionType ||
|
|
1663
|
+
when.event ||
|
|
1664
|
+
when.eventType
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function matchesMaskWhen(when: ReproMaskWhen | undefined, req: MaskRequestContext, trace: TraceEventForFilter | null): boolean {
|
|
1669
|
+
if (!when) return true;
|
|
1670
|
+
if (!matchesPattern(req.method, when.method)) return false;
|
|
1671
|
+
if (!matchesPattern(req.path, when.path)) return false;
|
|
1672
|
+
if (!matchesPattern(req.key, when.key)) return false;
|
|
1673
|
+
|
|
1674
|
+
if (maskWhenRequiresTrace(when)) {
|
|
1675
|
+
if (!trace) return false;
|
|
1676
|
+
if (!matchesRule(when, trace)) return false;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return true;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function maskKeysInPlace(node: any, keys: TraceRulePattern, replacement: any) {
|
|
1683
|
+
if (!node) return;
|
|
1684
|
+
if (Array.isArray(node)) {
|
|
1685
|
+
node.forEach(item => maskKeysInPlace(item, keys, replacement));
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (typeof node !== 'object') return;
|
|
1689
|
+
Object.keys(node).forEach((key) => {
|
|
1690
|
+
if (matchesPattern(key, keys, false)) {
|
|
1691
|
+
try { node[key] = replacement; } catch {}
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
maskKeysInPlace(node[key], keys, replacement);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function maskPathInPlace(node: any, pathParts: string[], replacement: any, depth: number = 0) {
|
|
1699
|
+
if (!node) return;
|
|
1700
|
+
if (depth >= pathParts.length) return;
|
|
1701
|
+
const part = pathParts[depth];
|
|
1702
|
+
const isLast = depth === pathParts.length - 1;
|
|
1703
|
+
|
|
1704
|
+
const applyAt = (container: any, key: string | number) => {
|
|
1705
|
+
if (!container) return;
|
|
1706
|
+
try { container[key] = replacement; } catch {}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
if (part === '*') {
|
|
1710
|
+
if (Array.isArray(node)) {
|
|
1711
|
+
for (let i = 0; i < node.length; i++) {
|
|
1712
|
+
if (isLast) applyAt(node, i);
|
|
1713
|
+
else maskPathInPlace(node[i], pathParts, replacement, depth + 1);
|
|
1714
|
+
}
|
|
1715
|
+
} else if (typeof node === 'object') {
|
|
1716
|
+
for (const key of Object.keys(node)) {
|
|
1717
|
+
if (isLast) applyAt(node, key);
|
|
1718
|
+
else maskPathInPlace(node[key], pathParts, replacement, depth + 1);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const index = Number(part);
|
|
1725
|
+
const isIndex = Number.isInteger(index) && String(index) === part;
|
|
1726
|
+
if (Array.isArray(node) && isIndex) {
|
|
1727
|
+
if (index < 0 || index >= node.length) return;
|
|
1728
|
+
if (isLast) applyAt(node, index);
|
|
1729
|
+
else maskPathInPlace(node[index], pathParts, replacement, depth + 1);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (typeof node !== 'object') return;
|
|
1734
|
+
if (!Object.prototype.hasOwnProperty.call(node, part)) return;
|
|
1735
|
+
if (isLast) applyAt(node, part);
|
|
1736
|
+
else maskPathInPlace(node[part], pathParts, replacement, depth + 1);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function applyMasking(
|
|
1740
|
+
target: ReproMaskTarget,
|
|
1741
|
+
value: any,
|
|
1742
|
+
req: MaskRequestContext,
|
|
1743
|
+
trace: TraceEventForFilter | null,
|
|
1744
|
+
masking: NormalizedMaskingConfig | null
|
|
1745
|
+
) {
|
|
1746
|
+
if (!masking || !masking.rules.length) return value;
|
|
1747
|
+
if (value === undefined) return value;
|
|
1748
|
+
for (const rule of masking.rules) {
|
|
1749
|
+
if (!rule.targets.includes(target)) continue;
|
|
1750
|
+
if (!matchesMaskWhen(rule.when, req, trace)) continue;
|
|
1751
|
+
const replacement = rule.replacement ?? masking.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1752
|
+
if (rule.keys) maskKeysInPlace(value, rule.keys, replacement);
|
|
1753
|
+
if (rule.paths.length) {
|
|
1754
|
+
rule.paths.forEach(parts => maskPathInPlace(value, parts, replacement, 0));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return value;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1487
1760
|
// ===================================================================
|
|
1488
1761
|
// reproMiddleware — unchanged behavior + passive per-request trace
|
|
1489
1762
|
// ===================================================================
|
|
@@ -1492,11 +1765,14 @@ export type ReproMiddlewareConfig = {
|
|
|
1492
1765
|
tenantId: string;
|
|
1493
1766
|
appSecret: string;
|
|
1494
1767
|
apiBase: string;
|
|
1495
|
-
/** Configure header capture/
|
|
1768
|
+
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
1496
1769
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
1770
|
+
/** Optional masking rules for request/response payloads and function traces. */
|
|
1771
|
+
masking?: ReproMaskingConfig;
|
|
1497
1772
|
};
|
|
1498
1773
|
|
|
1499
1774
|
export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
1775
|
+
const masking = normalizeMaskingConfig(cfg.masking);
|
|
1500
1776
|
return function (req: Request, res: Response, next: NextFunction) {
|
|
1501
1777
|
const sid = (req.headers['x-bug-session-id'] as string) || '';
|
|
1502
1778
|
const aid = (req.headers['x-bug-action-id'] as string) || '';
|
|
@@ -1509,8 +1785,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1509
1785
|
const rid = nextRequestId(requestEpochMs);
|
|
1510
1786
|
const t0 = requestStartRaw;
|
|
1511
1787
|
const url = (req as any).originalUrl || req.url || '/';
|
|
1788
|
+
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
1512
1789
|
const path = url; // back-compat
|
|
1513
1790
|
const key = normalizeRouteKey(req.method, url);
|
|
1791
|
+
const maskReq: MaskRequestContext = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
1514
1792
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
1515
1793
|
beginSessionRequest(sid);
|
|
1516
1794
|
|
|
@@ -1636,6 +1914,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1636
1914
|
unsubscribe = __TRACER__.tracer.on((ev: any) => {
|
|
1637
1915
|
if (!ev || ev.traceId !== tidNow) return;
|
|
1638
1916
|
|
|
1917
|
+
const sourceFileForLibrary = typeof ev.sourceFile === 'string' && ev.sourceFile
|
|
1918
|
+
? String(ev.sourceFile)
|
|
1919
|
+
: null;
|
|
1920
|
+
|
|
1639
1921
|
const evt: TraceEventRecord = {
|
|
1640
1922
|
t: alignTimestamp(ev.t),
|
|
1641
1923
|
type: ev.type,
|
|
@@ -1651,14 +1933,44 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1651
1933
|
evt.functionType = ev.functionType;
|
|
1652
1934
|
}
|
|
1653
1935
|
|
|
1936
|
+
const candidate: TraceEventForFilter = {
|
|
1937
|
+
type: evt.type,
|
|
1938
|
+
eventType: evt.type,
|
|
1939
|
+
functionType: evt.functionType ?? null,
|
|
1940
|
+
fn: evt.fn,
|
|
1941
|
+
wrapperClass: inferWrapperClassFromFn(evt.fn),
|
|
1942
|
+
file: evt.file ?? null,
|
|
1943
|
+
line: evt.line ?? null,
|
|
1944
|
+
depth: evt.depth,
|
|
1945
|
+
library: inferLibraryNameFromFile(sourceFileForLibrary ?? evt.file),
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1654
1948
|
if (ev.args !== undefined) {
|
|
1655
|
-
evt.args =
|
|
1949
|
+
evt.args = applyMasking(
|
|
1950
|
+
'trace.args',
|
|
1951
|
+
sanitizeTraceArgs(ev.args),
|
|
1952
|
+
maskReq,
|
|
1953
|
+
candidate,
|
|
1954
|
+
masking
|
|
1955
|
+
);
|
|
1656
1956
|
}
|
|
1657
1957
|
if (ev.returnValue !== undefined) {
|
|
1658
|
-
evt.returnValue =
|
|
1958
|
+
evt.returnValue = applyMasking(
|
|
1959
|
+
'trace.returnValue',
|
|
1960
|
+
sanitizeTraceValue(ev.returnValue),
|
|
1961
|
+
maskReq,
|
|
1962
|
+
candidate,
|
|
1963
|
+
masking
|
|
1964
|
+
);
|
|
1659
1965
|
}
|
|
1660
1966
|
if (ev.error !== undefined) {
|
|
1661
|
-
evt.error =
|
|
1967
|
+
evt.error = applyMasking(
|
|
1968
|
+
'trace.error',
|
|
1969
|
+
sanitizeTraceValue(ev.error),
|
|
1970
|
+
maskReq,
|
|
1971
|
+
candidate,
|
|
1972
|
+
masking
|
|
1973
|
+
);
|
|
1662
1974
|
}
|
|
1663
1975
|
if (ev.threw !== undefined) {
|
|
1664
1976
|
evt.threw = Boolean(ev.threw);
|
|
@@ -1667,16 +1979,6 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1667
1979
|
evt.unawaited = ev.unawaited === true;
|
|
1668
1980
|
}
|
|
1669
1981
|
|
|
1670
|
-
const candidate: TraceEventForFilter = {
|
|
1671
|
-
type: evt.type,
|
|
1672
|
-
eventType: evt.type,
|
|
1673
|
-
functionType: evt.functionType ?? null,
|
|
1674
|
-
fn: evt.fn,
|
|
1675
|
-
file: evt.file ?? null,
|
|
1676
|
-
depth: evt.depth,
|
|
1677
|
-
library: inferLibraryNameFromFile(evt.file),
|
|
1678
|
-
};
|
|
1679
|
-
|
|
1680
1982
|
const dropEvent = shouldDropTraceEvent(candidate);
|
|
1681
1983
|
const spanKey = normalizeSpanId(evt.spanId);
|
|
1682
1984
|
if (evt.type === 'enter') {
|
|
@@ -1759,9 +2061,55 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1759
2061
|
?? firstAppTrace
|
|
1760
2062
|
?? { fn: null, file: null, line: null, functionType: null };
|
|
1761
2063
|
const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
2064
|
+
const endpointTraceCtx: TraceEventForFilter | null = (() => {
|
|
2065
|
+
if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
|
|
2066
|
+
return {
|
|
2067
|
+
type: 'enter',
|
|
2068
|
+
eventType: 'enter',
|
|
2069
|
+
fn: chosenEndpoint.fn ?? undefined,
|
|
2070
|
+
wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
|
|
2071
|
+
file: chosenEndpoint.file ?? null,
|
|
2072
|
+
line: chosenEndpoint.line ?? null,
|
|
2073
|
+
functionType: chosenEndpoint.functionType ?? null,
|
|
2074
|
+
library: inferLibraryNameFromFile(chosenEndpoint.file),
|
|
2075
|
+
};
|
|
2076
|
+
})();
|
|
2077
|
+
|
|
2078
|
+
const requestBody = applyMasking(
|
|
2079
|
+
'request.body',
|
|
2080
|
+
sanitizeRequestSnapshot((req as any).body),
|
|
2081
|
+
maskReq,
|
|
2082
|
+
endpointTraceCtx,
|
|
2083
|
+
masking
|
|
2084
|
+
);
|
|
2085
|
+
const requestParams = applyMasking(
|
|
2086
|
+
'request.params',
|
|
2087
|
+
sanitizeRequestSnapshot((req as any).params),
|
|
2088
|
+
maskReq,
|
|
2089
|
+
endpointTraceCtx,
|
|
2090
|
+
masking
|
|
2091
|
+
);
|
|
2092
|
+
const requestQuery = applyMasking(
|
|
2093
|
+
'request.query',
|
|
2094
|
+
sanitizeRequestSnapshot((req as any).query),
|
|
2095
|
+
maskReq,
|
|
2096
|
+
endpointTraceCtx,
|
|
2097
|
+
masking
|
|
2098
|
+
);
|
|
2099
|
+
const maskedHeaders = applyMasking(
|
|
2100
|
+
'request.headers',
|
|
2101
|
+
requestHeaders,
|
|
2102
|
+
maskReq,
|
|
2103
|
+
endpointTraceCtx,
|
|
2104
|
+
masking
|
|
2105
|
+
);
|
|
2106
|
+
const responseBody = applyMasking(
|
|
2107
|
+
'response.body',
|
|
2108
|
+
capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody),
|
|
2109
|
+
maskReq,
|
|
2110
|
+
endpointTraceCtx,
|
|
2111
|
+
masking
|
|
2112
|
+
);
|
|
1765
2113
|
|
|
1766
2114
|
const requestPayload: Record<string, any> = {
|
|
1767
2115
|
rid,
|
|
@@ -1770,9 +2118,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1770
2118
|
path,
|
|
1771
2119
|
status: res.statusCode,
|
|
1772
2120
|
durMs: Date.now() - t0,
|
|
1773
|
-
headers:
|
|
2121
|
+
headers: maskedHeaders,
|
|
1774
2122
|
key,
|
|
1775
|
-
respBody:
|
|
2123
|
+
respBody: responseBody,
|
|
1776
2124
|
trace: traceBatches.length ? undefined : '[]',
|
|
1777
2125
|
};
|
|
1778
2126
|
if (requestBody !== undefined) requestPayload.body = requestBody;
|
package/tracer/runtime.js
CHANGED
|
@@ -128,6 +128,7 @@ const trace = {
|
|
|
128
128
|
fn,
|
|
129
129
|
file: meta?.file,
|
|
130
130
|
line: meta?.line,
|
|
131
|
+
sourceFile: meta?.sourceFile || null,
|
|
131
132
|
functionType: meta?.functionType || null,
|
|
132
133
|
traceId: ctx.traceId,
|
|
133
134
|
depth: ctx.depth,
|
|
@@ -144,6 +145,7 @@ const trace = {
|
|
|
144
145
|
fn: meta?.fn,
|
|
145
146
|
file: meta?.file,
|
|
146
147
|
line: meta?.line,
|
|
148
|
+
sourceFile: meta?.sourceFile || null,
|
|
147
149
|
functionType: meta?.functionType || null
|
|
148
150
|
};
|
|
149
151
|
const frameStack = ctx.__repro_frame_unawaited;
|
|
@@ -190,6 +192,7 @@ const trace = {
|
|
|
190
192
|
fn: baseMeta.fn,
|
|
191
193
|
file: baseMeta.file,
|
|
192
194
|
line: baseMeta.line,
|
|
195
|
+
sourceFile: baseMeta.sourceFile,
|
|
193
196
|
functionType: baseMeta.functionType || null,
|
|
194
197
|
traceId: traceIdAtExit,
|
|
195
198
|
depth: spanInfo.depth ?? depthAtExit,
|
|
@@ -601,10 +604,14 @@ if (!global.__repro_call) {
|
|
|
601
604
|
const name = (label && label.length) || fn.name
|
|
602
605
|
? (label && label.length ? label : fn.name)
|
|
603
606
|
: '(anonymous)';
|
|
604
|
-
const
|
|
607
|
+
const definitionFile = fn[SYM_SRC_FILE];
|
|
608
|
+
const normalizedCallFile = callFile ? String(callFile).trim() : '';
|
|
609
|
+
const callsiteFile = normalizedCallFile ? normalizedCallFile : null;
|
|
610
|
+
const callsiteLine = Number.isFinite(Number(callLine)) && Number(callLine) > 0 ? Number(callLine) : null;
|
|
605
611
|
const meta = {
|
|
606
|
-
file:
|
|
607
|
-
line:
|
|
612
|
+
file: callsiteFile || definitionFile || null,
|
|
613
|
+
line: callsiteLine,
|
|
614
|
+
sourceFile: definitionFile || null,
|
|
608
615
|
parentSpanId: forcedParentSpanId
|
|
609
616
|
};
|
|
610
617
|
|
|
@@ -638,7 +645,7 @@ if (!global.__repro_call) {
|
|
|
638
645
|
})();
|
|
639
646
|
|
|
640
647
|
const runExit = (detail) => {
|
|
641
|
-
const runner = () => trace.exit({ fn: name, file: meta.file, line: meta.line }, detail);
|
|
648
|
+
const runner = () => trace.exit({ fn: name, file: meta.file, line: meta.line, sourceFile: meta.sourceFile }, detail);
|
|
642
649
|
if (exitStore) return als.run(exitStore, runner);
|
|
643
650
|
return runner();
|
|
644
651
|
};
|
|
@@ -685,7 +692,7 @@ if (!global.__repro_call) {
|
|
|
685
692
|
runExit(exitDetailBase);
|
|
686
693
|
return out;
|
|
687
694
|
} catch (e) {
|
|
688
|
-
trace.exit({ fn: name, file: meta.file, line: meta.line }, { threw: true, error: e, args });
|
|
695
|
+
trace.exit({ fn: name, file: meta.file, line: meta.line, sourceFile: meta.sourceFile }, { threw: true, error: e, args });
|
|
689
696
|
throw e;
|
|
690
697
|
} finally {
|
|
691
698
|
if (pendingMarker) {
|