repro-nest 0.0.214 → 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 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 redacts them. */
58
+ /** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
58
59
  allowSensitiveHeaders?: boolean;
59
- /** Additional header names (string or RegExp) to drop from captures. */
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
- /** Explicit allowlist that overrides defaults and drop rules. */
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/redaction. Defaults to capturing with sensitive headers removed. */
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;
@@ -1197,6 +1218,7 @@ const DEFAULT_SENSITIVE_HEADERS = [
1197
1218
  'cookie',
1198
1219
  'set-cookie',
1199
1220
  ];
1221
+ const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
1200
1222
  function normalizeHeaderRules(rules) {
1201
1223
  return normalizePatternArray(rules || []);
1202
1224
  }
@@ -1218,14 +1240,14 @@ function matchesHeaderRule(name, rules) {
1218
1240
  }
1219
1241
  function normalizeHeaderCaptureConfig(raw) {
1220
1242
  if (raw === false) {
1221
- return { enabled: false, allowSensitive: false, drop: [], keep: [] };
1243
+ return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
1222
1244
  }
1223
1245
  const opts = raw && raw !== true ? raw : {};
1224
1246
  return {
1225
1247
  enabled: true,
1226
1248
  allowSensitive: opts.allowSensitiveHeaders === true,
1227
- drop: normalizeHeaderRules(opts.dropHeaders),
1228
- keep: normalizeHeaderRules(opts.keepHeaders),
1249
+ mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
1250
+ unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
1229
1251
  };
1230
1252
  }
1231
1253
  function sanitizeHeaderValue(value) {
@@ -1248,24 +1270,209 @@ function sanitizeHeaders(headers, rawCfg) {
1248
1270
  const cfg = normalizeHeaderCaptureConfig(rawCfg);
1249
1271
  if (!cfg.enabled)
1250
1272
  return {};
1251
- const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
1252
1273
  const out = {};
1253
1274
  for (const [rawKey, rawVal] of Object.entries(headers || {})) {
1254
1275
  const key = String(rawKey || '').toLowerCase();
1255
1276
  if (!key)
1256
1277
  continue;
1257
- const shouldDrop = matchesHeaderRule(key, dropList);
1258
- const keep = matchesHeaderRule(key, cfg.keep);
1259
- if (shouldDrop && !keep)
1260
- continue;
1261
1278
  const sanitizedValue = sanitizeHeaderValue(rawVal);
1262
1279
  if (sanitizedValue !== undefined) {
1263
1280
  out[key] = sanitizedValue;
1264
1281
  }
1265
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
+ });
1266
1296
  return out;
1267
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
+ }
1268
1474
  function reproMiddleware(cfg) {
1475
+ const masking = normalizeMaskingConfig(cfg.masking);
1269
1476
  return function (req, res, next) {
1270
1477
  const sid = req.headers['x-bug-session-id'] || '';
1271
1478
  const aid = req.headers['x-bug-action-id'] || '';
@@ -1278,8 +1485,10 @@ function reproMiddleware(cfg) {
1278
1485
  const rid = nextRequestId(requestEpochMs);
1279
1486
  const t0 = requestStartRaw;
1280
1487
  const url = req.originalUrl || req.url || '/';
1488
+ const urlPathOnly = (url || '/').split('?')[0] || '/';
1281
1489
  const path = url; // back-compat
1282
1490
  const key = normalizeRouteKey(req.method, url);
1491
+ const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
1283
1492
  const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
1284
1493
  beginSessionRequest(sid);
1285
1494
  // ---- response body capture (unchanged) ----
@@ -1439,14 +1648,24 @@ function reproMiddleware(cfg) {
1439
1648
  if (ev.functionType !== undefined) {
1440
1649
  evt.functionType = ev.functionType;
1441
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
+ };
1442
1661
  if (ev.args !== undefined) {
1443
- evt.args = sanitizeTraceArgs(ev.args);
1662
+ evt.args = applyMasking('trace.args', sanitizeTraceArgs(ev.args), maskReq, candidate, masking);
1444
1663
  }
1445
1664
  if (ev.returnValue !== undefined) {
1446
- evt.returnValue = sanitizeTraceValue(ev.returnValue);
1665
+ evt.returnValue = applyMasking('trace.returnValue', sanitizeTraceValue(ev.returnValue), maskReq, candidate, masking);
1447
1666
  }
1448
1667
  if (ev.error !== undefined) {
1449
- evt.error = sanitizeTraceValue(ev.error);
1668
+ evt.error = applyMasking('trace.error', sanitizeTraceValue(ev.error), maskReq, candidate, masking);
1450
1669
  }
1451
1670
  if (ev.threw !== undefined) {
1452
1671
  evt.threw = Boolean(ev.threw);
@@ -1454,15 +1673,6 @@ function reproMiddleware(cfg) {
1454
1673
  if (ev.unawaited !== undefined) {
1455
1674
  evt.unawaited = ev.unawaited === true;
1456
1675
  }
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
1676
  const dropEvent = shouldDropTraceEvent(candidate);
1467
1677
  const spanKey = normalizeSpanId(evt.spanId);
1468
1678
  if (evt.type === 'enter') {
@@ -1546,9 +1756,24 @@ function reproMiddleware(cfg) {
1546
1756
  ?? firstAppTrace
1547
1757
  ?? { fn: null, file: null, line: null, functionType: null };
1548
1758
  const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
1549
- const requestBody = sanitizeRequestSnapshot(req.body);
1550
- const requestParams = sanitizeRequestSnapshot(req.params);
1551
- const requestQuery = sanitizeRequestSnapshot(req.query);
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);
1552
1777
  const requestPayload = {
1553
1778
  rid,
1554
1779
  method: req.method,
@@ -1556,9 +1781,9 @@ function reproMiddleware(cfg) {
1556
1781
  path,
1557
1782
  status: res.statusCode,
1558
1783
  durMs: Date.now() - t0,
1559
- headers: requestHeaders,
1784
+ headers: maskedHeaders,
1560
1785
  key,
1561
- respBody: capturedBody,
1786
+ respBody: responseBody,
1562
1787
  trace: traceBatches.length ? undefined : '[]',
1563
1788
  };
1564
1789
  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 '@repro/sdk';
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 '@repro/sdk';
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 '@repro/sdk';
150
+ import { enableReproTraceLogs, disableReproTraceLogs } from 'repro-nest';
85
151
 
86
152
  enableReproTraceLogs();
87
153
  // ...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repro-nest",
3
- "version": "0.0.214",
3
+ "version": "0.0.215",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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 redacts them. */
271
+ /** When true, sensitive headers such as Authorization are kept unmasked; default masks them. */
271
272
  allowSensitiveHeaders?: boolean;
272
- /** Additional header names (string or RegExp) to drop from captures. */
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
- /** Explicit allowlist that overrides defaults and drop rules. */
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
- drop: HeaderRule[];
282
- keep: HeaderRule[];
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;
@@ -1421,6 +1502,8 @@ const DEFAULT_SENSITIVE_HEADERS: Array<string | RegExp> = [
1421
1502
  'set-cookie',
1422
1503
  ];
1423
1504
 
1505
+ const DEFAULT_MASK_REPLACEMENT = '[REDACTED]';
1506
+
1424
1507
  function normalizeHeaderRules(rules?: HeaderRule | HeaderRule[] | null): HeaderRule[] {
1425
1508
  return normalizePatternArray<HeaderRule>(rules || []);
1426
1509
  }
@@ -1438,14 +1521,14 @@ function matchesHeaderRule(name: string, rules: HeaderRule[]): boolean {
1438
1521
 
1439
1522
  function normalizeHeaderCaptureConfig(raw?: boolean | HeaderCaptureOptions): NormalizedHeaderCapture {
1440
1523
  if (raw === false) {
1441
- return { enabled: false, allowSensitive: false, drop: [], keep: [] };
1524
+ return { enabled: false, allowSensitive: false, mask: [], unmask: [] };
1442
1525
  }
1443
1526
  const opts: HeaderCaptureOptions = raw && raw !== true ? raw : {};
1444
1527
  return {
1445
1528
  enabled: true,
1446
1529
  allowSensitive: opts.allowSensitiveHeaders === true,
1447
- drop: normalizeHeaderRules(opts.dropHeaders),
1448
- keep: normalizeHeaderRules(opts.keepHeaders),
1530
+ mask: [...normalizeHeaderRules(opts.maskHeaders), ...normalizeHeaderRules(opts.dropHeaders)],
1531
+ unmask: [...normalizeHeaderRules(opts.unmaskHeaders), ...normalizeHeaderRules(opts.keepHeaders)],
1449
1532
  };
1450
1533
  }
1451
1534
 
@@ -1466,24 +1549,210 @@ function sanitizeHeaders(headers: any, rawCfg?: boolean | HeaderCaptureOptions)
1466
1549
  const cfg = normalizeHeaderCaptureConfig(rawCfg);
1467
1550
  if (!cfg.enabled) return {};
1468
1551
 
1469
- const dropList = cfg.allowSensitive ? cfg.drop : [...DEFAULT_SENSITIVE_HEADERS, ...cfg.drop];
1470
1552
  const out: Record<string, any> = {};
1471
1553
 
1472
1554
  for (const [rawKey, rawVal] of Object.entries(headers || {})) {
1473
1555
  const key = String(rawKey || '').toLowerCase();
1474
1556
  if (!key) continue;
1475
- const shouldDrop = matchesHeaderRule(key, dropList);
1476
- const keep = matchesHeaderRule(key, cfg.keep);
1477
- if (shouldDrop && !keep) continue;
1478
-
1479
1557
  const sanitizedValue = sanitizeHeaderValue(rawVal);
1480
1558
  if (sanitizedValue !== undefined) {
1481
1559
  out[key] = sanitizedValue;
1482
1560
  }
1483
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
+
1484
1575
  return out;
1485
1576
  }
1486
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
+
1487
1756
  // ===================================================================
1488
1757
  // reproMiddleware — unchanged behavior + passive per-request trace
1489
1758
  // ===================================================================
@@ -1492,11 +1761,14 @@ export type ReproMiddlewareConfig = {
1492
1761
  tenantId: string;
1493
1762
  appSecret: string;
1494
1763
  apiBase: string;
1495
- /** Configure header capture/redaction. Defaults to capturing with sensitive headers removed. */
1764
+ /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
1496
1765
  captureHeaders?: boolean | HeaderCaptureOptions;
1766
+ /** Optional masking rules for request/response payloads and function traces. */
1767
+ masking?: ReproMaskingConfig;
1497
1768
  };
1498
1769
 
1499
1770
  export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1771
+ const masking = normalizeMaskingConfig(cfg.masking);
1500
1772
  return function (req: Request, res: Response, next: NextFunction) {
1501
1773
  const sid = (req.headers['x-bug-session-id'] as string) || '';
1502
1774
  const aid = (req.headers['x-bug-action-id'] as string) || '';
@@ -1509,8 +1781,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1509
1781
  const rid = nextRequestId(requestEpochMs);
1510
1782
  const t0 = requestStartRaw;
1511
1783
  const url = (req as any).originalUrl || req.url || '/';
1784
+ const urlPathOnly = (url || '/').split('?')[0] || '/';
1512
1785
  const path = url; // back-compat
1513
1786
  const key = normalizeRouteKey(req.method, url);
1787
+ const maskReq: MaskRequestContext = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
1514
1788
  const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
1515
1789
  beginSessionRequest(sid);
1516
1790
 
@@ -1651,14 +1925,43 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1651
1925
  evt.functionType = ev.functionType;
1652
1926
  }
1653
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
+
1654
1939
  if (ev.args !== undefined) {
1655
- evt.args = sanitizeTraceArgs(ev.args);
1940
+ evt.args = applyMasking(
1941
+ 'trace.args',
1942
+ sanitizeTraceArgs(ev.args),
1943
+ maskReq,
1944
+ candidate,
1945
+ masking
1946
+ );
1656
1947
  }
1657
1948
  if (ev.returnValue !== undefined) {
1658
- evt.returnValue = sanitizeTraceValue(ev.returnValue);
1949
+ evt.returnValue = applyMasking(
1950
+ 'trace.returnValue',
1951
+ sanitizeTraceValue(ev.returnValue),
1952
+ maskReq,
1953
+ candidate,
1954
+ masking
1955
+ );
1659
1956
  }
1660
1957
  if (ev.error !== undefined) {
1661
- evt.error = sanitizeTraceValue(ev.error);
1958
+ evt.error = applyMasking(
1959
+ 'trace.error',
1960
+ sanitizeTraceValue(ev.error),
1961
+ maskReq,
1962
+ candidate,
1963
+ masking
1964
+ );
1662
1965
  }
1663
1966
  if (ev.threw !== undefined) {
1664
1967
  evt.threw = Boolean(ev.threw);
@@ -1667,16 +1970,6 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1667
1970
  evt.unawaited = ev.unawaited === true;
1668
1971
  }
1669
1972
 
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
1973
  const dropEvent = shouldDropTraceEvent(candidate);
1681
1974
  const spanKey = normalizeSpanId(evt.spanId);
1682
1975
  if (evt.type === 'enter') {
@@ -1759,9 +2052,54 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1759
2052
  ?? firstAppTrace
1760
2053
  ?? { fn: null, file: null, line: null, functionType: null };
1761
2054
  const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
1762
- const requestBody = sanitizeRequestSnapshot((req as any).body);
1763
- const requestParams = sanitizeRequestSnapshot((req as any).params);
1764
- const requestQuery = sanitizeRequestSnapshot((req as any).query);
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
+ );
1765
2103
 
1766
2104
  const requestPayload: Record<string, any> = {
1767
2105
  rid,
@@ -1770,9 +2108,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
1770
2108
  path,
1771
2109
  status: res.statusCode,
1772
2110
  durMs: Date.now() - t0,
1773
- headers: requestHeaders,
2111
+ headers: maskedHeaders,
1774
2112
  key,
1775
- respBody: capturedBody,
2113
+ respBody: responseBody,
1776
2114
  trace: traceBatches.length ? undefined : '[]',
1777
2115
  };
1778
2116
  if (requestBody !== undefined) requestPayload.body = requestBody;