repro-nest 0.0.213 → 0.0.215
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 +55 -4
- package/dist/index.js +289 -28
- package/docs/tracing.md +69 -3
- package/package.json +1 -1
- package/src/index.ts +400 -36
- package/tracer/runtime.js +7 -1
package/dist/index.d.ts
CHANGED
|
@@ -48,17 +48,28 @@ export type TraceEventForFilter = {
|
|
|
48
48
|
eventType: TraceEventPhase;
|
|
49
49
|
functionType?: string | null;
|
|
50
50
|
fn?: string;
|
|
51
|
+
wrapperClass?: string | null;
|
|
51
52
|
file?: string | null;
|
|
52
53
|
depth?: number;
|
|
53
54
|
library?: string | null;
|
|
54
55
|
};
|
|
55
56
|
export type HeaderRule = string | RegExp;
|
|
56
57
|
export type HeaderCaptureOptions = {
|
|
57
|
-
/** When true, sensitive headers such as Authorization are kept; default
|
|
58
|
+
/** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
|
|
58
59
|
allowSensitiveHeaders?: boolean;
|
|
59
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Header names (string or RegExp) to mask.
|
|
62
|
+
* `dropHeaders` is kept for backward-compatibility and treated as an alias for `maskHeaders`.
|
|
63
|
+
*/
|
|
64
|
+
maskHeaders?: HeaderRule | HeaderRule[];
|
|
65
|
+
/** @deprecated Alias for {@link maskHeaders}. */
|
|
60
66
|
dropHeaders?: HeaderRule | HeaderRule[];
|
|
61
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Header names (string or RegExp) to keep unmasked, overriding defaults and `maskHeaders`.
|
|
69
|
+
* `keepHeaders` is kept for backward-compatibility and treated as an alias for `unmaskHeaders`.
|
|
70
|
+
*/
|
|
71
|
+
unmaskHeaders?: HeaderRule | HeaderRule[];
|
|
72
|
+
/** @deprecated Alias for {@link unmaskHeaders}. */
|
|
62
73
|
keepHeaders?: HeaderRule | HeaderRule[];
|
|
63
74
|
};
|
|
64
75
|
/** Lightweight helper to disable every trace emitted from specific files. */
|
|
@@ -78,6 +89,17 @@ export type DisableFunctionTraceRule = {
|
|
|
78
89
|
fn?: TraceRulePattern;
|
|
79
90
|
/** Function name (e.g. `"findOne"`, `/^UserService\./`). */
|
|
80
91
|
functionName?: TraceRulePattern;
|
|
92
|
+
/** Shortcut for {@link wrapperClass}. */
|
|
93
|
+
wrapper?: TraceRulePattern;
|
|
94
|
+
/**
|
|
95
|
+
* Wrapper/owner name derived from {@link functionName} (e.g. `"UserService"` in `"UserService.create"`).
|
|
96
|
+
* Useful when multiple functions share the same method name.
|
|
97
|
+
*/
|
|
98
|
+
wrapperClass?: TraceRulePattern;
|
|
99
|
+
/** Alias for {@link wrapperClass}. */
|
|
100
|
+
className?: TraceRulePattern;
|
|
101
|
+
/** Alias for {@link wrapperClass}. */
|
|
102
|
+
owner?: TraceRulePattern;
|
|
81
103
|
/** Absolute file path where the function was defined. */
|
|
82
104
|
file?: TraceRulePattern;
|
|
83
105
|
/** Shortcut for {@link library}. */
|
|
@@ -95,6 +117,33 @@ export type DisableFunctionTraceRule = {
|
|
|
95
117
|
};
|
|
96
118
|
export type DisableFunctionTracePredicate = (event: TraceEventForFilter) => boolean;
|
|
97
119
|
export type DisableFunctionTraceConfig = DisableFunctionTraceRule | DisableFunctionTracePredicate;
|
|
120
|
+
export type ReproMaskTarget = 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body' | 'trace.args' | 'trace.returnValue' | 'trace.error';
|
|
121
|
+
export type ReproMaskWhen = DisableFunctionTraceRule & {
|
|
122
|
+
/** Match HTTP method (e.g. `"GET"`, `/^post$/i`). */
|
|
123
|
+
method?: TraceRulePattern;
|
|
124
|
+
/** Match request path without query string (e.g. `"/api/auth/login"`). */
|
|
125
|
+
path?: TraceRulePattern;
|
|
126
|
+
/** Match normalized endpoint key (e.g. `"POST /api/auth/login"`). */
|
|
127
|
+
key?: TraceRulePattern;
|
|
128
|
+
};
|
|
129
|
+
export type ReproMaskRule = {
|
|
130
|
+
when?: ReproMaskWhen;
|
|
131
|
+
target: ReproMaskTarget | ReproMaskTarget[];
|
|
132
|
+
/**
|
|
133
|
+
* Dot/bracket paths to mask (supports `*`, `[0]`, `[*]`).
|
|
134
|
+
* Examples: `"password"`, `"user.token"`, `"items[*].serial"`, `"0.password"` (for trace args arrays).
|
|
135
|
+
*/
|
|
136
|
+
paths?: string | string[];
|
|
137
|
+
/** Key name patterns to mask anywhere in the payload. */
|
|
138
|
+
keys?: TraceRulePattern;
|
|
139
|
+
/** Override replacement value for this rule (defaults to config replacement / `"[REDACTED]"`). */
|
|
140
|
+
replacement?: any;
|
|
141
|
+
};
|
|
142
|
+
export type ReproMaskingConfig = {
|
|
143
|
+
/** Default replacement value (defaults to `"[REDACTED]"`). */
|
|
144
|
+
replacement?: any;
|
|
145
|
+
rules?: ReproMaskRule[] | null;
|
|
146
|
+
};
|
|
98
147
|
export declare function setDisabledFunctionTraces(rules?: DisableFunctionTraceConfig[] | null): void;
|
|
99
148
|
export declare function setDisabledFunctionTypes(patterns?: TraceRulePattern | null): void;
|
|
100
149
|
export declare function setDisabledTraceFiles(config?: DisableTraceFileConfig | DisableTraceFileConfig[] | null): void;
|
|
@@ -146,8 +195,10 @@ export type ReproMiddlewareConfig = {
|
|
|
146
195
|
tenantId: string;
|
|
147
196
|
appSecret: string;
|
|
148
197
|
apiBase: string;
|
|
149
|
-
/** Configure header capture/
|
|
198
|
+
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
150
199
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
200
|
+
/** Optional masking rules for request/response payloads and function traces. */
|
|
201
|
+
masking?: ReproMaskingConfig;
|
|
151
202
|
};
|
|
152
203
|
export declare function reproMiddleware(cfg: ReproMiddlewareConfig): (req: Request, res: Response, next: NextFunction) => void;
|
|
153
204
|
export declare function reproMongoosePlugin(cfg: {
|
package/dist/index.js
CHANGED
|
@@ -298,10 +298,31 @@ 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;
|
|
307
328
|
const libPattern = rule.lib ?? rule.library;
|
|
@@ -542,6 +563,26 @@ function captureSpanContextFromTracer(source) {
|
|
|
542
563
|
catch { }
|
|
543
564
|
return null;
|
|
544
565
|
}
|
|
566
|
+
function isExcludedSpanId(spanId) {
|
|
567
|
+
if (spanId === null || spanId === undefined)
|
|
568
|
+
return false;
|
|
569
|
+
try {
|
|
570
|
+
const excluded = getCtx().excludedSpanIds;
|
|
571
|
+
if (!excluded || excluded.size === 0)
|
|
572
|
+
return false;
|
|
573
|
+
return excluded.has(String(spanId));
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function shouldCaptureDbSpan(span) {
|
|
580
|
+
if (!span || span.spanId === null || span.spanId === undefined)
|
|
581
|
+
return false;
|
|
582
|
+
if (isExcludedSpanId(span.spanId))
|
|
583
|
+
return false;
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
545
586
|
function attachSpanContext(target, span) {
|
|
546
587
|
if (!target)
|
|
547
588
|
return target;
|
|
@@ -1177,6 +1218,7 @@ const DEFAULT_SENSITIVE_HEADERS = [
|
|
|
1177
1218
|
'cookie',
|
|
1178
1219
|
'set-cookie',
|
|
1179
1220
|
];
|
|
1221
|
+
const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
|
|
1180
1222
|
function normalizeHeaderRules(rules) {
|
|
1181
1223
|
return normalizePatternArray(rules || []);
|
|
1182
1224
|
}
|
|
@@ -1198,14 +1240,14 @@ function matchesHeaderRule(name, rules) {
|
|
|
1198
1240
|
}
|
|
1199
1241
|
function normalizeHeaderCaptureConfig(raw) {
|
|
1200
1242
|
if (raw === false) {
|
|
1201
|
-
return { enabled: false, allowSensitive: false,
|
|
1243
|
+
return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
|
|
1202
1244
|
}
|
|
1203
1245
|
const opts = raw && raw !== true ? raw : {};
|
|
1204
1246
|
return {
|
|
1205
1247
|
enabled: true,
|
|
1206
1248
|
allowSensitive: opts.allowSensitiveHeaders === true,
|
|
1207
|
-
|
|
1208
|
-
|
|
1249
|
+
mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
|
|
1250
|
+
unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
|
|
1209
1251
|
};
|
|
1210
1252
|
}
|
|
1211
1253
|
function sanitizeHeaderValue(value) {
|
|
@@ -1228,24 +1270,209 @@ function sanitizeHeaders(headers, rawCfg) {
|
|
|
1228
1270
|
const cfg = normalizeHeaderCaptureConfig(rawCfg);
|
|
1229
1271
|
if (!cfg.enabled)
|
|
1230
1272
|
return {};
|
|
1231
|
-
const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
|
|
1232
1273
|
const out = {};
|
|
1233
1274
|
for (const [rawKey, rawVal] of Object.entries(headers || {})) {
|
|
1234
1275
|
const key = String(rawKey || '').toLowerCase();
|
|
1235
1276
|
if (!key)
|
|
1236
1277
|
continue;
|
|
1237
|
-
const shouldDrop = matchesHeaderRule(key, dropList);
|
|
1238
|
-
const keep = matchesHeaderRule(key, cfg.keep);
|
|
1239
|
-
if (shouldDrop && !keep)
|
|
1240
|
-
continue;
|
|
1241
1278
|
const sanitizedValue = sanitizeHeaderValue(rawVal);
|
|
1242
1279
|
if (sanitizedValue !== undefined) {
|
|
1243
1280
|
out[key] = sanitizedValue;
|
|
1244
1281
|
}
|
|
1245
1282
|
}
|
|
1283
|
+
const maskList = cfg.allowSensitive ? cfg.mask : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.mask];
|
|
1284
|
+
Object.keys(out).forEach((key) => {
|
|
1285
|
+
if (!matchesHeaderRule(key, maskList))
|
|
1286
|
+
return;
|
|
1287
|
+
if (matchesHeaderRule(key, cfg.unmask))
|
|
1288
|
+
return;
|
|
1289
|
+
const current = out[key];
|
|
1290
|
+
if (Array.isArray(current)) {
|
|
1291
|
+
out[key] = current.map(() => DEFAULT_MASK_REPLACEMENT);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
out[key] = DEFAULT_MASK_REPLACEMENT;
|
|
1295
|
+
});
|
|
1246
1296
|
return out;
|
|
1247
1297
|
}
|
|
1298
|
+
function normalizeMaskTargets(target) {
|
|
1299
|
+
const out = Array.isArray(target) ? target : [target];
|
|
1300
|
+
return out.filter((t) => typeof t === 'string' && t.length > 0);
|
|
1301
|
+
}
|
|
1302
|
+
function parseMaskPath(raw) {
|
|
1303
|
+
if (!raw)
|
|
1304
|
+
return [];
|
|
1305
|
+
let path = String(raw).trim();
|
|
1306
|
+
if (!path)
|
|
1307
|
+
return [];
|
|
1308
|
+
if (path.startsWith('$.'))
|
|
1309
|
+
path = path.slice(2);
|
|
1310
|
+
if (path.startsWith('.'))
|
|
1311
|
+
path = path.slice(1);
|
|
1312
|
+
path = path.replace(/\[(\d+|\*)\]/g, '.$1');
|
|
1313
|
+
return path.split('.').map(s => s.trim()).filter(Boolean);
|
|
1314
|
+
}
|
|
1315
|
+
function normalizeMaskPaths(paths) {
|
|
1316
|
+
if (!paths)
|
|
1317
|
+
return [];
|
|
1318
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1319
|
+
return list
|
|
1320
|
+
.map(p => parseMaskPath(p))
|
|
1321
|
+
.filter(parts => parts.length > 0);
|
|
1322
|
+
}
|
|
1323
|
+
function normalizeMaskingConfig(raw) {
|
|
1324
|
+
const rules = raw?.rules;
|
|
1325
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
1326
|
+
return null;
|
|
1327
|
+
const replacement = raw?.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1328
|
+
const normalized = [];
|
|
1329
|
+
for (const rule of rules) {
|
|
1330
|
+
if (!rule)
|
|
1331
|
+
continue;
|
|
1332
|
+
const targets = normalizeMaskTargets(rule.target);
|
|
1333
|
+
if (!targets.length)
|
|
1334
|
+
continue;
|
|
1335
|
+
const paths = normalizeMaskPaths(rule.paths);
|
|
1336
|
+
const keys = rule.keys ?? undefined;
|
|
1337
|
+
if (!paths.length && !keys)
|
|
1338
|
+
continue;
|
|
1339
|
+
normalized.push({
|
|
1340
|
+
when: rule.when,
|
|
1341
|
+
targets,
|
|
1342
|
+
paths,
|
|
1343
|
+
keys,
|
|
1344
|
+
replacement: rule.replacement ?? replacement,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
return normalized.length ? { replacement, rules: normalized } : null;
|
|
1348
|
+
}
|
|
1349
|
+
function maskWhenRequiresTrace(when) {
|
|
1350
|
+
return Boolean(when.fn ||
|
|
1351
|
+
when.functionName ||
|
|
1352
|
+
when.wrapper ||
|
|
1353
|
+
when.wrapperClass ||
|
|
1354
|
+
when.className ||
|
|
1355
|
+
when.owner ||
|
|
1356
|
+
when.file ||
|
|
1357
|
+
when.lib ||
|
|
1358
|
+
when.library ||
|
|
1359
|
+
when.type ||
|
|
1360
|
+
when.functionType ||
|
|
1361
|
+
when.event ||
|
|
1362
|
+
when.eventType);
|
|
1363
|
+
}
|
|
1364
|
+
function matchesMaskWhen(when, req, trace) {
|
|
1365
|
+
if (!when)
|
|
1366
|
+
return true;
|
|
1367
|
+
if (!matchesPattern(req.method, when.method))
|
|
1368
|
+
return false;
|
|
1369
|
+
if (!matchesPattern(req.path, when.path))
|
|
1370
|
+
return false;
|
|
1371
|
+
if (!matchesPattern(req.key, when.key))
|
|
1372
|
+
return false;
|
|
1373
|
+
if (maskWhenRequiresTrace(when)) {
|
|
1374
|
+
if (!trace)
|
|
1375
|
+
return false;
|
|
1376
|
+
if (!matchesRule(when, trace))
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
function maskKeysInPlace(node, keys, replacement) {
|
|
1382
|
+
if (!node)
|
|
1383
|
+
return;
|
|
1384
|
+
if (Array.isArray(node)) {
|
|
1385
|
+
node.forEach(item => maskKeysInPlace(item, keys, replacement));
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof node !== 'object')
|
|
1389
|
+
return;
|
|
1390
|
+
Object.keys(node).forEach((key) => {
|
|
1391
|
+
if (matchesPattern(key, keys, false)) {
|
|
1392
|
+
try {
|
|
1393
|
+
node[key] = replacement;
|
|
1394
|
+
}
|
|
1395
|
+
catch { }
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
maskKeysInPlace(node[key], keys, replacement);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
function maskPathInPlace(node, pathParts, replacement, depth = 0) {
|
|
1402
|
+
if (!node)
|
|
1403
|
+
return;
|
|
1404
|
+
if (depth >= pathParts.length)
|
|
1405
|
+
return;
|
|
1406
|
+
const part = pathParts[depth];
|
|
1407
|
+
const isLast = depth === pathParts.length - 1;
|
|
1408
|
+
const applyAt = (container, key) => {
|
|
1409
|
+
if (!container)
|
|
1410
|
+
return;
|
|
1411
|
+
try {
|
|
1412
|
+
container[key] = replacement;
|
|
1413
|
+
}
|
|
1414
|
+
catch { }
|
|
1415
|
+
};
|
|
1416
|
+
if (part === '*') {
|
|
1417
|
+
if (Array.isArray(node)) {
|
|
1418
|
+
for (let i = 0; i < node.length; i++) {
|
|
1419
|
+
if (isLast)
|
|
1420
|
+
applyAt(node, i);
|
|
1421
|
+
else
|
|
1422
|
+
maskPathInPlace(node[i], pathParts, replacement, depth + 1);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
else if (typeof node === 'object') {
|
|
1426
|
+
for (const key of Object.keys(node)) {
|
|
1427
|
+
if (isLast)
|
|
1428
|
+
applyAt(node, key);
|
|
1429
|
+
else
|
|
1430
|
+
maskPathInPlace(node[key], pathParts, replacement, depth + 1);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const index = Number(part);
|
|
1436
|
+
const isIndex = Number.isInteger(index) && String(index) === part;
|
|
1437
|
+
if (Array.isArray(node) && isIndex) {
|
|
1438
|
+
if (index < 0 || index >= node.length)
|
|
1439
|
+
return;
|
|
1440
|
+
if (isLast)
|
|
1441
|
+
applyAt(node, index);
|
|
1442
|
+
else
|
|
1443
|
+
maskPathInPlace(node[index], pathParts, replacement, depth + 1);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (typeof node !== 'object')
|
|
1447
|
+
return;
|
|
1448
|
+
if (!Object.prototype.hasOwnProperty.call(node, part))
|
|
1449
|
+
return;
|
|
1450
|
+
if (isLast)
|
|
1451
|
+
applyAt(node, part);
|
|
1452
|
+
else
|
|
1453
|
+
maskPathInPlace(node[part], pathParts, replacement, depth + 1);
|
|
1454
|
+
}
|
|
1455
|
+
function applyMasking(target, value, req, trace, masking) {
|
|
1456
|
+
if (!masking || !masking.rules.length)
|
|
1457
|
+
return value;
|
|
1458
|
+
if (value === undefined)
|
|
1459
|
+
return value;
|
|
1460
|
+
for (const rule of masking.rules) {
|
|
1461
|
+
if (!rule.targets.includes(target))
|
|
1462
|
+
continue;
|
|
1463
|
+
if (!matchesMaskWhen(rule.when, req, trace))
|
|
1464
|
+
continue;
|
|
1465
|
+
const replacement = rule.replacement ?? masking.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1466
|
+
if (rule.keys)
|
|
1467
|
+
maskKeysInPlace(value, rule.keys, replacement);
|
|
1468
|
+
if (rule.paths.length) {
|
|
1469
|
+
rule.paths.forEach(parts => maskPathInPlace(value, parts, replacement, 0));
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return value;
|
|
1473
|
+
}
|
|
1248
1474
|
function reproMiddleware(cfg) {
|
|
1475
|
+
const masking = normalizeMaskingConfig(cfg.masking);
|
|
1249
1476
|
return function (req, res, next) {
|
|
1250
1477
|
const sid = req.headers['x-bug-session-id'] || '';
|
|
1251
1478
|
const aid = req.headers['x-bug-action-id'] || '';
|
|
@@ -1258,8 +1485,10 @@ function reproMiddleware(cfg) {
|
|
|
1258
1485
|
const rid = nextRequestId(requestEpochMs);
|
|
1259
1486
|
const t0 = requestStartRaw;
|
|
1260
1487
|
const url = req.originalUrl || req.url || '/';
|
|
1488
|
+
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
1261
1489
|
const path = url; // back-compat
|
|
1262
1490
|
const key = normalizeRouteKey(req.method, url);
|
|
1491
|
+
const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
1263
1492
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
1264
1493
|
beginSessionRequest(sid);
|
|
1265
1494
|
// ---- response body capture (unchanged) ----
|
|
@@ -1294,7 +1523,7 @@ function reproMiddleware(cfg) {
|
|
|
1294
1523
|
}
|
|
1295
1524
|
return fn();
|
|
1296
1525
|
};
|
|
1297
|
-
runInTrace(() => als.run({ sid, aid, clockSkewMs }, () => {
|
|
1526
|
+
runInTrace(() => als.run({ sid, aid, clockSkewMs, excludedSpanIds: new Set() }, () => {
|
|
1298
1527
|
const events = [];
|
|
1299
1528
|
let endpointTrace = null;
|
|
1300
1529
|
let preferredAppTrace = null;
|
|
@@ -1419,14 +1648,24 @@ function reproMiddleware(cfg) {
|
|
|
1419
1648
|
if (ev.functionType !== undefined) {
|
|
1420
1649
|
evt.functionType = ev.functionType;
|
|
1421
1650
|
}
|
|
1651
|
+
const candidate = {
|
|
1652
|
+
type: evt.type,
|
|
1653
|
+
eventType: evt.type,
|
|
1654
|
+
functionType: evt.functionType ?? null,
|
|
1655
|
+
fn: evt.fn,
|
|
1656
|
+
wrapperClass: inferWrapperClassFromFn(evt.fn),
|
|
1657
|
+
file: evt.file ?? null,
|
|
1658
|
+
depth: evt.depth,
|
|
1659
|
+
library: inferLibraryNameFromFile(evt.file),
|
|
1660
|
+
};
|
|
1422
1661
|
if (ev.args !== undefined) {
|
|
1423
|
-
evt.args = sanitizeTraceArgs(ev.args);
|
|
1662
|
+
evt.args = applyMasking('trace.args', sanitizeTraceArgs(ev.args), maskReq, candidate, masking);
|
|
1424
1663
|
}
|
|
1425
1664
|
if (ev.returnValue !== undefined) {
|
|
1426
|
-
evt.returnValue = sanitizeTraceValue(ev.returnValue);
|
|
1665
|
+
evt.returnValue = applyMasking('trace.returnValue', sanitizeTraceValue(ev.returnValue), maskReq, candidate, masking);
|
|
1427
1666
|
}
|
|
1428
1667
|
if (ev.error !== undefined) {
|
|
1429
|
-
evt.error = sanitizeTraceValue(ev.error);
|
|
1668
|
+
evt.error = applyMasking('trace.error', sanitizeTraceValue(ev.error), maskReq, candidate, masking);
|
|
1430
1669
|
}
|
|
1431
1670
|
if (ev.threw !== undefined) {
|
|
1432
1671
|
evt.threw = Boolean(ev.threw);
|
|
@@ -1434,15 +1673,7 @@ function reproMiddleware(cfg) {
|
|
|
1434
1673
|
if (ev.unawaited !== undefined) {
|
|
1435
1674
|
evt.unawaited = ev.unawaited === true;
|
|
1436
1675
|
}
|
|
1437
|
-
const
|
|
1438
|
-
type: evt.type,
|
|
1439
|
-
eventType: evt.type,
|
|
1440
|
-
functionType: evt.functionType ?? null,
|
|
1441
|
-
fn: evt.fn,
|
|
1442
|
-
file: evt.file ?? null,
|
|
1443
|
-
depth: evt.depth,
|
|
1444
|
-
library: inferLibraryNameFromFile(evt.file),
|
|
1445
|
-
};
|
|
1676
|
+
const dropEvent = shouldDropTraceEvent(candidate);
|
|
1446
1677
|
const spanKey = normalizeSpanId(evt.spanId);
|
|
1447
1678
|
if (evt.type === 'enter') {
|
|
1448
1679
|
lastEventAt = Date.now();
|
|
@@ -1462,7 +1693,13 @@ function reproMiddleware(cfg) {
|
|
|
1462
1693
|
anonymousSpanDepth = Math.max(0, anonymousSpanDepth - 1);
|
|
1463
1694
|
}
|
|
1464
1695
|
}
|
|
1465
|
-
if (
|
|
1696
|
+
if (dropEvent) {
|
|
1697
|
+
if (evt.type === 'enter' && spanKey) {
|
|
1698
|
+
try {
|
|
1699
|
+
getCtx().excludedSpanIds?.add(spanKey);
|
|
1700
|
+
}
|
|
1701
|
+
catch { }
|
|
1702
|
+
}
|
|
1466
1703
|
if (finished) {
|
|
1467
1704
|
scheduleIdleFlush();
|
|
1468
1705
|
}
|
|
@@ -1519,9 +1756,24 @@ function reproMiddleware(cfg) {
|
|
|
1519
1756
|
?? firstAppTrace
|
|
1520
1757
|
?? { fn: null, file: null, line: null, functionType: null };
|
|
1521
1758
|
const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1759
|
+
const endpointTraceCtx = (() => {
|
|
1760
|
+
if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
|
|
1761
|
+
return null;
|
|
1762
|
+
return {
|
|
1763
|
+
type: 'enter',
|
|
1764
|
+
eventType: 'enter',
|
|
1765
|
+
fn: chosenEndpoint.fn ?? undefined,
|
|
1766
|
+
wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
|
|
1767
|
+
file: chosenEndpoint.file ?? null,
|
|
1768
|
+
functionType: chosenEndpoint.functionType ?? null,
|
|
1769
|
+
library: inferLibraryNameFromFile(chosenEndpoint.file),
|
|
1770
|
+
};
|
|
1771
|
+
})();
|
|
1772
|
+
const requestBody = applyMasking('request.body', sanitizeRequestSnapshot(req.body), maskReq, endpointTraceCtx, masking);
|
|
1773
|
+
const requestParams = applyMasking('request.params', sanitizeRequestSnapshot(req.params), maskReq, endpointTraceCtx, masking);
|
|
1774
|
+
const requestQuery = applyMasking('request.query', sanitizeRequestSnapshot(req.query), maskReq, endpointTraceCtx, masking);
|
|
1775
|
+
const maskedHeaders = applyMasking('request.headers', requestHeaders, maskReq, endpointTraceCtx, masking);
|
|
1776
|
+
const responseBody = applyMasking('response.body', capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody), maskReq, endpointTraceCtx, masking);
|
|
1525
1777
|
const requestPayload = {
|
|
1526
1778
|
rid,
|
|
1527
1779
|
method: req.method,
|
|
@@ -1529,9 +1781,9 @@ function reproMiddleware(cfg) {
|
|
|
1529
1781
|
path,
|
|
1530
1782
|
status: res.statusCode,
|
|
1531
1783
|
durMs: Date.now() - t0,
|
|
1532
|
-
headers:
|
|
1784
|
+
headers: maskedHeaders,
|
|
1533
1785
|
key,
|
|
1534
|
-
respBody:
|
|
1786
|
+
respBody: responseBody,
|
|
1535
1787
|
trace: traceBatches.length ? undefined : '[]',
|
|
1536
1788
|
};
|
|
1537
1789
|
if (requestBody !== undefined)
|
|
@@ -1646,6 +1898,8 @@ function reproMongoosePlugin(cfg) {
|
|
|
1646
1898
|
const after = this.toObject({ depopulate: true });
|
|
1647
1899
|
const collection = meta.collection || resolveCollectionOrWarn(this, 'doc');
|
|
1648
1900
|
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
1901
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
1902
|
+
return;
|
|
1649
1903
|
const query = meta.wasNew
|
|
1650
1904
|
? { op: 'insertOne', doc: after }
|
|
1651
1905
|
: { filter: { _id: this._id }, update: buildMinimalUpdate(before, after), options: { upsert: false } };
|
|
@@ -1688,6 +1942,8 @@ function reproMongoosePlugin(cfg) {
|
|
|
1688
1942
|
const after = res ?? null;
|
|
1689
1943
|
const collection = this.__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
1690
1944
|
const spanContext = this.__repro_spanContext || captureSpanContextFromTracer(this);
|
|
1945
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
1946
|
+
return;
|
|
1691
1947
|
const pk = after?._id ?? before?._id;
|
|
1692
1948
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, getCtx().sid, {
|
|
1693
1949
|
entries: [{
|
|
@@ -1728,6 +1984,8 @@ function reproMongoosePlugin(cfg) {
|
|
|
1728
1984
|
const collection = this.__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
1729
1985
|
const filter = this.__repro_filter ?? { _id: before._id };
|
|
1730
1986
|
const spanContext = this.__repro_spanContext || captureSpanContextFromTracer(this);
|
|
1987
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
1988
|
+
return;
|
|
1731
1989
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, getCtx().sid, {
|
|
1732
1990
|
entries: [{
|
|
1733
1991
|
actionId: getCtx().aid,
|
|
@@ -2039,6 +2297,9 @@ function dehydrateComplexValue(value) {
|
|
|
2039
2297
|
function emitDbQuery(cfg, sid, aid, payload) {
|
|
2040
2298
|
if (!sid)
|
|
2041
2299
|
return;
|
|
2300
|
+
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
2301
|
+
if (!shouldCaptureDbSpan(spanContext))
|
|
2302
|
+
return;
|
|
2042
2303
|
const dbEntry = attachSpanContext({
|
|
2043
2304
|
collection: payload.collection,
|
|
2044
2305
|
op: payload.op,
|
|
@@ -2047,7 +2308,7 @@ function emitDbQuery(cfg, sid, aid, payload) {
|
|
|
2047
2308
|
durMs: payload.durMs ?? undefined,
|
|
2048
2309
|
pk: null, before: null, after: null,
|
|
2049
2310
|
error: payload.error ?? undefined,
|
|
2050
|
-
},
|
|
2311
|
+
}, spanContext);
|
|
2051
2312
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, sid, {
|
|
2052
2313
|
entries: [{
|
|
2053
2314
|
actionId: aid ?? null,
|
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,6 +50,7 @@ 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
|
+
| `wrapper`/`wrapperClass`/`className`/`owner` | Match the wrapper/owner inferred from the function name (e.g. `"UserService"` in `"UserService.create"`). |
|
|
53
54
|
| `file` | Match the absolute source filename, useful for filtering entire modules. |
|
|
54
55
|
| `lib`/`library` | Match the npm package inferred from the file path (e.g. `"mongoose"`). |
|
|
55
56
|
| `type`/`functionType` | Match the detected function kind (e.g. `"constructor"`, `"method"`, `"arrow"`). |
|
|
@@ -64,7 +65,7 @@ Provide a function that receives the raw trace event and returns `true` when it
|
|
|
64
65
|
should be discarded. Use this form for complex, stateful, or cross-field logic.
|
|
65
66
|
|
|
66
67
|
```ts
|
|
67
|
-
import { setDisabledFunctionTraces } from '
|
|
68
|
+
import { setDisabledFunctionTraces } from 'repro-nest';
|
|
68
69
|
|
|
69
70
|
setDisabledFunctionTraces([
|
|
70
71
|
(event) => event.library === 'mongoose' && event.functionType === 'constructor',
|
|
@@ -74,6 +75,71 @@ setDisabledFunctionTraces([
|
|
|
74
75
|
Pass `null` or an empty array to `initReproTracing` or `setDisabledFunctionTraces`
|
|
75
76
|
to remove previously configured rules.
|
|
76
77
|
|
|
78
|
+
## Payload masking (`reproMiddleware`)
|
|
79
|
+
|
|
80
|
+
`reproMiddleware` can mask request/response payloads, request headers, and traced
|
|
81
|
+
function inputs/outputs before they are persisted.
|
|
82
|
+
|
|
83
|
+
### Targets
|
|
84
|
+
|
|
85
|
+
- `request.headers`
|
|
86
|
+
- `request.body`
|
|
87
|
+
- `request.params`
|
|
88
|
+
- `request.query`
|
|
89
|
+
- `response.body`
|
|
90
|
+
- `trace.args`
|
|
91
|
+
- `trace.returnValue`
|
|
92
|
+
- `trace.error`
|
|
93
|
+
|
|
94
|
+
### Rules
|
|
95
|
+
|
|
96
|
+
- `when.method` / `when.path` / `when.key` scope rules by endpoint (`key` is `"METHOD /path"` without query string).
|
|
97
|
+
- For function-specific rules, `when` supports the same fields as `disableFunctionTraces` (`fn`, `wrapperClass`, `file`, etc). This is useful when multiple functions share the same name.
|
|
98
|
+
- `paths` uses dot/bracket syntax and supports `*`, `[0]`, `[*]` (example: `"items[*].token"` or `"0.password"` for trace args arrays).
|
|
99
|
+
- `keys` masks matching key names anywhere in the payload (string/RegExp/array).
|
|
100
|
+
- `replacement` overrides the default replacement value (defaults to `"[REDACTED]"`).
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { reproMiddleware } from 'repro-nest';
|
|
104
|
+
|
|
105
|
+
app.use(reproMiddleware({
|
|
106
|
+
appId,
|
|
107
|
+
tenantId,
|
|
108
|
+
appSecret,
|
|
109
|
+
apiBase,
|
|
110
|
+
masking: {
|
|
111
|
+
rules: [
|
|
112
|
+
{
|
|
113
|
+
when: { key: 'POST /api/auth/login' },
|
|
114
|
+
target: 'request.body',
|
|
115
|
+
paths: ['password'],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
when: { wrapperClass: 'AuthService', functionName: 'login', file: '/app/src/auth/auth.service.ts' },
|
|
119
|
+
target: ['trace.args', 'trace.returnValue', 'trace.error'],
|
|
120
|
+
keys: [/token/i],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
target: 'request.headers',
|
|
124
|
+
keys: [/authorization/i, /cookie/i],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Header capture/masking (`captureHeaders`)
|
|
132
|
+
|
|
133
|
+
Headers are captured by default with sensitive values masked. Configure `captureHeaders`
|
|
134
|
+
to change the behavior:
|
|
135
|
+
|
|
136
|
+
- `captureHeaders: false` disables header capture entirely.
|
|
137
|
+
- `captureHeaders: true` (or omitted) captures headers and masks sensitive ones.
|
|
138
|
+
- `captureHeaders.allowSensitiveHeaders: true` keeps default sensitive headers unmasked (use with care).
|
|
139
|
+
- `captureHeaders.maskHeaders` adds additional header names to mask.
|
|
140
|
+
- `captureHeaders.unmaskHeaders` keeps specific header names unmasked (overrides defaults and `maskHeaders`).
|
|
141
|
+
- `captureHeaders.dropHeaders` / `captureHeaders.keepHeaders` are legacy aliases for `maskHeaders` / `unmaskHeaders`.
|
|
142
|
+
|
|
77
143
|
## `logFunctionCalls`
|
|
78
144
|
|
|
79
145
|
Set to `true` to enable verbose console logging of function entry/exit events at
|
|
@@ -81,7 +147,7 @@ runtime, or to `false` to silence them. The value is forwarded to
|
|
|
81
147
|
`setReproTraceLogsEnabled`, so you can also toggle logging later in the process:
|
|
82
148
|
|
|
83
149
|
```ts
|
|
84
|
-
import { enableReproTraceLogs, disableReproTraceLogs } from '
|
|
150
|
+
import { enableReproTraceLogs, disableReproTraceLogs } from 'repro-nest';
|
|
85
151
|
|
|
86
152
|
enableReproTraceLogs();
|
|
87
153
|
// ...
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -236,6 +236,7 @@ export type TraceEventForFilter = {
|
|
|
236
236
|
eventType: TraceEventPhase;
|
|
237
237
|
functionType?: string | null;
|
|
238
238
|
fn?: string;
|
|
239
|
+
wrapperClass?: string | null;
|
|
239
240
|
file?: string | null;
|
|
240
241
|
depth?: number;
|
|
241
242
|
library?: string | null;
|
|
@@ -267,19 +268,29 @@ type EndpointTraceInfo = {
|
|
|
267
268
|
|
|
268
269
|
export type HeaderRule = string | RegExp;
|
|
269
270
|
export type HeaderCaptureOptions = {
|
|
270
|
-
/** When true, sensitive headers such as Authorization are kept; default
|
|
271
|
+
/** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
|
|
271
272
|
allowSensitiveHeaders?: boolean;
|
|
272
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Header names (string or RegExp) to mask.
|
|
275
|
+
* `dropHeaders` is kept for backward-compatibility and treated as an alias for `maskHeaders`.
|
|
276
|
+
*/
|
|
277
|
+
maskHeaders?: HeaderRule | HeaderRule[];
|
|
278
|
+
/** @deprecated Alias for {@link maskHeaders}. */
|
|
273
279
|
dropHeaders?: HeaderRule | HeaderRule[];
|
|
274
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Header names (string or RegExp) to keep unmasked, overriding defaults and `maskHeaders`.
|
|
282
|
+
* `keepHeaders` is kept for backward-compatibility and treated as an alias for `unmaskHeaders`.
|
|
283
|
+
*/
|
|
284
|
+
unmaskHeaders?: HeaderRule | HeaderRule[];
|
|
285
|
+
/** @deprecated Alias for {@link unmaskHeaders}. */
|
|
275
286
|
keepHeaders?: HeaderRule | HeaderRule[];
|
|
276
287
|
};
|
|
277
288
|
|
|
278
289
|
type NormalizedHeaderCapture = {
|
|
279
290
|
enabled: boolean;
|
|
280
291
|
allowSensitive: boolean;
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
mask: HeaderRule[];
|
|
293
|
+
unmask: HeaderRule[];
|
|
283
294
|
};
|
|
284
295
|
|
|
285
296
|
/** Lightweight helper to disable every trace emitted from specific files. */
|
|
@@ -301,6 +312,17 @@ export type DisableFunctionTraceRule = {
|
|
|
301
312
|
fn?: TraceRulePattern;
|
|
302
313
|
/** Function name (e.g. `"findOne"`, `/^UserService\./`). */
|
|
303
314
|
functionName?: TraceRulePattern;
|
|
315
|
+
/** Shortcut for {@link wrapperClass}. */
|
|
316
|
+
wrapper?: TraceRulePattern;
|
|
317
|
+
/**
|
|
318
|
+
* Wrapper/owner name derived from {@link functionName} (e.g. `"UserService"` in `"UserService.create"`).
|
|
319
|
+
* Useful when multiple functions share the same method name.
|
|
320
|
+
*/
|
|
321
|
+
wrapperClass?: TraceRulePattern;
|
|
322
|
+
/** Alias for {@link wrapperClass}. */
|
|
323
|
+
className?: TraceRulePattern;
|
|
324
|
+
/** Alias for {@link wrapperClass}. */
|
|
325
|
+
owner?: TraceRulePattern;
|
|
304
326
|
/** Absolute file path where the function was defined. */
|
|
305
327
|
file?: TraceRulePattern;
|
|
306
328
|
/** Shortcut for {@link library}. */
|
|
@@ -323,6 +345,45 @@ export type DisableFunctionTraceConfig =
|
|
|
323
345
|
| DisableFunctionTraceRule
|
|
324
346
|
| DisableFunctionTracePredicate;
|
|
325
347
|
|
|
348
|
+
export type ReproMaskTarget =
|
|
349
|
+
| 'request.headers'
|
|
350
|
+
| 'request.body'
|
|
351
|
+
| 'request.params'
|
|
352
|
+
| 'request.query'
|
|
353
|
+
| 'response.body'
|
|
354
|
+
| 'trace.args'
|
|
355
|
+
| 'trace.returnValue'
|
|
356
|
+
| 'trace.error';
|
|
357
|
+
|
|
358
|
+
export type ReproMaskWhen = DisableFunctionTraceRule & {
|
|
359
|
+
/** Match HTTP method (e.g. `"GET"`, `/^post$/i`). */
|
|
360
|
+
method?: TraceRulePattern;
|
|
361
|
+
/** Match request path without query string (e.g. `"/api/auth/login"`). */
|
|
362
|
+
path?: TraceRulePattern;
|
|
363
|
+
/** Match normalized endpoint key (e.g. `"POST /api/auth/login"`). */
|
|
364
|
+
key?: TraceRulePattern;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export type ReproMaskRule = {
|
|
368
|
+
when?: ReproMaskWhen;
|
|
369
|
+
target: ReproMaskTarget | ReproMaskTarget[];
|
|
370
|
+
/**
|
|
371
|
+
* Dot/bracket paths to mask (supports `*`, `[0]`, `[*]`).
|
|
372
|
+
* Examples: `"password"`, `"user.token"`, `"items[*].serial"`, `"0.password"` (for trace args arrays).
|
|
373
|
+
*/
|
|
374
|
+
paths?: string | string[];
|
|
375
|
+
/** Key name patterns to mask anywhere in the payload. */
|
|
376
|
+
keys?: TraceRulePattern;
|
|
377
|
+
/** Override replacement value for this rule (defaults to config replacement / `"[REDACTED]"`). */
|
|
378
|
+
replacement?: any;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export type ReproMaskingConfig = {
|
|
382
|
+
/** Default replacement value (defaults to `"[REDACTED]"`). */
|
|
383
|
+
replacement?: any;
|
|
384
|
+
rules?: ReproMaskRule[] | null;
|
|
385
|
+
};
|
|
386
|
+
|
|
326
387
|
const DEFAULT_INTERCEPTOR_TRACE_RULES: DisableFunctionTraceConfig[] = [
|
|
327
388
|
{ fn: /switchToHttp$/i },
|
|
328
389
|
{ fn: /intercept$/i },
|
|
@@ -403,10 +464,30 @@ function inferLibraryNameFromFile(file?: string | null): string | null {
|
|
|
403
464
|
return segments[0] || null;
|
|
404
465
|
}
|
|
405
466
|
|
|
467
|
+
function inferWrapperClassFromFn(fn?: string | null): string | null {
|
|
468
|
+
if (!fn) return null;
|
|
469
|
+
const raw = String(fn);
|
|
470
|
+
if (!raw) return null;
|
|
471
|
+
const hashIdx = raw.indexOf('#');
|
|
472
|
+
if (hashIdx > 0) {
|
|
473
|
+
const left = raw.slice(0, hashIdx).trim();
|
|
474
|
+
return left || null;
|
|
475
|
+
}
|
|
476
|
+
const dotIdx = raw.lastIndexOf('.');
|
|
477
|
+
if (dotIdx > 0) {
|
|
478
|
+
const left = raw.slice(0, dotIdx).trim();
|
|
479
|
+
return left || null;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
406
484
|
function matchesRule(rule: DisableFunctionTraceRule, event: TraceEventForFilter): boolean {
|
|
407
485
|
const namePattern = rule.fn ?? rule.functionName;
|
|
408
486
|
if (!matchesPattern(event.fn, namePattern)) return false;
|
|
409
487
|
|
|
488
|
+
const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
|
|
489
|
+
if (!matchesPattern(event.wrapperClass, wrapperPattern)) return false;
|
|
490
|
+
|
|
410
491
|
if (!matchesPattern(event.file, rule.file)) return false;
|
|
411
492
|
|
|
412
493
|
const libPattern = rule.lib ?? rule.library;
|
|
@@ -736,6 +817,23 @@ function captureSpanContextFromTracer(source?: any): SpanContext | null {
|
|
|
736
817
|
return null;
|
|
737
818
|
}
|
|
738
819
|
|
|
820
|
+
function isExcludedSpanId(spanId: string | number | null | undefined): boolean {
|
|
821
|
+
if (spanId === null || spanId === undefined) return false;
|
|
822
|
+
try {
|
|
823
|
+
const excluded = (getCtx() as Ctx).excludedSpanIds;
|
|
824
|
+
if (!excluded || excluded.size === 0) return false;
|
|
825
|
+
return excluded.has(String(spanId));
|
|
826
|
+
} catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function shouldCaptureDbSpan(span: SpanContext | null | undefined): span is SpanContext {
|
|
832
|
+
if (!span || span.spanId === null || span.spanId === undefined) return false;
|
|
833
|
+
if (isExcludedSpanId(span.spanId)) return false;
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
|
|
739
837
|
function attachSpanContext<T extends Record<string, any>>(target: T, span?: SpanContext | null): T {
|
|
740
838
|
if (!target) return target;
|
|
741
839
|
const ctx = span ?? captureSpanContextFromTracer();
|
|
@@ -745,7 +843,7 @@ function attachSpanContext<T extends Record<string, any>>(target: T, span?: Span
|
|
|
745
843
|
return target;
|
|
746
844
|
}
|
|
747
845
|
|
|
748
|
-
type Ctx = { sid?: string; aid?: string; clockSkewMs?: number };
|
|
846
|
+
type Ctx = { sid?: string; aid?: string; clockSkewMs?: number; excludedSpanIds?: Set<string> };
|
|
749
847
|
const als = new AsyncLocalStorage<Ctx>();
|
|
750
848
|
const getCtx = () => als.getStore() || {};
|
|
751
849
|
|
|
@@ -1404,6 +1502,8 @@ const DEFAULT_SENSITIVE_HEADERS: Array<string | RegExp> = [
|
|
|
1404
1502
|
'set-cookie',
|
|
1405
1503
|
];
|
|
1406
1504
|
|
|
1505
|
+
const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
|
|
1506
|
+
|
|
1407
1507
|
function normalizeHeaderRules(rules?: HeaderRule | HeaderRule[] | null): HeaderRule[] {
|
|
1408
1508
|
return normalizePatternArray<HeaderRule>(rules || []);
|
|
1409
1509
|
}
|
|
@@ -1421,14 +1521,14 @@ function matchesHeaderRule(name: string, rules: HeaderRule[]): boolean {
|
|
|
1421
1521
|
|
|
1422
1522
|
function normalizeHeaderCaptureConfig(raw?: boolean | HeaderCaptureOptions): NormalizedHeaderCapture {
|
|
1423
1523
|
if (raw === false) {
|
|
1424
|
-
return { enabled: false, allowSensitive: false,
|
|
1524
|
+
return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
|
|
1425
1525
|
}
|
|
1426
1526
|
const opts: HeaderCaptureOptions = raw && raw !== true ? raw : {};
|
|
1427
1527
|
return {
|
|
1428
1528
|
enabled: true,
|
|
1429
1529
|
allowSensitive: opts.allowSensitiveHeaders === true,
|
|
1430
|
-
|
|
1431
|
-
|
|
1530
|
+
mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
|
|
1531
|
+
unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
|
|
1432
1532
|
};
|
|
1433
1533
|
}
|
|
1434
1534
|
|
|
@@ -1449,24 +1549,210 @@ function sanitizeHeaders(headers: any, rawCfg?: boolean | HeaderCaptureOptions)
|
|
|
1449
1549
|
const cfg = normalizeHeaderCaptureConfig(rawCfg);
|
|
1450
1550
|
if (!cfg.enabled) return {};
|
|
1451
1551
|
|
|
1452
|
-
const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
|
|
1453
1552
|
const out: Record<string, any> = {};
|
|
1454
1553
|
|
|
1455
1554
|
for (const [rawKey, rawVal] of Object.entries(headers || {})) {
|
|
1456
1555
|
const key = String(rawKey || '').toLowerCase();
|
|
1457
1556
|
if (!key) continue;
|
|
1458
|
-
const shouldDrop = matchesHeaderRule(key, dropList);
|
|
1459
|
-
const keep = matchesHeaderRule(key, cfg.keep);
|
|
1460
|
-
if (shouldDrop && !keep) continue;
|
|
1461
|
-
|
|
1462
1557
|
const sanitizedValue = sanitizeHeaderValue(rawVal);
|
|
1463
1558
|
if (sanitizedValue !== undefined) {
|
|
1464
1559
|
out[key] = sanitizedValue;
|
|
1465
1560
|
}
|
|
1466
1561
|
}
|
|
1562
|
+
|
|
1563
|
+
const maskList = cfg.allowSensitive ? cfg.mask : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.mask];
|
|
1564
|
+
Object.keys(out).forEach((key) => {
|
|
1565
|
+
if (!matchesHeaderRule(key, maskList)) return;
|
|
1566
|
+
if (matchesHeaderRule(key, cfg.unmask)) return;
|
|
1567
|
+
const current = out[key];
|
|
1568
|
+
if (Array.isArray(current)) {
|
|
1569
|
+
out[key] = current.map(() => DEFAULT_MASK_REPLACEMENT);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
out[key] = DEFAULT_MASK_REPLACEMENT;
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1467
1575
|
return out;
|
|
1468
1576
|
}
|
|
1469
1577
|
|
|
1578
|
+
// ===================================================================
|
|
1579
|
+
// Masking (request/response payloads + trace args/returns/errors)
|
|
1580
|
+
// ===================================================================
|
|
1581
|
+
type NormalizedMaskRule = {
|
|
1582
|
+
when?: ReproMaskWhen;
|
|
1583
|
+
targets: ReproMaskTarget[];
|
|
1584
|
+
paths: string[][];
|
|
1585
|
+
keys?: TraceRulePattern;
|
|
1586
|
+
replacement: any;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
type NormalizedMaskingConfig = {
|
|
1590
|
+
replacement: any;
|
|
1591
|
+
rules: NormalizedMaskRule[];
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
type MaskRequestContext = {
|
|
1595
|
+
method: string;
|
|
1596
|
+
path: string;
|
|
1597
|
+
key: string;
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
function normalizeMaskTargets(target: ReproMaskTarget | ReproMaskTarget[]): ReproMaskTarget[] {
|
|
1601
|
+
const out = Array.isArray(target) ? target : [target];
|
|
1602
|
+
return out.filter((t): t is ReproMaskTarget => typeof t === 'string' && t.length > 0);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function parseMaskPath(raw: string): string[] {
|
|
1606
|
+
if (!raw) return [];
|
|
1607
|
+
let path = String(raw).trim();
|
|
1608
|
+
if (!path) return [];
|
|
1609
|
+
if (path.startsWith('$.')) path = path.slice(2);
|
|
1610
|
+
if (path.startsWith('.')) path = path.slice(1);
|
|
1611
|
+
path = path.replace(/\[(\d+|\*)\]/g, '.$1');
|
|
1612
|
+
return path.split('.').map(s => s.trim()).filter(Boolean);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function normalizeMaskPaths(paths?: string | string[]): string[][] {
|
|
1616
|
+
if (!paths) return [];
|
|
1617
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
1618
|
+
return list
|
|
1619
|
+
.map(p => parseMaskPath(p))
|
|
1620
|
+
.filter(parts => parts.length > 0);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function normalizeMaskingConfig(raw?: ReproMaskingConfig): NormalizedMaskingConfig | null {
|
|
1624
|
+
const rules = raw?.rules;
|
|
1625
|
+
if (!Array.isArray(rules) || rules.length === 0) return null;
|
|
1626
|
+
const replacement = raw?.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1627
|
+
const normalized: NormalizedMaskRule[] = [];
|
|
1628
|
+
for (const rule of rules) {
|
|
1629
|
+
if (!rule) continue;
|
|
1630
|
+
const targets = normalizeMaskTargets(rule.target);
|
|
1631
|
+
if (!targets.length) continue;
|
|
1632
|
+
const paths = normalizeMaskPaths(rule.paths);
|
|
1633
|
+
const keys = rule.keys ?? undefined;
|
|
1634
|
+
if (!paths.length && !keys) continue;
|
|
1635
|
+
normalized.push({
|
|
1636
|
+
when: rule.when,
|
|
1637
|
+
targets,
|
|
1638
|
+
paths,
|
|
1639
|
+
keys,
|
|
1640
|
+
replacement: rule.replacement ?? replacement,
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
return normalized.length ? { replacement, rules: normalized } : null;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function maskWhenRequiresTrace(when: ReproMaskWhen): boolean {
|
|
1647
|
+
return Boolean(
|
|
1648
|
+
when.fn ||
|
|
1649
|
+
when.functionName ||
|
|
1650
|
+
when.wrapper ||
|
|
1651
|
+
when.wrapperClass ||
|
|
1652
|
+
when.className ||
|
|
1653
|
+
when.owner ||
|
|
1654
|
+
when.file ||
|
|
1655
|
+
when.lib ||
|
|
1656
|
+
when.library ||
|
|
1657
|
+
when.type ||
|
|
1658
|
+
when.functionType ||
|
|
1659
|
+
when.event ||
|
|
1660
|
+
when.eventType
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function matchesMaskWhen(when: ReproMaskWhen | undefined, req: MaskRequestContext, trace: TraceEventForFilter | null): boolean {
|
|
1665
|
+
if (!when) return true;
|
|
1666
|
+
if (!matchesPattern(req.method, when.method)) return false;
|
|
1667
|
+
if (!matchesPattern(req.path, when.path)) return false;
|
|
1668
|
+
if (!matchesPattern(req.key, when.key)) return false;
|
|
1669
|
+
|
|
1670
|
+
if (maskWhenRequiresTrace(when)) {
|
|
1671
|
+
if (!trace) return false;
|
|
1672
|
+
if (!matchesRule(when, trace)) return false;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function maskKeysInPlace(node: any, keys: TraceRulePattern, replacement: any) {
|
|
1679
|
+
if (!node) return;
|
|
1680
|
+
if (Array.isArray(node)) {
|
|
1681
|
+
node.forEach(item => maskKeysInPlace(item, keys, replacement));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (typeof node !== 'object') return;
|
|
1685
|
+
Object.keys(node).forEach((key) => {
|
|
1686
|
+
if (matchesPattern(key, keys, false)) {
|
|
1687
|
+
try { node[key] = replacement; } catch {}
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
maskKeysInPlace(node[key], keys, replacement);
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function maskPathInPlace(node: any, pathParts: string[], replacement: any, depth: number = 0) {
|
|
1695
|
+
if (!node) return;
|
|
1696
|
+
if (depth >= pathParts.length) return;
|
|
1697
|
+
const part = pathParts[depth];
|
|
1698
|
+
const isLast = depth === pathParts.length - 1;
|
|
1699
|
+
|
|
1700
|
+
const applyAt = (container: any, key: string | number) => {
|
|
1701
|
+
if (!container) return;
|
|
1702
|
+
try { container[key] = replacement; } catch {}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
if (part === '*') {
|
|
1706
|
+
if (Array.isArray(node)) {
|
|
1707
|
+
for (let i = 0; i < node.length; i++) {
|
|
1708
|
+
if (isLast) applyAt(node, i);
|
|
1709
|
+
else maskPathInPlace(node[i], pathParts, replacement, depth + 1);
|
|
1710
|
+
}
|
|
1711
|
+
} else if (typeof node === 'object') {
|
|
1712
|
+
for (const key of Object.keys(node)) {
|
|
1713
|
+
if (isLast) applyAt(node, key);
|
|
1714
|
+
else maskPathInPlace(node[key], pathParts, replacement, depth + 1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const index = Number(part);
|
|
1721
|
+
const isIndex = Number.isInteger(index) && String(index) === part;
|
|
1722
|
+
if (Array.isArray(node) && isIndex) {
|
|
1723
|
+
if (index < 0 || index >= node.length) return;
|
|
1724
|
+
if (isLast) applyAt(node, index);
|
|
1725
|
+
else maskPathInPlace(node[index], pathParts, replacement, depth + 1);
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (typeof node !== 'object') return;
|
|
1730
|
+
if (!Object.prototype.hasOwnProperty.call(node, part)) return;
|
|
1731
|
+
if (isLast) applyAt(node, part);
|
|
1732
|
+
else maskPathInPlace(node[part], pathParts, replacement, depth + 1);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function applyMasking(
|
|
1736
|
+
target: ReproMaskTarget,
|
|
1737
|
+
value: any,
|
|
1738
|
+
req: MaskRequestContext,
|
|
1739
|
+
trace: TraceEventForFilter | null,
|
|
1740
|
+
masking: NormalizedMaskingConfig | null
|
|
1741
|
+
) {
|
|
1742
|
+
if (!masking || !masking.rules.length) return value;
|
|
1743
|
+
if (value === undefined) return value;
|
|
1744
|
+
for (const rule of masking.rules) {
|
|
1745
|
+
if (!rule.targets.includes(target)) continue;
|
|
1746
|
+
if (!matchesMaskWhen(rule.when, req, trace)) continue;
|
|
1747
|
+
const replacement = rule.replacement ?? masking.replacement ?? DEFAULT_MASK_REPLACEMENT;
|
|
1748
|
+
if (rule.keys) maskKeysInPlace(value, rule.keys, replacement);
|
|
1749
|
+
if (rule.paths.length) {
|
|
1750
|
+
rule.paths.forEach(parts => maskPathInPlace(value, parts, replacement, 0));
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return value;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1470
1756
|
// ===================================================================
|
|
1471
1757
|
// reproMiddleware — unchanged behavior + passive per-request trace
|
|
1472
1758
|
// ===================================================================
|
|
@@ -1475,11 +1761,14 @@ export type ReproMiddlewareConfig = {
|
|
|
1475
1761
|
tenantId: string;
|
|
1476
1762
|
appSecret: string;
|
|
1477
1763
|
apiBase: string;
|
|
1478
|
-
/** Configure header capture/
|
|
1764
|
+
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
1479
1765
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
1766
|
+
/** Optional masking rules for request/response payloads and function traces. */
|
|
1767
|
+
masking?: ReproMaskingConfig;
|
|
1480
1768
|
};
|
|
1481
1769
|
|
|
1482
1770
|
export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
1771
|
+
const masking = normalizeMaskingConfig(cfg.masking);
|
|
1483
1772
|
return function (req: Request, res: Response, next: NextFunction) {
|
|
1484
1773
|
const sid = (req.headers['x-bug-session-id'] as string) || '';
|
|
1485
1774
|
const aid = (req.headers['x-bug-action-id'] as string) || '';
|
|
@@ -1492,8 +1781,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1492
1781
|
const rid = nextRequestId(requestEpochMs);
|
|
1493
1782
|
const t0 = requestStartRaw;
|
|
1494
1783
|
const url = (req as any).originalUrl || req.url || '/';
|
|
1784
|
+
const urlPathOnly = (url || '/').split('?')[0] || '/';
|
|
1495
1785
|
const path = url; // back-compat
|
|
1496
1786
|
const key = normalizeRouteKey(req.method, url);
|
|
1787
|
+
const maskReq: MaskRequestContext = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
1497
1788
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
1498
1789
|
beginSessionRequest(sid);
|
|
1499
1790
|
|
|
@@ -1525,7 +1816,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1525
1816
|
return fn();
|
|
1526
1817
|
};
|
|
1527
1818
|
|
|
1528
|
-
runInTrace(() => als.run({ sid, aid, clockSkewMs }, () => {
|
|
1819
|
+
runInTrace(() => als.run({ sid, aid, clockSkewMs, excludedSpanIds: new Set<string>() }, () => {
|
|
1529
1820
|
const events: TraceEventRecord[] = [];
|
|
1530
1821
|
let endpointTrace: EndpointTraceInfo | null = null;
|
|
1531
1822
|
let preferredAppTrace: EndpointTraceInfo | null = null;
|
|
@@ -1634,14 +1925,43 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1634
1925
|
evt.functionType = ev.functionType;
|
|
1635
1926
|
}
|
|
1636
1927
|
|
|
1928
|
+
const candidate: TraceEventForFilter = {
|
|
1929
|
+
type: evt.type,
|
|
1930
|
+
eventType: evt.type,
|
|
1931
|
+
functionType: evt.functionType ?? null,
|
|
1932
|
+
fn: evt.fn,
|
|
1933
|
+
wrapperClass: inferWrapperClassFromFn(evt.fn),
|
|
1934
|
+
file: evt.file ?? null,
|
|
1935
|
+
depth: evt.depth,
|
|
1936
|
+
library: inferLibraryNameFromFile(evt.file),
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1637
1939
|
if (ev.args !== undefined) {
|
|
1638
|
-
evt.args =
|
|
1940
|
+
evt.args = applyMasking(
|
|
1941
|
+
'trace.args',
|
|
1942
|
+
sanitizeTraceArgs(ev.args),
|
|
1943
|
+
maskReq,
|
|
1944
|
+
candidate,
|
|
1945
|
+
masking
|
|
1946
|
+
);
|
|
1639
1947
|
}
|
|
1640
1948
|
if (ev.returnValue !== undefined) {
|
|
1641
|
-
evt.returnValue =
|
|
1949
|
+
evt.returnValue = applyMasking(
|
|
1950
|
+
'trace.returnValue',
|
|
1951
|
+
sanitizeTraceValue(ev.returnValue),
|
|
1952
|
+
maskReq,
|
|
1953
|
+
candidate,
|
|
1954
|
+
masking
|
|
1955
|
+
);
|
|
1642
1956
|
}
|
|
1643
1957
|
if (ev.error !== undefined) {
|
|
1644
|
-
evt.error =
|
|
1958
|
+
evt.error = applyMasking(
|
|
1959
|
+
'trace.error',
|
|
1960
|
+
sanitizeTraceValue(ev.error),
|
|
1961
|
+
maskReq,
|
|
1962
|
+
candidate,
|
|
1963
|
+
masking
|
|
1964
|
+
);
|
|
1645
1965
|
}
|
|
1646
1966
|
if (ev.threw !== undefined) {
|
|
1647
1967
|
evt.threw = Boolean(ev.threw);
|
|
@@ -1650,16 +1970,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1650
1970
|
evt.unawaited = ev.unawaited === true;
|
|
1651
1971
|
}
|
|
1652
1972
|
|
|
1653
|
-
const
|
|
1654
|
-
type: evt.type,
|
|
1655
|
-
eventType: evt.type,
|
|
1656
|
-
functionType: evt.functionType ?? null,
|
|
1657
|
-
fn: evt.fn,
|
|
1658
|
-
file: evt.file ?? null,
|
|
1659
|
-
depth: evt.depth,
|
|
1660
|
-
library: inferLibraryNameFromFile(evt.file),
|
|
1661
|
-
};
|
|
1662
|
-
|
|
1973
|
+
const dropEvent = shouldDropTraceEvent(candidate);
|
|
1663
1974
|
const spanKey = normalizeSpanId(evt.spanId);
|
|
1664
1975
|
if (evt.type === 'enter') {
|
|
1665
1976
|
lastEventAt = Date.now();
|
|
@@ -1677,7 +1988,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1677
1988
|
}
|
|
1678
1989
|
}
|
|
1679
1990
|
|
|
1680
|
-
if (
|
|
1991
|
+
if (dropEvent) {
|
|
1992
|
+
if (evt.type === 'enter' && spanKey) {
|
|
1993
|
+
try { (getCtx() as Ctx).excludedSpanIds?.add(spanKey); } catch {}
|
|
1994
|
+
}
|
|
1681
1995
|
if (finished) {
|
|
1682
1996
|
scheduleIdleFlush();
|
|
1683
1997
|
}
|
|
@@ -1738,9 +2052,54 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1738
2052
|
?? firstAppTrace
|
|
1739
2053
|
?? { fn: null, file: null, line: null, functionType: null };
|
|
1740
2054
|
const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
2055
|
+
const endpointTraceCtx: TraceEventForFilter | null = (() => {
|
|
2056
|
+
if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
|
|
2057
|
+
return {
|
|
2058
|
+
type: 'enter',
|
|
2059
|
+
eventType: 'enter',
|
|
2060
|
+
fn: chosenEndpoint.fn ?? undefined,
|
|
2061
|
+
wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
|
|
2062
|
+
file: chosenEndpoint.file ?? null,
|
|
2063
|
+
functionType: chosenEndpoint.functionType ?? null,
|
|
2064
|
+
library: inferLibraryNameFromFile(chosenEndpoint.file),
|
|
2065
|
+
};
|
|
2066
|
+
})();
|
|
2067
|
+
|
|
2068
|
+
const requestBody = applyMasking(
|
|
2069
|
+
'request.body',
|
|
2070
|
+
sanitizeRequestSnapshot((req as any).body),
|
|
2071
|
+
maskReq,
|
|
2072
|
+
endpointTraceCtx,
|
|
2073
|
+
masking
|
|
2074
|
+
);
|
|
2075
|
+
const requestParams = applyMasking(
|
|
2076
|
+
'request.params',
|
|
2077
|
+
sanitizeRequestSnapshot((req as any).params),
|
|
2078
|
+
maskReq,
|
|
2079
|
+
endpointTraceCtx,
|
|
2080
|
+
masking
|
|
2081
|
+
);
|
|
2082
|
+
const requestQuery = applyMasking(
|
|
2083
|
+
'request.query',
|
|
2084
|
+
sanitizeRequestSnapshot((req as any).query),
|
|
2085
|
+
maskReq,
|
|
2086
|
+
endpointTraceCtx,
|
|
2087
|
+
masking
|
|
2088
|
+
);
|
|
2089
|
+
const maskedHeaders = applyMasking(
|
|
2090
|
+
'request.headers',
|
|
2091
|
+
requestHeaders,
|
|
2092
|
+
maskReq,
|
|
2093
|
+
endpointTraceCtx,
|
|
2094
|
+
masking
|
|
2095
|
+
);
|
|
2096
|
+
const responseBody = applyMasking(
|
|
2097
|
+
'response.body',
|
|
2098
|
+
capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody),
|
|
2099
|
+
maskReq,
|
|
2100
|
+
endpointTraceCtx,
|
|
2101
|
+
masking
|
|
2102
|
+
);
|
|
1744
2103
|
|
|
1745
2104
|
const requestPayload: Record<string, any> = {
|
|
1746
2105
|
rid,
|
|
@@ -1749,9 +2108,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
1749
2108
|
path,
|
|
1750
2109
|
status: res.statusCode,
|
|
1751
2110
|
durMs: Date.now() - t0,
|
|
1752
|
-
headers:
|
|
2111
|
+
headers: maskedHeaders,
|
|
1753
2112
|
key,
|
|
1754
|
-
respBody:
|
|
2113
|
+
respBody: responseBody,
|
|
1755
2114
|
trace: traceBatches.length ? undefined : '[]',
|
|
1756
2115
|
};
|
|
1757
2116
|
if (requestBody !== undefined) requestPayload.body = requestBody;
|
|
@@ -1865,6 +2224,7 @@ export function reproMongoosePlugin(cfg: { appId: string; tenantId: string; appS
|
|
|
1865
2224
|
const after = this.toObject({ depopulate: true });
|
|
1866
2225
|
const collection = meta.collection || resolveCollectionOrWarn(this, 'doc');
|
|
1867
2226
|
const spanContext = meta.spanContext || captureSpanContextFromTracer(this);
|
|
2227
|
+
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
1868
2228
|
|
|
1869
2229
|
const query = meta.wasNew
|
|
1870
2230
|
? { op: 'insertOne', doc: after }
|
|
@@ -1909,6 +2269,7 @@ export function reproMongoosePlugin(cfg: { appId: string; tenantId: string; appS
|
|
|
1909
2269
|
const after = res ?? null;
|
|
1910
2270
|
const collection = (this as any).__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
1911
2271
|
const spanContext = (this as any).__repro_spanContext || captureSpanContextFromTracer(this);
|
|
2272
|
+
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
1912
2273
|
const pk = after?._id ?? before?._id;
|
|
1913
2274
|
|
|
1914
2275
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, (getCtx() as Ctx).sid!, {
|
|
@@ -1946,6 +2307,7 @@ export function reproMongoosePlugin(cfg: { appId: string; tenantId: string; appS
|
|
|
1946
2307
|
const collection = (this as any).__repro_collection || resolveCollectionOrWarn(this, 'query');
|
|
1947
2308
|
const filter = (this as any).__repro_filter ?? { _id: before._id };
|
|
1948
2309
|
const spanContext = (this as any).__repro_spanContext || captureSpanContextFromTracer(this);
|
|
2310
|
+
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
1949
2311
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, (getCtx() as Ctx).sid!, {
|
|
1950
2312
|
entries: [{
|
|
1951
2313
|
actionId: (getCtx() as Ctx).aid!,
|
|
@@ -2261,6 +2623,8 @@ function dehydrateComplexValue(value: any) {
|
|
|
2261
2623
|
|
|
2262
2624
|
function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
|
|
2263
2625
|
if (!sid) return;
|
|
2626
|
+
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
2627
|
+
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
2264
2628
|
const dbEntry = attachSpanContext({
|
|
2265
2629
|
collection: payload.collection,
|
|
2266
2630
|
op: payload.op,
|
|
@@ -2269,7 +2633,7 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
|
|
|
2269
2633
|
durMs: payload.durMs ?? undefined,
|
|
2270
2634
|
pk: null, before: null, after: null,
|
|
2271
2635
|
error: payload.error ?? undefined,
|
|
2272
|
-
},
|
|
2636
|
+
}, spanContext);
|
|
2273
2637
|
post(cfg.apiBase, cfg.tenantId, cfg.appId, cfg.appSecret, sid, {
|
|
2274
2638
|
entries: [{
|
|
2275
2639
|
actionId: aid ?? null,
|
package/tracer/runtime.js
CHANGED
|
@@ -545,7 +545,13 @@ if (!global.__repro_call) {
|
|
|
545
545
|
const perCallStore = cloneStore(baseStoreSnapshot);
|
|
546
546
|
let result;
|
|
547
547
|
als.run(perCallStore, () => {
|
|
548
|
-
|
|
548
|
+
// Support both callbacks and constructors: some libraries (e.g., class-transformer)
|
|
549
|
+
// pass class constructors as args and invoke them with `new`.
|
|
550
|
+
if (new.target) {
|
|
551
|
+
result = Reflect.construct(arg, arguments, arg);
|
|
552
|
+
} else {
|
|
553
|
+
result = arg.apply(this, arguments);
|
|
554
|
+
}
|
|
549
555
|
});
|
|
550
556
|
return result;
|
|
551
557
|
};
|