@reproapp/node-sdk 0.0.5 → 0.0.7

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/src/index.ts CHANGED
@@ -355,9 +355,6 @@ type TraceEventRecord = {
355
355
  threw?: boolean;
356
356
  error?: any;
357
357
  unawaited?: boolean;
358
- argsValueCapture?: TraceValueCaptureRef;
359
- returnValueCapture?: TraceValueCaptureRef;
360
- errorValueCapture?: TraceValueCaptureRef;
361
358
  };
362
359
 
363
360
  type PendingTraceEventRecord = TraceEventRecord & {
@@ -390,10 +387,7 @@ type TraceValueBatchEntry = {
390
387
  | 'request.body'
391
388
  | 'request.params'
392
389
  | 'request.query'
393
- | 'response.body'
394
- | 'trace.args'
395
- | 'trace.returnValue'
396
- | 'trace.error';
390
+ | 'response.body';
397
391
  value: any;
398
392
  truncatedForStorage?: boolean;
399
393
  projectedKind?: string;
@@ -1094,6 +1088,10 @@ function traceEventCandidateFromRecord(event: PendingTraceEventRecord): TraceEve
1094
1088
  };
1095
1089
  }
1096
1090
 
1091
+ function shouldOmitJsonBuiltinTraceValues(event: TraceEventForFilter | { fn?: any }): boolean {
1092
+ return event.fn === 'JSON.parse' || event.fn === 'JSON.stringify';
1093
+ }
1094
+
1097
1095
  function preparePendingTraceEventsForFlush(events: PendingTraceEventRecord[]): PendingTraceEventRecord[] {
1098
1096
  const prepared: PendingTraceEventRecord[] = [];
1099
1097
  const excludedSpanIds = new Set<string>();
@@ -1116,6 +1114,13 @@ function preparePendingTraceEventsForFlush(events: PendingTraceEventRecord[]): P
1116
1114
 
1117
1115
  if (event.__reproPending) {
1118
1116
  event.__reproPending.candidate = candidate;
1117
+ if (shouldOmitJsonBuiltinTraceValues(candidate)) {
1118
+ delete event.__reproPending.argsRaw;
1119
+ delete event.__reproPending.returnValueRaw;
1120
+ if (event.__reproPending.errorRaw === undefined) {
1121
+ delete event.__reproPending;
1122
+ }
1123
+ }
1119
1124
  }
1120
1125
  delete event.__reproSourceFile;
1121
1126
  prepared.push(event);
@@ -1180,6 +1185,12 @@ const SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = (() => {
1180
1185
  return 1000;
1181
1186
  })();
1182
1187
 
1188
+ const SDK_BACKGROUND_MAX_DEFER_MS = (() => {
1189
+ const env = Number(process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS);
1190
+ if (Number.isFinite(env) && env >= 0) return Math.trunc(env);
1191
+ return 2000;
1192
+ })();
1193
+
1183
1194
  const SDK_BACKGROUND_WORK_TIMEOUT_MS = (() => {
1184
1195
  const env = Number(process.env.REPRO_SDK_BACKGROUND_WORK_TIMEOUT_MS);
1185
1196
  if (Number.isFinite(env) && env >= 0) return Math.trunc(env);
@@ -1217,6 +1228,7 @@ const TRACE_MATERIALIZE_WORKER_IDLE_SHUTDOWN_MS = (() => {
1217
1228
  })();
1218
1229
 
1219
1230
  let activeClientRequestCount = 0;
1231
+ let oldestActiveClientRequestAt: number | null = null;
1220
1232
  let lastClientActivityAt = 0;
1221
1233
  let sdkBackgroundQuietUntil = 0;
1222
1234
  const sdkBackgroundQueue: Array<() => Promise<void> | void> = [];
@@ -1243,6 +1255,11 @@ function noteClientActivity(now = Date.now()): void {
1243
1255
 
1244
1256
  function getSdkBackgroundDelayMs(now = Date.now()): number {
1245
1257
  if (hasActiveClientRequests()) {
1258
+ if (oldestActiveClientRequestAt !== null && SDK_BACKGROUND_MAX_DEFER_MS >= 0) {
1259
+ const activeForMs = now - oldestActiveClientRequestAt;
1260
+ if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS) return 0;
1261
+ return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, Math.max(0, SDK_BACKGROUND_MAX_DEFER_MS - activeForMs));
1262
+ }
1246
1263
  return SDK_BACKGROUND_IDLE_DELAY_MS;
1247
1264
  }
1248
1265
  const sinceLastActivity = now - lastClientActivityAt;
@@ -1433,9 +1450,18 @@ function shouldOffloadPendingTraceEvents(events: PendingTraceEventRecord[]): boo
1433
1450
  for (const event of events) {
1434
1451
  const pending = event.__reproPending;
1435
1452
  if (!pending) continue;
1436
- if (pending.argsRaw !== undefined) pendingValueCount += 1;
1437
- if (pending.returnValueRaw !== undefined) pendingValueCount += 1;
1438
- if (pending.errorRaw !== undefined) pendingValueCount += 1;
1453
+ if (pending.argsRaw !== undefined) {
1454
+ if (traceInlineValueExceedsLimit(pending.argsRaw)) return false;
1455
+ pendingValueCount += 1;
1456
+ }
1457
+ if (pending.returnValueRaw !== undefined) {
1458
+ if (traceInlineValueExceedsLimit(pending.returnValueRaw)) return false;
1459
+ pendingValueCount += 1;
1460
+ }
1461
+ if (pending.errorRaw !== undefined) {
1462
+ if (traceInlineValueExceedsLimit(pending.errorRaw)) return false;
1463
+ pendingValueCount += 1;
1464
+ }
1439
1465
  if (pendingValueCount >= TRACE_MATERIALIZE_OFFTHREAD_MIN_VALUES) {
1440
1466
  return true;
1441
1467
  }
@@ -1449,6 +1475,9 @@ function beginClientRequest(): void {
1449
1475
  sdkBackgroundQuietUntil = Math.max(sdkBackgroundQuietUntil, now + SDK_BACKGROUND_BUSY_IDLE_DELAY_MS);
1450
1476
  }
1451
1477
  noteClientActivity(now);
1478
+ if (activeClientRequestCount === 0) {
1479
+ oldestActiveClientRequestAt = now;
1480
+ }
1452
1481
  activeClientRequestCount += 1;
1453
1482
  if (sdkBackgroundTimer) {
1454
1483
  try { clearTimeout(sdkBackgroundTimer); } catch {}
@@ -1460,6 +1489,7 @@ function endClientRequest(): void {
1460
1489
  activeClientRequestCount = Math.max(0, activeClientRequestCount - 1);
1461
1490
  noteClientActivity();
1462
1491
  if (!hasActiveClientRequests()) {
1492
+ oldestActiveClientRequestAt = null;
1463
1493
  kickIngestQueueDrain();
1464
1494
  if (sdkBackgroundTimer) {
1465
1495
  try { clearTimeout(sdkBackgroundTimer); } catch {}
@@ -1987,25 +2017,16 @@ const TRACE_VALUE_MAX_DEPTH = 3;
1987
2017
  const TRACE_VALUE_MAX_KEYS = 20;
1988
2018
  const TRACE_VALUE_MAX_ITEMS = 20;
1989
2019
  const TRACE_VALUE_MAX_STRING = 2000;
1990
- const TRACE_FULL_VALUE_MAX_DEPTH = 8;
1991
- const TRACE_FULL_VALUE_MAX_KEYS = 200;
1992
- const TRACE_FULL_VALUE_MAX_ITEMS = 200;
1993
- const TRACE_FULL_VALUE_MAX_STRING = 32000;
1994
- const TRACE_FULL_VALUE_FALLBACK_DEPTH = 6;
1995
- const TRACE_FULL_VALUE_FALLBACK_KEYS = 100;
1996
- const TRACE_FULL_VALUE_FALLBACK_ITEMS = 100;
1997
- const TRACE_FULL_VALUE_FALLBACK_STRING = 8000;
1998
- const TRACE_FULL_VALUE_MAX_SERIALIZED_CHARS = 128 * 1024;
1999
- const INLINE_PRIVACY_MAX_SERIALIZED_CHARS = (() => {
2000
- const env = Number(process.env.REPRO_SDK_INLINE_PRIVACY_MAX_SERIALIZED_CHARS);
2020
+ const TRACE_FULL_VALUE_MAX_DEPTH = Number.MAX_SAFE_INTEGER;
2021
+ const TRACE_FULL_VALUE_MAX_KEYS = Number.MAX_SAFE_INTEGER;
2022
+ const TRACE_FULL_VALUE_MAX_ITEMS = Number.MAX_SAFE_INTEGER;
2023
+ const TRACE_FULL_VALUE_MAX_STRING = Number.MAX_SAFE_INTEGER;
2024
+ const TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = (() => {
2025
+ const env = Number(process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS);
2001
2026
  if (Number.isFinite(env) && env > 0) return Math.trunc(env);
2002
2027
  return 8 * 1024;
2003
2028
  })();
2004
- const INLINE_PRIVACY_PREVIEW_MAX_DEPTH = 3;
2005
- const INLINE_PRIVACY_PREVIEW_MAX_KEYS = 12;
2006
- const INLINE_PRIVACY_PREVIEW_MAX_ITEMS = 12;
2007
- const INLINE_PRIVACY_PREVIEW_MAX_STRING = 512;
2008
- const INLINE_PRIVACY_PREVIEW_JSON_PARSE_MAX_CHARS = 128 * 1024;
2029
+ const TRACE_VALUE_SIZE_EXCEEDED = Symbol('trace-value-size-exceeded');
2009
2030
  const TRACE_BATCH_SIZE = 100;
2010
2031
  const TRACE_FLUSH_DELAY_MS = 20;
2011
2032
  // Choose how to order trace events in payloads.
@@ -2208,6 +2229,7 @@ function isMongooseQueryLike(value: any): boolean {
2208
2229
 
2209
2230
  type TraceSanitizeOptions = {
2210
2231
  preserveLongStrings?: boolean;
2232
+ disableTruncation?: boolean;
2211
2233
  };
2212
2234
 
2213
2235
  function circularReference(path?: string): Record<string, string> {
@@ -2352,7 +2374,7 @@ function sanitizeTraceValue(
2352
2374
  if (existingPath) return circularReference(existingPath);
2353
2375
  seen.set(value, valuePath);
2354
2376
 
2355
- if (depth >= TRACE_VALUE_MAX_DEPTH) {
2377
+ if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
2356
2378
  const shallow = safeJson(value);
2357
2379
  if (shallow !== undefined) {
2358
2380
  return shallow;
@@ -2364,9 +2386,10 @@ function sanitizeTraceValue(
2364
2386
  }
2365
2387
 
2366
2388
  if (Array.isArray(value)) {
2367
- const out = value.slice(0, TRACE_VALUE_MAX_ITEMS)
2389
+ const sourceItems = options.disableTruncation ? value : value.slice(0, TRACE_VALUE_MAX_ITEMS);
2390
+ const out = sourceItems
2368
2391
  .map((item, index) => sanitizeTraceValue(item, depth + 1, seen, options, childCapturePath(valuePath, index)));
2369
- if (value.length > TRACE_VALUE_MAX_ITEMS) {
2392
+ if (!options.disableTruncation && value.length > TRACE_VALUE_MAX_ITEMS) {
2370
2393
  out.push(`…(${value.length - TRACE_VALUE_MAX_ITEMS} more items)`);
2371
2394
  }
2372
2395
  return out;
@@ -2376,12 +2399,12 @@ function sanitizeTraceValue(
2376
2399
  const out: Record<string, any> = {};
2377
2400
  let index = 0;
2378
2401
  for (const [k, v] of value) {
2379
- if (index >= TRACE_VALUE_MAX_ITEMS) break;
2402
+ if (!options.disableTruncation && index >= TRACE_VALUE_MAX_ITEMS) break;
2380
2403
  const key = normalizeCollectionKeyForCapture(k, index);
2381
2404
  out[key] = sanitizeTraceValue(v, depth + 1, seen, options, childCapturePath(valuePath, key));
2382
2405
  index += 1;
2383
2406
  }
2384
- if (value.size > TRACE_VALUE_MAX_ITEMS) {
2407
+ if (!options.disableTruncation && value.size > TRACE_VALUE_MAX_ITEMS) {
2385
2408
  out.__truncatedEntries = value.size - TRACE_VALUE_MAX_ITEMS;
2386
2409
  }
2387
2410
  return out;
@@ -2390,10 +2413,10 @@ function sanitizeTraceValue(
2390
2413
  if (value instanceof Set) {
2391
2414
  const arr: any[] = [];
2392
2415
  for (const item of value) {
2393
- if (arr.length >= TRACE_VALUE_MAX_ITEMS) break;
2416
+ if (!options.disableTruncation && arr.length >= TRACE_VALUE_MAX_ITEMS) break;
2394
2417
  arr.push(sanitizeTraceValue(item, depth + 1, seen, options, childCapturePath(valuePath, arr.length)));
2395
2418
  }
2396
- if (value.size > TRACE_VALUE_MAX_ITEMS) {
2419
+ if (!options.disableTruncation && value.size > TRACE_VALUE_MAX_ITEMS) {
2397
2420
  arr.push(`…(${value.size - TRACE_VALUE_MAX_ITEMS} more items)`);
2398
2421
  }
2399
2422
  return arr;
@@ -2402,14 +2425,15 @@ function sanitizeTraceValue(
2402
2425
  const ctor = value?.constructor?.name;
2403
2426
  const result: Record<string, any> = {};
2404
2427
  const keys = Object.keys(value);
2405
- for (const key of keys.slice(0, TRACE_VALUE_MAX_KEYS)) {
2428
+ const visibleKeys = options.disableTruncation ? keys : keys.slice(0, TRACE_VALUE_MAX_KEYS);
2429
+ for (const key of visibleKeys) {
2406
2430
  try {
2407
2431
  result[key] = sanitizeTraceValue((value as any)[key], depth + 1, seen, options, childCapturePath(valuePath, key));
2408
2432
  } catch (err) {
2409
2433
  result[key] = `[Cannot serialize: ${(err as Error)?.message || 'unknown error'}]`;
2410
2434
  }
2411
2435
  }
2412
- if (keys.length > TRACE_VALUE_MAX_KEYS) {
2436
+ if (!options.disableTruncation && keys.length > TRACE_VALUE_MAX_KEYS) {
2413
2437
  result.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
2414
2438
  }
2415
2439
  if (ctor && ctor !== 'Object') {
@@ -2424,7 +2448,11 @@ function sanitizeTraceArgs(values: any): any {
2424
2448
  }
2425
2449
 
2426
2450
  function sanitizeTraceValueForPrivacy(value: any): any {
2427
- return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true });
2451
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2452
+ }
2453
+
2454
+ function sanitizeMaterializedTraceValue(value: any): any {
2455
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2428
2456
  }
2429
2457
 
2430
2458
  function sanitizeTraceArgsForPrivacy(values: any): any {
@@ -2437,6 +2465,10 @@ function normalizeRawTraceArgs(values: any): any[] {
2437
2465
  return [values];
2438
2466
  }
2439
2467
 
2468
+ function hasMeaningfulRawTraceError(error: any): boolean {
2469
+ return error !== undefined && error !== null;
2470
+ }
2471
+
2440
2472
  async function materializePendingTraceEvents(
2441
2473
  events: PendingTraceEventRecord[],
2442
2474
  params: {
@@ -2452,96 +2484,41 @@ async function materializePendingTraceEvents(
2452
2484
  delete evt.__reproPending;
2453
2485
 
2454
2486
  const candidate = pending.candidate ?? traceEventCandidateFromRecord(evt);
2487
+ const omitJsonBuiltinValues = shouldOmitJsonBuiltinTraceValues(candidate);
2455
2488
  const activePrivacy = params.resolvePrivacy();
2456
- const capture: FullValueCaptureContext = {
2457
- runtimeConfig: params.cfg,
2458
- captureHeaders: params.cfg.captureHeaders,
2459
- maskReq: params.maskReq,
2460
- trace: candidate,
2461
- masking: params.masking,
2462
- privacy: activePrivacy,
2463
- };
2464
2489
 
2465
- if (pending.argsRaw !== undefined) {
2466
- const argsPreview = sanitizeTraceArgsForPrivacy(pending.argsRaw);
2467
- const argsMaterialization = await materializeInlinePrivacyValueAsync(
2490
+ if (!omitJsonBuiltinValues && pending.argsRaw !== undefined) {
2491
+ evt.args = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2468
2492
  'trace.args',
2469
- argsPreview,
2493
+ sanitizeTraceArgsForPrivacy(pending.argsRaw),
2470
2494
  params.cfg,
2471
2495
  params.maskReq,
2472
2496
  candidate,
2473
2497
  params.masking,
2474
2498
  activePrivacy,
2475
- );
2476
- if (argsMaterialization.skipped) {
2477
- (evt as any).argsMaterialization = argsMaterialization.skipped;
2478
- if (argsMaterialization.value !== undefined) {
2479
- evt.args = sanitizeTraceValue(argsMaterialization.value);
2480
- }
2481
- } else {
2482
- evt.args = sanitizeTraceValue(argsMaterialization.value);
2483
- evt.argsValueCapture = await maybeCaptureFullTraceValueAsync({
2484
- target: 'trace.args',
2485
- rawValue: pending.argsRaw,
2486
- previewValue: evt.args,
2487
- event: evt,
2488
- capture,
2489
- });
2490
- }
2499
+ ));
2491
2500
  }
2492
- if (pending.returnValueRaw !== undefined) {
2493
- const returnValuePreview = sanitizeTraceValueForPrivacy(pending.returnValueRaw);
2494
- const returnValueMaterialization = await materializeInlinePrivacyValueAsync(
2501
+ if (!omitJsonBuiltinValues && pending.returnValueRaw !== undefined) {
2502
+ evt.returnValue = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2495
2503
  'trace.returnValue',
2496
- returnValuePreview,
2504
+ sanitizeTraceValueForPrivacy(pending.returnValueRaw),
2497
2505
  params.cfg,
2498
2506
  params.maskReq,
2499
2507
  candidate,
2500
2508
  params.masking,
2501
2509
  activePrivacy,
2502
- );
2503
- if (returnValueMaterialization.skipped) {
2504
- (evt as any).returnValueMaterialization = returnValueMaterialization.skipped;
2505
- if (returnValueMaterialization.value !== undefined) {
2506
- evt.returnValue = sanitizeTraceValue(returnValueMaterialization.value);
2507
- }
2508
- } else {
2509
- evt.returnValue = sanitizeTraceValue(returnValueMaterialization.value);
2510
- evt.returnValueCapture = await maybeCaptureFullTraceValueAsync({
2511
- target: 'trace.returnValue',
2512
- rawValue: pending.returnValueRaw,
2513
- previewValue: evt.returnValue,
2514
- event: evt,
2515
- capture,
2516
- });
2517
- }
2510
+ ));
2518
2511
  }
2519
2512
  if (pending.errorRaw !== undefined) {
2520
- const errorPreview = sanitizeTraceValueForPrivacy(pending.errorRaw);
2521
- const errorMaterialization = await materializeInlinePrivacyValueAsync(
2513
+ evt.error = sanitizeMaterializedTraceValue(await materializeTracePrivacyValueAsync(
2522
2514
  'trace.error',
2523
- errorPreview,
2515
+ sanitizeTraceValueForPrivacy(pending.errorRaw),
2524
2516
  params.cfg,
2525
2517
  params.maskReq,
2526
2518
  candidate,
2527
2519
  params.masking,
2528
2520
  activePrivacy,
2529
- );
2530
- if (errorMaterialization.skipped) {
2531
- (evt as any).errorMaterialization = errorMaterialization.skipped;
2532
- if (errorMaterialization.value !== undefined) {
2533
- evt.error = sanitizeTraceValue(errorMaterialization.value);
2534
- }
2535
- } else {
2536
- evt.error = sanitizeTraceValue(errorMaterialization.value);
2537
- evt.errorValueCapture = await maybeCaptureFullTraceValueAsync({
2538
- target: 'trace.error',
2539
- rawValue: pending.errorRaw,
2540
- previewValue: evt.error,
2541
- event: evt,
2542
- capture,
2543
- });
2544
- }
2521
+ ));
2545
2522
  }
2546
2523
  }
2547
2524
  return events;
@@ -2554,10 +2531,6 @@ function stripPendingTraceValues(
2554
2531
  for (const evt of events) {
2555
2532
  if (!evt.__reproPending) continue;
2556
2533
  delete evt.__reproPending;
2557
- (evt as any).valueMaterialization = {
2558
- status: 'skipped',
2559
- reason,
2560
- };
2561
2534
  }
2562
2535
  return events;
2563
2536
  }
@@ -2646,7 +2619,7 @@ export async function __materializePendingTraceEventsForWorker(
2646
2619
  function sanitizeRequestSnapshot(value: any) {
2647
2620
  if (value === undefined) return undefined;
2648
2621
  try {
2649
- return sanitizeTraceValue(value);
2622
+ return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
2650
2623
  } catch {
2651
2624
  if (value === null) return null;
2652
2625
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
@@ -2860,6 +2833,22 @@ function shouldCapturePrivacyRaw(cfg: ReproMiddlewareConfig): boolean {
2860
2833
  return cfg.privacy?.captureRaw === true;
2861
2834
  }
2862
2835
 
2836
+ function normalizeValueForRuntimePrivacy(value: any): any {
2837
+ if (value === undefined || value === null) {
2838
+ return value;
2839
+ }
2840
+
2841
+ try {
2842
+ return sanitizeTraceValue(value, 0, new WeakMap(), {
2843
+ preserveLongStrings: true,
2844
+ disableTruncation: true,
2845
+ });
2846
+ } catch {
2847
+ const fallback = safeJson(value);
2848
+ return fallback === undefined ? value : fallback;
2849
+ }
2850
+ }
2851
+
2863
2852
  function resolveRuntimePrivacyStartupMaxWaitMs(cfg?: RuntimePrivacyConfig | null): number {
2864
2853
  const value = Number(cfg?.startupMaxWaitMs);
2865
2854
  if (!Number.isFinite(value)) {
@@ -3092,7 +3081,8 @@ function applyPrivacyThenMask(
3092
3081
  if (shouldCapturePrivacyRaw(cfg)) {
3093
3082
  return value;
3094
3083
  }
3095
- const transformed = applyRuntimePrivacyPolicy(privacy, target, value, {
3084
+ const privacyInput = normalizeValueForRuntimePrivacy(value);
3085
+ const transformed = applyRuntimePrivacyPolicy(privacy, target, privacyInput, {
3096
3086
  routeKey: req.key,
3097
3087
  trace: trace
3098
3088
  ? {
@@ -3121,7 +3111,8 @@ async function applyPrivacyThenMaskAsync(
3121
3111
  if (shouldCapturePrivacyRaw(cfg)) {
3122
3112
  return value;
3123
3113
  }
3124
- const transformed = await applyRuntimePrivacyPolicyAsync(privacy, target, value, {
3114
+ const privacyInput = normalizeValueForRuntimePrivacy(value);
3115
+ const transformed = await applyRuntimePrivacyPolicyAsync(privacy, target, privacyInput, {
3125
3116
  routeKey: req.key,
3126
3117
  trace: trace
3127
3118
  ? {
@@ -3145,150 +3136,56 @@ type InlinePrivacyMaterializationResult = {
3145
3136
  target: RuntimePrivacySurface;
3146
3137
  serializedLength?: number;
3147
3138
  limit?: number;
3148
- preview?: 'bounded';
3149
3139
  };
3150
3140
  };
3151
3141
 
3152
- function shouldBoundInlinePrivacyTarget(target: RuntimePrivacySurface): boolean {
3153
- return (
3154
- target === 'request.body' ||
3155
- target === 'response.body' ||
3156
- target === 'trace.args' ||
3157
- target === 'trace.returnValue' ||
3158
- target === 'trace.error' ||
3159
- target === 'db.before' ||
3160
- target === 'db.after' ||
3161
- target === 'db.query' ||
3162
- target === 'db.resultMeta' ||
3163
- target === 'db.error'
3164
- );
3142
+ function traceInlineValueExceedsLimit(value: any): boolean {
3143
+ return boundedSerializedTraceValueLength(value, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS).exceeded;
3165
3144
  }
3166
3145
 
3167
- function buildOversizedInlinePreview(
3146
+ function limitInlinePrivacyValue(
3147
+ target: RuntimePrivacySurface,
3168
3148
  value: any,
3169
- depth: number = 0,
3170
- seen: WeakSet<object> = new WeakSet<object>(),
3171
- preserveScalarValue: boolean = true,
3172
- ): any {
3173
- if (value === null || value === undefined) return value;
3174
-
3175
- const type = typeof value;
3176
- if (type === 'string') {
3177
- if (value.length <= INLINE_PRIVACY_PREVIEW_MAX_STRING) {
3178
- return preserveScalarValue ? value : { __type: 'string', length: value.length };
3179
- }
3180
- const trimmed = value.trim();
3181
- const mayBeJson = trimmed.startsWith('{') || trimmed.startsWith('[');
3182
- if (mayBeJson && value.length <= INLINE_PRIVACY_PREVIEW_JSON_PARSE_MAX_CHARS) {
3183
- try {
3184
- return {
3185
- __type: 'json-string',
3186
- length: value.length,
3187
- parsed: buildOversizedInlinePreview(JSON.parse(value), depth + 1, seen, false),
3188
- };
3189
- } catch {}
3190
- }
3191
- return {
3192
- __type: 'string',
3193
- length: value.length,
3194
- truncated: true,
3195
- };
3196
- }
3197
- if (type === 'number' || type === 'boolean') return preserveScalarValue ? value : { __type: type };
3198
- if (type === 'bigint') return preserveScalarValue ? `${value.toString()}n` : { __type: 'bigint' };
3199
- if (type === 'symbol') return preserveScalarValue ? value.toString() : { __type: 'symbol' };
3200
- if (type === 'function') return `[Function${value.name ? ` ${value.name}` : ''}]`;
3201
-
3202
- if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
3203
- return {
3204
- __type: Buffer.isBuffer(value) ? 'Buffer' : 'Uint8Array',
3205
- length: value.length,
3206
- preview: value.length ? Buffer.from(value).slice(0, 32).toString('hex') : '',
3207
- };
3208
- }
3209
- if (value instanceof Date) return preserveScalarValue ? value.toISOString() : { __type: 'Date' };
3210
- if (value instanceof RegExp) return preserveScalarValue ? value.toString() : { __type: 'RegExp' };
3211
- if (value instanceof Error) {
3212
- return {
3213
- __type: 'Error',
3214
- name: value.name,
3215
- message: preserveScalarValue ? value.message : { __type: 'string', length: value.message.length },
3216
- };
3217
- }
3218
- if (value instanceof WeakMap || value instanceof WeakSet) {
3219
- return { __type: getCtorName(value) || 'WeakCollection', uninspectable: true };
3220
- }
3221
-
3222
- if (type !== 'object') return preserveScalarValue ? String(value) : { __type: type };
3223
- if (seen.has(value)) return { __type: 'circular-reference' };
3224
- seen.add(value);
3225
-
3226
- if (Array.isArray(value)) {
3227
- const items = value
3228
- .slice(0, INLINE_PRIVACY_PREVIEW_MAX_ITEMS)
3229
- .map((item) => buildOversizedInlinePreview(item, depth + 1, seen, true));
3230
- if (value.length > INLINE_PRIVACY_PREVIEW_MAX_ITEMS) {
3231
- items.push({ __truncatedItems: value.length - INLINE_PRIVACY_PREVIEW_MAX_ITEMS });
3232
- }
3233
- return items;
3234
- }
3149
+ ): InlinePrivacyMaterializationResult {
3150
+ void target;
3151
+ return { value };
3152
+ }
3235
3153
 
3236
- if (value instanceof Map) {
3237
- const out: Record<string, any> = { __type: 'Map', size: value.size };
3238
- let index = 0;
3239
- for (const [key, item] of value.entries()) {
3240
- if (index >= INLINE_PRIVACY_PREVIEW_MAX_ITEMS) break;
3241
- out[normalizeCollectionKeyForCapture(key, index)] = buildOversizedInlinePreview(item, depth + 1, seen, false);
3242
- index += 1;
3243
- }
3244
- if (value.size > INLINE_PRIVACY_PREVIEW_MAX_ITEMS) {
3245
- out.__truncatedEntries = value.size - INLINE_PRIVACY_PREVIEW_MAX_ITEMS;
3246
- }
3247
- return out;
3248
- }
3154
+ function limitRawInlinePrivacyValue(
3155
+ target: RuntimePrivacySurface,
3156
+ value: any,
3157
+ ): InlinePrivacyMaterializationResult | null {
3158
+ const limited = limitInlinePrivacyValue(target, value);
3159
+ return limited.skipped ? limited : null;
3160
+ }
3249
3161
 
3250
- if (value instanceof Set) {
3251
- const items: any[] = [];
3252
- let index = 0;
3253
- for (const item of value.values()) {
3254
- if (index >= INLINE_PRIVACY_PREVIEW_MAX_ITEMS) break;
3255
- items.push(buildOversizedInlinePreview(item, depth + 1, seen, false));
3256
- index += 1;
3257
- }
3258
- if (value.size > INLINE_PRIVACY_PREVIEW_MAX_ITEMS) {
3259
- items.push({ __truncatedItems: value.size - INLINE_PRIVACY_PREVIEW_MAX_ITEMS });
3260
- }
3261
- return {
3262
- __type: 'Set',
3263
- size: value.size,
3264
- items,
3265
- };
3162
+ function materializeInlinePrivacyValue(
3163
+ target: RuntimePrivacySurface,
3164
+ value: any,
3165
+ cfg: ReproMiddlewareConfig,
3166
+ req: MaskRequestContext,
3167
+ trace: TraceEventForFilter | null,
3168
+ masking: NormalizedMaskingConfig | null,
3169
+ privacy: NormalizedRuntimePrivacyPolicy | null,
3170
+ db?: RuntimePrivacyDbContext | null,
3171
+ ): InlinePrivacyMaterializationResult {
3172
+ if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3173
+ return { value };
3266
3174
  }
3267
3175
 
3268
- const keys = Object.keys(value);
3269
- const ctor = getCtorName(value);
3270
- if (depth >= INLINE_PRIVACY_PREVIEW_MAX_DEPTH) {
3176
+ try {
3177
+ const valueAfterPrivacy = applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db);
3178
+ return { value: valueAfterPrivacy };
3179
+ } catch {
3271
3180
  return {
3272
- ...(ctor && ctor !== 'Object' ? { __type: ctor } : { __type: 'Object' }),
3273
- __keys: keys.slice(0, INLINE_PRIVACY_PREVIEW_MAX_KEYS),
3274
- ...(keys.length > INLINE_PRIVACY_PREVIEW_MAX_KEYS
3275
- ? { __truncatedKeys: keys.length - INLINE_PRIVACY_PREVIEW_MAX_KEYS }
3276
- : {}),
3181
+ value: undefined,
3182
+ skipped: {
3183
+ status: 'skipped',
3184
+ reason: 'inline_privacy_failed',
3185
+ target,
3186
+ },
3277
3187
  };
3278
3188
  }
3279
-
3280
- const out: Record<string, any> = ctor && ctor !== 'Object' ? { __type: ctor } : {};
3281
- for (const key of keys.slice(0, INLINE_PRIVACY_PREVIEW_MAX_KEYS)) {
3282
- try {
3283
- out[key] = buildOversizedInlinePreview(value[key], depth + 1, seen, false);
3284
- } catch (err) {
3285
- out[key] = { __type: 'uninspectable', message: (err as Error)?.message || 'unknown error' };
3286
- }
3287
- }
3288
- if (keys.length > INLINE_PRIVACY_PREVIEW_MAX_KEYS) {
3289
- out.__truncatedKeys = keys.length - INLINE_PRIVACY_PREVIEW_MAX_KEYS;
3290
- }
3291
- return out;
3292
3189
  }
3293
3190
 
3294
3191
  async function materializeInlinePrivacyValueAsync(
@@ -3305,32 +3202,9 @@ async function materializeInlinePrivacyValueAsync(
3305
3202
  return { value };
3306
3203
  }
3307
3204
 
3308
- if (shouldBoundInlinePrivacyTarget(target)) {
3309
- const serialized = stableSerializeTraceValue(value);
3310
- const serializedLength = serialized?.length ?? 0;
3311
- if (serializedLength > INLINE_PRIVACY_MAX_SERIALIZED_CHARS) {
3312
- let preview = buildOversizedInlinePreview(value);
3313
- try {
3314
- preview = await applyPrivacyThenMaskAsync(target, preview, cfg, req, trace, masking, privacy, db);
3315
- } catch {}
3316
- return {
3317
- value: preview,
3318
- skipped: {
3319
- status: 'skipped',
3320
- reason: 'inline_value_too_large',
3321
- target,
3322
- serializedLength,
3323
- limit: INLINE_PRIVACY_MAX_SERIALIZED_CHARS,
3324
- preview: 'bounded',
3325
- },
3326
- };
3327
- }
3328
- }
3329
-
3330
3205
  try {
3331
- return {
3332
- value: await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db),
3333
- };
3206
+ const valueAfterPrivacy = await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db);
3207
+ return { value: valueAfterPrivacy };
3334
3208
  } catch {
3335
3209
  return {
3336
3210
  value: undefined,
@@ -3343,6 +3217,36 @@ async function materializeInlinePrivacyValueAsync(
3343
3217
  }
3344
3218
  }
3345
3219
 
3220
+ function materializeTracePrivacyValue(
3221
+ target: 'trace.args' | 'trace.returnValue' | 'trace.error',
3222
+ value: any,
3223
+ cfg: ReproMiddlewareConfig,
3224
+ req: MaskRequestContext,
3225
+ trace: TraceEventForFilter | null,
3226
+ masking: NormalizedMaskingConfig | null,
3227
+ privacy: NormalizedRuntimePrivacyPolicy | null,
3228
+ ): any {
3229
+ if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3230
+ return value;
3231
+ }
3232
+ return applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy);
3233
+ }
3234
+
3235
+ async function materializeTracePrivacyValueAsync(
3236
+ target: 'trace.args' | 'trace.returnValue' | 'trace.error',
3237
+ value: any,
3238
+ cfg: ReproMiddlewareConfig,
3239
+ req: MaskRequestContext,
3240
+ trace: TraceEventForFilter | null,
3241
+ masking: NormalizedMaskingConfig | null,
3242
+ privacy: NormalizedRuntimePrivacyPolicy | null,
3243
+ ): Promise<any> {
3244
+ if (value === undefined || shouldCapturePrivacyRaw(cfg)) {
3245
+ return value;
3246
+ }
3247
+ return applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy);
3248
+ }
3249
+
3346
3250
  function maskWhenRequiresTrace(when: ReproMaskWhen): boolean {
3347
3251
  return Boolean(
3348
3252
  when.fn ||
@@ -3603,6 +3507,134 @@ function stableSerializeTraceValue(value: any): string | null {
3603
3507
  }
3604
3508
  }
3605
3509
 
3510
+ function boundedSerializedTraceValueLength(value: any, limit: number): { exceeded: boolean; length: number } {
3511
+ let length = 0;
3512
+ const seen = new WeakSet<object>();
3513
+
3514
+ const add = (amount: number) => {
3515
+ length += amount;
3516
+ if (length > limit) {
3517
+ throw TRACE_VALUE_SIZE_EXCEEDED;
3518
+ }
3519
+ };
3520
+
3521
+ const addString = (input: string) => {
3522
+ if (length + input.length + 2 > limit) {
3523
+ throw TRACE_VALUE_SIZE_EXCEEDED;
3524
+ }
3525
+ add(JSON.stringify(input).length);
3526
+ };
3527
+
3528
+ const walk = (input: any): void => {
3529
+ if (input === undefined) {
3530
+ addString('__undefined__');
3531
+ return;
3532
+ }
3533
+ if (input === null) {
3534
+ add(4);
3535
+ return;
3536
+ }
3537
+ const inputType = typeof input;
3538
+ if (inputType === 'string') {
3539
+ addString(input);
3540
+ return;
3541
+ }
3542
+ if (inputType === 'number' || inputType === 'boolean') {
3543
+ add(JSON.stringify(input)?.length ?? String(input).length);
3544
+ return;
3545
+ }
3546
+ if (inputType === 'bigint') {
3547
+ addString(`${input.toString()}n`);
3548
+ return;
3549
+ }
3550
+ if (inputType === 'symbol') {
3551
+ addString(input.toString());
3552
+ return;
3553
+ }
3554
+ if (inputType === 'function') {
3555
+ addString('[Function]');
3556
+ return;
3557
+ }
3558
+
3559
+ if (Array.isArray(input)) {
3560
+ add(1);
3561
+ input.forEach((item, index) => {
3562
+ if (index > 0) add(1);
3563
+ walk(item);
3564
+ });
3565
+ add(1);
3566
+ return;
3567
+ }
3568
+
3569
+ if (input instanceof Map) {
3570
+ add(1);
3571
+ let index = 0;
3572
+ for (const [key, item] of input.entries()) {
3573
+ if (index > 0) add(1);
3574
+ addString(normalizeCollectionKeyForCapture(key, index));
3575
+ add(1);
3576
+ walk(item);
3577
+ index += 1;
3578
+ }
3579
+ add(1);
3580
+ return;
3581
+ }
3582
+
3583
+ if (input instanceof Set) {
3584
+ add(1);
3585
+ let index = 0;
3586
+ for (const item of input.values()) {
3587
+ if (index > 0) add(1);
3588
+ walk(item);
3589
+ index += 1;
3590
+ }
3591
+ add(1);
3592
+ return;
3593
+ }
3594
+
3595
+ if (input instanceof WeakMap || input instanceof WeakSet) {
3596
+ addString(`[${getCtorName(input) || 'WeakCollection'}: uninspectable]`);
3597
+ return;
3598
+ }
3599
+
3600
+ if (input && typeof input === 'object') {
3601
+ if (seen.has(input)) {
3602
+ addString('[Circular]');
3603
+ return;
3604
+ }
3605
+ seen.add(input);
3606
+ add(1);
3607
+ let index = 0;
3608
+ for (const key of Object.keys(input).sort()) {
3609
+ if (index > 0) add(1);
3610
+ addString(key);
3611
+ add(1);
3612
+ try {
3613
+ walk((input as Record<string, any>)[key]);
3614
+ } catch (err) {
3615
+ if (err === TRACE_VALUE_SIZE_EXCEEDED) throw err;
3616
+ addString(`[Cannot serialize: ${(err as Error)?.message || 'unknown error'}]`);
3617
+ }
3618
+ index += 1;
3619
+ }
3620
+ add(1);
3621
+ return;
3622
+ }
3623
+
3624
+ addString(String(input));
3625
+ };
3626
+
3627
+ try {
3628
+ walk(value);
3629
+ return { exceeded: false, length };
3630
+ } catch (err) {
3631
+ if (err === TRACE_VALUE_SIZE_EXCEEDED) {
3632
+ return { exceeded: true, length };
3633
+ }
3634
+ return { exceeded: true, length: limit + 1 };
3635
+ }
3636
+ }
3637
+
3606
3638
  function normalizeCollectionKeyForCapture(value: any, index: number): string {
3607
3639
  const mongoId = coerceMongoId(value);
3608
3640
  if (mongoId !== null) return mongoId;
@@ -4147,10 +4179,7 @@ function createCapturedValueEntry(
4147
4179
  | 'request.body'
4148
4180
  | 'request.params'
4149
4181
  | 'request.query'
4150
- | 'response.body'
4151
- | 'trace.args'
4152
- | 'trace.returnValue'
4153
- | 'trace.error';
4182
+ | 'response.body';
4154
4183
  rawValue: any;
4155
4184
  previewValue: any;
4156
4185
  capture: FullValueCaptureContext;
@@ -4165,13 +4194,6 @@ function createCapturedValueEntry(
4165
4194
  maxItems: TRACE_FULL_VALUE_MAX_ITEMS,
4166
4195
  maxString: TRACE_FULL_VALUE_MAX_STRING,
4167
4196
  };
4168
- const fallbackLimits: FullValueCloneLimits = {
4169
- maxDepth: TRACE_FULL_VALUE_FALLBACK_DEPTH,
4170
- maxKeys: TRACE_FULL_VALUE_FALLBACK_KEYS,
4171
- maxItems: TRACE_FULL_VALUE_FALLBACK_ITEMS,
4172
- maxString: TRACE_FULL_VALUE_FALLBACK_STRING,
4173
- };
4174
-
4175
4197
  const buildMaskedCandidate = (limits: FullValueCloneLimits) => {
4176
4198
  const projected = params.target === 'request.headers'
4177
4199
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
@@ -4200,15 +4222,6 @@ function createCapturedValueEntry(
4200
4222
  return undefined;
4201
4223
  }
4202
4224
 
4203
- let truncatedForStorage = false;
4204
- if (candidateSerialized.length > TRACE_FULL_VALUE_MAX_SERIALIZED_CHARS) {
4205
- const fallback = buildMaskedCandidate(fallbackLimits);
4206
- if (fallback !== undefined && hasMeaningfulFullValue(fallback)) {
4207
- candidate = fallback;
4208
- truncatedForStorage = true;
4209
- }
4210
- }
4211
-
4212
4225
  const valueId = randomUUID();
4213
4226
  const projectedKind =
4214
4227
  candidate && typeof candidate === 'object'
@@ -4218,13 +4231,13 @@ function createCapturedValueEntry(
4218
4231
  id: valueId,
4219
4232
  target: params.target,
4220
4233
  value: candidate,
4221
- truncatedForStorage,
4234
+ truncatedForStorage: false,
4222
4235
  projectedKind: typeof projectedKind === 'string' ? projectedKind : undefined,
4223
4236
  ...(params.metadata ?? {}),
4224
4237
  };
4225
4238
  return {
4226
4239
  captureRef: {
4227
- status: truncatedForStorage ? 'truncated-for-storage' : 'stored',
4240
+ status: 'stored',
4228
4241
  valueId,
4229
4242
  projectedKind: entry.projectedKind,
4230
4243
  },
@@ -4245,10 +4258,7 @@ async function createCapturedValueEntryAsync(
4245
4258
  | 'request.body'
4246
4259
  | 'request.params'
4247
4260
  | 'request.query'
4248
- | 'response.body'
4249
- | 'trace.args'
4250
- | 'trace.returnValue'
4251
- | 'trace.error';
4261
+ | 'response.body';
4252
4262
  rawValue: any;
4253
4263
  previewValue: any;
4254
4264
  capture: FullValueCaptureContext;
@@ -4263,13 +4273,6 @@ async function createCapturedValueEntryAsync(
4263
4273
  maxItems: TRACE_FULL_VALUE_MAX_ITEMS,
4264
4274
  maxString: TRACE_FULL_VALUE_MAX_STRING,
4265
4275
  };
4266
- const fallbackLimits: FullValueCloneLimits = {
4267
- maxDepth: TRACE_FULL_VALUE_FALLBACK_DEPTH,
4268
- maxKeys: TRACE_FULL_VALUE_FALLBACK_KEYS,
4269
- maxItems: TRACE_FULL_VALUE_FALLBACK_ITEMS,
4270
- maxString: TRACE_FULL_VALUE_FALLBACK_STRING,
4271
- };
4272
-
4273
4276
  const buildMaskedCandidate = async (limits: FullValueCloneLimits) => {
4274
4277
  const projected = params.target === 'request.headers'
4275
4278
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
@@ -4298,15 +4301,6 @@ async function createCapturedValueEntryAsync(
4298
4301
  return undefined;
4299
4302
  }
4300
4303
 
4301
- let truncatedForStorage = false;
4302
- if (candidateSerialized.length > TRACE_FULL_VALUE_MAX_SERIALIZED_CHARS) {
4303
- const fallback = await buildMaskedCandidate(fallbackLimits);
4304
- if (fallback !== undefined && hasMeaningfulFullValue(fallback)) {
4305
- candidate = fallback;
4306
- truncatedForStorage = true;
4307
- }
4308
- }
4309
-
4310
4304
  const valueId = randomUUID();
4311
4305
  const projectedKind =
4312
4306
  candidate && typeof candidate === 'object'
@@ -4316,13 +4310,13 @@ async function createCapturedValueEntryAsync(
4316
4310
  id: valueId,
4317
4311
  target: params.target,
4318
4312
  value: candidate,
4319
- truncatedForStorage,
4313
+ truncatedForStorage: false,
4320
4314
  projectedKind: typeof projectedKind === 'string' ? projectedKind : undefined,
4321
4315
  ...(params.metadata ?? {}),
4322
4316
  };
4323
4317
  return {
4324
4318
  captureRef: {
4325
- status: truncatedForStorage ? 'truncated-for-storage' : 'stored',
4319
+ status: 'stored',
4326
4320
  valueId,
4327
4321
  projectedKind: entry.projectedKind,
4328
4322
  },
@@ -4330,84 +4324,6 @@ async function createCapturedValueEntryAsync(
4330
4324
  };
4331
4325
  }
4332
4326
 
4333
- function maybeCaptureFullTraceValue(
4334
- params: {
4335
- target: 'trace.args' | 'trace.returnValue' | 'trace.error';
4336
- rawValue: any;
4337
- previewValue: any;
4338
- event: TraceEventRecord;
4339
- capture: FullValueCaptureContext;
4340
- },
4341
- ): TraceValueCaptureRef | undefined {
4342
- const result = createCapturedValueEntry({
4343
- target: params.target,
4344
- rawValue: params.rawValue,
4345
- previewValue: params.previewValue,
4346
- capture: params.capture,
4347
- metadata: {
4348
- fn: params.event.fn,
4349
- file: params.event.file,
4350
- line: params.event.line ?? null,
4351
- spanId: params.event.spanId ?? null,
4352
- parentSpanId: params.event.parentSpanId ?? null,
4353
- },
4354
- });
4355
- if (!result) return undefined;
4356
- if (result.entry) {
4357
- const existing = (params.event as any)[TRACE_FULL_VALUE_ENTRY_SYMBOL] as TraceValueBatchEntry[] | undefined;
4358
- if (Array.isArray(existing)) {
4359
- existing.push(result.entry);
4360
- } else {
4361
- Object.defineProperty(params.event, TRACE_FULL_VALUE_ENTRY_SYMBOL, {
4362
- value: [result.entry],
4363
- enumerable: false,
4364
- configurable: true,
4365
- writable: true,
4366
- });
4367
- }
4368
- }
4369
- return result.captureRef;
4370
- }
4371
-
4372
- async function maybeCaptureFullTraceValueAsync(
4373
- params: {
4374
- target: 'trace.args' | 'trace.returnValue' | 'trace.error';
4375
- rawValue: any;
4376
- previewValue: any;
4377
- event: TraceEventRecord;
4378
- capture: FullValueCaptureContext;
4379
- },
4380
- ): Promise<TraceValueCaptureRef | undefined> {
4381
- const result = await createCapturedValueEntryAsync({
4382
- target: params.target,
4383
- rawValue: params.rawValue,
4384
- previewValue: params.previewValue,
4385
- capture: params.capture,
4386
- metadata: {
4387
- fn: params.event.fn,
4388
- file: params.event.file,
4389
- line: params.event.line ?? null,
4390
- spanId: params.event.spanId ?? null,
4391
- parentSpanId: params.event.parentSpanId ?? null,
4392
- },
4393
- });
4394
- if (!result) return undefined;
4395
- if (result.entry) {
4396
- const existing = (params.event as any)[TRACE_FULL_VALUE_ENTRY_SYMBOL] as TraceValueBatchEntry[] | undefined;
4397
- if (Array.isArray(existing)) {
4398
- existing.push(result.entry);
4399
- } else {
4400
- Object.defineProperty(params.event, TRACE_FULL_VALUE_ENTRY_SYMBOL, {
4401
- value: [result.entry],
4402
- enumerable: false,
4403
- configurable: true,
4404
- writable: true,
4405
- });
4406
- }
4407
- }
4408
- return result.captureRef;
4409
- }
4410
-
4411
4327
  function maybeCaptureRequestValue(
4412
4328
  params: {
4413
4329
  target: 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body';
@@ -4583,6 +4499,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4583
4499
  let idleTimer: NodeJS.Timeout | null = null;
4584
4500
  let hardStopTimer: NodeJS.Timeout | null = null;
4585
4501
  let flushPayload: null | (() => Promise<void>) = null;
4502
+ let requestCaptureScheduled = false;
4586
4503
  let sessionDrainWait: Promise<void> | null = null;
4587
4504
  const activeSpans = new Set<string>();
4588
4505
  let anonymousSpanDepth = 0;
@@ -4656,6 +4573,226 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4656
4573
  scheduleIdleFlush();
4657
4574
  };
4658
4575
 
4576
+ const chooseRequestEndpoint = (): {
4577
+ chosenEndpoint: EndpointTraceInfo;
4578
+ hasTraceEvents: boolean;
4579
+ } => {
4580
+ const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
4581
+ const baseEvents = balanceTraceEvents(pendingEvents.slice() as TraceEventRecord[]);
4582
+ const orderedEvents = TRACE_ORDER_MODE === 'tree'
4583
+ ? reorderTraceEvents(baseEvents)
4584
+ : sortTraceEventsChronologically(baseEvents);
4585
+ const summary = summarizeEndpointFromEvents(orderedEvents);
4586
+ return {
4587
+ chosenEndpoint: summary.endpointTrace
4588
+ ?? summary.preferredAppTrace
4589
+ ?? summary.firstAppTrace
4590
+ ?? endpointTrace
4591
+ ?? preferredAppTrace
4592
+ ?? firstAppTrace
4593
+ ?? { fn: null, file: null, line: null, functionType: null },
4594
+ hasTraceEvents: orderedEvents.length > 0,
4595
+ };
4596
+ };
4597
+
4598
+ const buildRequestCapturePayloadAsync = async (
4599
+ chosenEndpoint: EndpointTraceInfo,
4600
+ hasTraceEvents: boolean,
4601
+ ): Promise<{
4602
+ requestPayload: Record<string, any>;
4603
+ requestValueEntries: TraceValueBatchEntry[];
4604
+ }> => {
4605
+ const endpointTraceCtx: TraceEventForFilter | null = (() => {
4606
+ if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
4607
+ return {
4608
+ type: 'enter',
4609
+ eventType: 'enter',
4610
+ fn: chosenEndpoint.fn ?? undefined,
4611
+ wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
4612
+ file: chosenEndpoint.file ?? null,
4613
+ line: chosenEndpoint.line ?? null,
4614
+ functionType: chosenEndpoint.functionType ?? null,
4615
+ library: inferLibraryNameFromFile(chosenEndpoint.file),
4616
+ };
4617
+ })();
4618
+ const activePrivacy = resolvePrivacy();
4619
+
4620
+ const requestBodyRaw = (req as any).body;
4621
+ const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
4622
+ ?? await materializeInlinePrivacyValueAsync(
4623
+ 'request.body',
4624
+ sanitizeRequestSnapshot(requestBodyRaw),
4625
+ cfg,
4626
+ maskReq,
4627
+ endpointTraceCtx,
4628
+ masking,
4629
+ activePrivacy,
4630
+ );
4631
+ const requestBody = requestBodyMaterialization.value;
4632
+ const requestParams = await applyPrivacyThenMaskAsync(
4633
+ 'request.params',
4634
+ sanitizeRequestSnapshot((req as any).params),
4635
+ cfg,
4636
+ maskReq,
4637
+ endpointTraceCtx,
4638
+ masking,
4639
+ activePrivacy,
4640
+ );
4641
+ const requestQuery = await applyPrivacyThenMaskAsync(
4642
+ 'request.query',
4643
+ sanitizeRequestSnapshot((req as any).query),
4644
+ cfg,
4645
+ maskReq,
4646
+ endpointTraceCtx,
4647
+ masking,
4648
+ activePrivacy,
4649
+ );
4650
+ const maskedHeaders = await applyPrivacyThenMaskAsync(
4651
+ 'request.headers',
4652
+ requestHeaders,
4653
+ cfg,
4654
+ maskReq,
4655
+ endpointTraceCtx,
4656
+ masking,
4657
+ activePrivacy,
4658
+ );
4659
+ const responseBodyMaterialization = capturedBody === undefined
4660
+ ? { value: undefined }
4661
+ : limitRawInlinePrivacyValue('response.body', capturedBody)
4662
+ ?? await materializeInlinePrivacyValueAsync(
4663
+ 'response.body',
4664
+ sanitizeRequestSnapshot(capturedBody),
4665
+ cfg,
4666
+ maskReq,
4667
+ endpointTraceCtx,
4668
+ masking,
4669
+ activePrivacy,
4670
+ );
4671
+ const responseBody = responseBodyMaterialization.value;
4672
+ const requestValueEntries: TraceValueBatchEntry[] = [];
4673
+ const bodyValueCapture = requestBodyMaterialization.skipped
4674
+ ? undefined
4675
+ : await maybeCaptureRequestValueAsync({
4676
+ target: 'request.body',
4677
+ rawValue: (req as any).body,
4678
+ previewValue: requestBody,
4679
+ capture: {
4680
+ runtimeConfig: cfg,
4681
+ captureHeaders: cfg.captureHeaders,
4682
+ maskReq,
4683
+ trace: endpointTraceCtx,
4684
+ masking,
4685
+ privacy: activePrivacy,
4686
+ },
4687
+ }, requestValueEntries);
4688
+ const paramsValueCapture = await maybeCaptureRequestValueAsync({
4689
+ target: 'request.params',
4690
+ rawValue: (req as any).params,
4691
+ previewValue: requestParams,
4692
+ capture: {
4693
+ runtimeConfig: cfg,
4694
+ captureHeaders: cfg.captureHeaders,
4695
+ maskReq,
4696
+ trace: endpointTraceCtx,
4697
+ masking,
4698
+ privacy: activePrivacy,
4699
+ },
4700
+ }, requestValueEntries);
4701
+ const queryValueCapture = await maybeCaptureRequestValueAsync({
4702
+ target: 'request.query',
4703
+ rawValue: (req as any).query,
4704
+ previewValue: requestQuery,
4705
+ capture: {
4706
+ runtimeConfig: cfg,
4707
+ captureHeaders: cfg.captureHeaders,
4708
+ maskReq,
4709
+ trace: endpointTraceCtx,
4710
+ masking,
4711
+ privacy: activePrivacy,
4712
+ },
4713
+ }, requestValueEntries);
4714
+ const headersValueCapture = await maybeCaptureRequestValueAsync({
4715
+ target: 'request.headers',
4716
+ rawValue: req.headers,
4717
+ previewValue: maskedHeaders,
4718
+ capture: {
4719
+ runtimeConfig: cfg,
4720
+ captureHeaders: cfg.captureHeaders,
4721
+ maskReq,
4722
+ trace: endpointTraceCtx,
4723
+ masking,
4724
+ privacy: activePrivacy,
4725
+ },
4726
+ }, requestValueEntries);
4727
+ const respBodyValueCapture = responseBodyMaterialization.skipped
4728
+ ? undefined
4729
+ : await maybeCaptureRequestValueAsync({
4730
+ target: 'response.body',
4731
+ rawValue: capturedBody,
4732
+ previewValue: responseBody,
4733
+ capture: {
4734
+ runtimeConfig: cfg,
4735
+ captureHeaders: cfg.captureHeaders,
4736
+ maskReq,
4737
+ trace: endpointTraceCtx,
4738
+ masking,
4739
+ privacy: activePrivacy,
4740
+ },
4741
+ }, requestValueEntries);
4742
+
4743
+ const requestPayload: Record<string, any> = {
4744
+ rid,
4745
+ method: req.method,
4746
+ url,
4747
+ path,
4748
+ status: res.statusCode,
4749
+ durMs: Date.now() - t0,
4750
+ headers: maskedHeaders,
4751
+ key,
4752
+ respBody: responseBody,
4753
+ trace: hasTraceEvents ? undefined : [],
4754
+ };
4755
+ if (requestBody !== undefined) requestPayload.body = requestBody;
4756
+ if (bodyValueCapture) requestPayload.bodyValueCapture = bodyValueCapture;
4757
+ if (requestParams !== undefined) requestPayload.params = requestParams;
4758
+ if (paramsValueCapture) requestPayload.paramsValueCapture = paramsValueCapture;
4759
+ if (requestQuery !== undefined) requestPayload.query = requestQuery;
4760
+ if (queryValueCapture) requestPayload.queryValueCapture = queryValueCapture;
4761
+ if (headersValueCapture) requestPayload.headersValueCapture = headersValueCapture;
4762
+ if (respBodyValueCapture) requestPayload.respBodyValueCapture = respBodyValueCapture;
4763
+ if (requestBodyMaterialization.skipped) {
4764
+ requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4765
+ }
4766
+ if (responseBodyMaterialization.skipped) {
4767
+ requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4768
+ }
4769
+ requestPayload.entryPoint = chosenEndpoint;
4770
+
4771
+ return { requestPayload, requestValueEntries };
4772
+ };
4773
+
4774
+ const emitRequestCaptureAsync = async (): Promise<void> => {
4775
+ if (requestCaptureScheduled) return;
4776
+ requestCaptureScheduled = true;
4777
+ try {
4778
+ const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
4779
+ const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
4780
+ chosenEndpoint,
4781
+ hasTraceEvents,
4782
+ );
4783
+ post(cfg, sid, {
4784
+ entries: [{
4785
+ actionId: aid,
4786
+ request: requestPayload,
4787
+ requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4788
+ t: requestEpochMs,
4789
+ }]
4790
+ });
4791
+ } catch {
4792
+ // never break user code
4793
+ }
4794
+ };
4795
+
4659
4796
  try {
4660
4797
  if (__TRACER__?.tracer?.on) {
4661
4798
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -4710,7 +4847,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4710
4847
  scheduleIdleFlush();
4711
4848
  }
4712
4849
 
4713
- if (ev.args !== undefined || ev.returnValue !== undefined || ev.error !== undefined) {
4850
+ const hasErrorPayload = hasMeaningfulRawTraceError(ev.error);
4851
+ if (ev.args !== undefined || ev.returnValue !== undefined || hasErrorPayload) {
4714
4852
  evt.__reproPending = {
4715
4853
  ...(ev.args !== undefined
4716
4854
  ? {
@@ -4722,7 +4860,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4722
4860
  returnValueRaw: ev.returnValue,
4723
4861
  }
4724
4862
  : {}),
4725
- ...(ev.error !== undefined
4863
+ ...(hasErrorPayload
4726
4864
  ? {
4727
4865
  errorRaw: ev.error,
4728
4866
  }
@@ -4750,6 +4888,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4750
4888
  capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
4751
4889
  }
4752
4890
 
4891
+ void emitRequestCaptureAsync();
4892
+
4753
4893
  if (!flushPayload) {
4754
4894
  flushPayload = async () => {
4755
4895
  try {
@@ -4764,184 +4904,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4764
4904
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4765
4905
  ? reorderTraceEvents(baseEvents)
4766
4906
  : sortTraceEventsChronologically(baseEvents);
4767
- const summary = summarizeEndpointFromEvents(orderedEvents);
4768
- const chosenEndpoint = summary.endpointTrace
4769
- ?? summary.preferredAppTrace
4770
- ?? summary.firstAppTrace
4771
- ?? endpointTrace
4772
- ?? preferredAppTrace
4773
- ?? firstAppTrace
4774
- ?? { fn: null, file: null, line: null, functionType: null };
4775
4907
  const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
4776
- const endpointTraceCtx: TraceEventForFilter | null = (() => {
4777
- if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
4778
- return {
4779
- type: 'enter',
4780
- eventType: 'enter',
4781
- fn: chosenEndpoint.fn ?? undefined,
4782
- wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
4783
- file: chosenEndpoint.file ?? null,
4784
- line: chosenEndpoint.line ?? null,
4785
- functionType: chosenEndpoint.functionType ?? null,
4786
- library: inferLibraryNameFromFile(chosenEndpoint.file),
4787
- };
4788
- })();
4789
- const activePrivacy = resolvePrivacy();
4790
-
4791
- const requestBodyMaterialization = await materializeInlinePrivacyValueAsync(
4792
- 'request.body',
4793
- sanitizeRequestSnapshot((req as any).body),
4794
- cfg,
4795
- maskReq,
4796
- endpointTraceCtx,
4797
- masking,
4798
- activePrivacy,
4799
- );
4800
- const requestBody = requestBodyMaterialization.value;
4801
- const requestParams = await applyPrivacyThenMaskAsync(
4802
- 'request.params',
4803
- sanitizeRequestSnapshot((req as any).params),
4804
- cfg,
4805
- maskReq,
4806
- endpointTraceCtx,
4807
- masking,
4808
- activePrivacy,
4809
- );
4810
- const requestQuery = await applyPrivacyThenMaskAsync(
4811
- 'request.query',
4812
- sanitizeRequestSnapshot((req as any).query),
4813
- cfg,
4814
- maskReq,
4815
- endpointTraceCtx,
4816
- masking,
4817
- activePrivacy,
4818
- );
4819
- const maskedHeaders = await applyPrivacyThenMaskAsync(
4820
- 'request.headers',
4821
- requestHeaders,
4822
- cfg,
4823
- maskReq,
4824
- endpointTraceCtx,
4825
- masking,
4826
- activePrivacy,
4827
- );
4828
- const responseBodyMaterialization = await materializeInlinePrivacyValueAsync(
4829
- 'response.body',
4830
- capturedBody === undefined ? undefined : sanitizeRequestSnapshot(capturedBody),
4831
- cfg,
4832
- maskReq,
4833
- endpointTraceCtx,
4834
- masking,
4835
- activePrivacy,
4836
- );
4837
- const responseBody = responseBodyMaterialization.value;
4838
- const requestValueEntries: TraceValueBatchEntry[] = [];
4839
- const bodyValueCapture = requestBodyMaterialization.skipped
4840
- ? undefined
4841
- : await maybeCaptureRequestValueAsync({
4842
- target: 'request.body',
4843
- rawValue: (req as any).body,
4844
- previewValue: requestBody,
4845
- capture: {
4846
- runtimeConfig: cfg,
4847
- captureHeaders: cfg.captureHeaders,
4848
- maskReq,
4849
- trace: endpointTraceCtx,
4850
- masking,
4851
- privacy: activePrivacy,
4852
- },
4853
- }, requestValueEntries);
4854
- const paramsValueCapture = await maybeCaptureRequestValueAsync({
4855
- target: 'request.params',
4856
- rawValue: (req as any).params,
4857
- previewValue: requestParams,
4858
- capture: {
4859
- runtimeConfig: cfg,
4860
- captureHeaders: cfg.captureHeaders,
4861
- maskReq,
4862
- trace: endpointTraceCtx,
4863
- masking,
4864
- privacy: activePrivacy,
4865
- },
4866
- }, requestValueEntries);
4867
- const queryValueCapture = await maybeCaptureRequestValueAsync({
4868
- target: 'request.query',
4869
- rawValue: (req as any).query,
4870
- previewValue: requestQuery,
4871
- capture: {
4872
- runtimeConfig: cfg,
4873
- captureHeaders: cfg.captureHeaders,
4874
- maskReq,
4875
- trace: endpointTraceCtx,
4876
- masking,
4877
- privacy: activePrivacy,
4878
- },
4879
- }, requestValueEntries);
4880
- const headersValueCapture = await maybeCaptureRequestValueAsync({
4881
- target: 'request.headers',
4882
- rawValue: req.headers,
4883
- previewValue: maskedHeaders,
4884
- capture: {
4885
- runtimeConfig: cfg,
4886
- captureHeaders: cfg.captureHeaders,
4887
- maskReq,
4888
- trace: endpointTraceCtx,
4889
- masking,
4890
- privacy: activePrivacy,
4891
- },
4892
- }, requestValueEntries);
4893
- const respBodyValueCapture = responseBodyMaterialization.skipped
4894
- ? undefined
4895
- : await maybeCaptureRequestValueAsync({
4896
- target: 'response.body',
4897
- rawValue: capturedBody,
4898
- previewValue: responseBody,
4899
- capture: {
4900
- runtimeConfig: cfg,
4901
- captureHeaders: cfg.captureHeaders,
4902
- maskReq,
4903
- trace: endpointTraceCtx,
4904
- masking,
4905
- privacy: activePrivacy,
4906
- },
4907
- }, requestValueEntries);
4908
-
4909
- const requestPayload: Record<string, any> = {
4910
- rid,
4911
- method: req.method,
4912
- url,
4913
- path,
4914
- status: res.statusCode,
4915
- durMs: Date.now() - t0,
4916
- headers: maskedHeaders,
4917
- key,
4918
- respBody: responseBody,
4919
- trace: traceBatches.length ? undefined : [],
4920
- };
4921
- if (requestBody !== undefined) requestPayload.body = requestBody;
4922
- if (bodyValueCapture) requestPayload.bodyValueCapture = bodyValueCapture;
4923
- if (requestParams !== undefined) requestPayload.params = requestParams;
4924
- if (paramsValueCapture) requestPayload.paramsValueCapture = paramsValueCapture;
4925
- if (requestQuery !== undefined) requestPayload.query = requestQuery;
4926
- if (queryValueCapture) requestPayload.queryValueCapture = queryValueCapture;
4927
- if (headersValueCapture) requestPayload.headersValueCapture = headersValueCapture;
4928
- if (respBodyValueCapture) requestPayload.respBodyValueCapture = respBodyValueCapture;
4929
- if (requestBodyMaterialization.skipped) {
4930
- requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4931
- }
4932
- if (responseBodyMaterialization.skipped) {
4933
- requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4934
- }
4935
- requestPayload.entryPoint = chosenEndpoint;
4936
-
4937
- post(cfg, sid, {
4938
- entries: [{
4939
- actionId: aid,
4940
- request: requestPayload,
4941
- requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4942
- t: requestEpochMs,
4943
- }]
4944
- });
4945
4908
 
4946
4909
  if (traceBatches.length) {
4947
4910
  for (let i = 0; i < traceBatches.length; i++) {
@@ -5390,7 +5353,11 @@ function sanitizeResultForMeta(value: any, options: TraceSanitizeOptions = {}) {
5390
5353
  if (value === undefined) return undefined;
5391
5354
  if (typeof value === 'function') return undefined;
5392
5355
  try {
5393
- return sanitizeTraceValue(value, 0, new WeakMap(), options);
5356
+ return sanitizeTraceValue(value, 0, new WeakMap(), {
5357
+ preserveLongStrings: true,
5358
+ disableTruncation: true,
5359
+ ...options,
5360
+ });
5394
5361
  } catch {
5395
5362
  const fallback = safeJson(value);
5396
5363
  return fallback === undefined ? undefined : fallback;
@@ -5471,7 +5438,7 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
5471
5438
  const afterPreview = afterMaterialization.value;
5472
5439
  const resultMetaMaterialization = await materializeInlinePrivacyValueAsync('db.resultMeta', resultMetaSource ?? undefined, cfg, maskReq, null, null, privacy, dbContext);
5473
5440
  const resultMetaPreviewRaw = resultMetaMaterialization.value;
5474
- const resultMetaPreview = sanitizeTraceValue(resultMetaPreviewRaw);
5441
+ const resultMetaPreview = sanitizeMaterializedTraceValue(resultMetaPreviewRaw);
5475
5442
  const errorMaterialization = await materializeInlinePrivacyValueAsync('db.error', payload.error ?? undefined, cfg, maskReq, null, null, privacy, dbContext);
5476
5443
  const errorPreview = errorMaterialization.value;
5477
5444
  const capture: FullValueCaptureContext = {
@@ -5894,27 +5861,111 @@ function normalizeKafkaHeadersForPayload(headers: any): Record<string, string> {
5894
5861
  return out;
5895
5862
  }
5896
5863
 
5897
- function previewKafkaValue(value: any, maxLength: number = 120): string | null {
5898
- if (value === null || value === undefined) return null;
5899
- let text: string;
5900
- if (typeof value === 'string') {
5901
- text = value;
5902
- } else if (Buffer.isBuffer(value)) {
5903
- text = value.toString('utf8');
5904
- } else {
5905
- return null;
5906
- }
5907
- if (text.length <= maxLength) return text;
5908
- return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
5909
- }
5910
-
5911
5864
  function kafkaValueSize(value: any): number | null {
5912
5865
  if (value === null || value === undefined) return null;
5913
5866
  if (typeof value === 'string') return Buffer.byteLength(value, 'utf8');
5914
5867
  if (Buffer.isBuffer(value)) return value.length;
5868
+ if (value instanceof Uint8Array) return value.byteLength;
5915
5869
  return null;
5916
5870
  }
5917
5871
 
5872
+ function decodeKafkaValue(value: any): any {
5873
+ if (value === null || value === undefined) return null;
5874
+ if (typeof value === 'string') return value;
5875
+ if (Buffer.isBuffer(value)) return value.toString('utf8');
5876
+ if (value instanceof Uint8Array) return Buffer.from(value).toString('utf8');
5877
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
5878
+ return sanitizeResultForMeta(value);
5879
+ }
5880
+
5881
+ function parseKafkaJsonString(value: string): { parsed: true; value: any } | { parsed: false; value: string } {
5882
+ const trimmed = value.trim();
5883
+ if (!trimmed || !'{['.includes(trimmed[0]!)) {
5884
+ return { parsed: false, value };
5885
+ }
5886
+ try {
5887
+ return { parsed: true, value: JSON.parse(value) };
5888
+ } catch {
5889
+ return { parsed: false, value };
5890
+ }
5891
+ }
5892
+
5893
+ function decodeKafkaMessageValue(value: any): {
5894
+ value: any;
5895
+ rawValue?: string;
5896
+ valueEncoding: string | null;
5897
+ } {
5898
+ const decoded = decodeKafkaValue(value);
5899
+ if (typeof decoded !== 'string') {
5900
+ return {
5901
+ value: decoded,
5902
+ valueEncoding: decoded === null ? null : typeof decoded,
5903
+ };
5904
+ }
5905
+ const parsed = parseKafkaJsonString(decoded);
5906
+ if (parsed.parsed) {
5907
+ return {
5908
+ value: parsed.value,
5909
+ rawValue: decoded,
5910
+ valueEncoding: 'json',
5911
+ };
5912
+ }
5913
+ return {
5914
+ value: decoded,
5915
+ valueEncoding: 'utf8',
5916
+ };
5917
+ }
5918
+
5919
+ function buildKafkaMessagePayload(message: any): Record<string, any> {
5920
+ const msgObj = message && typeof message === 'object' ? message : { value: message };
5921
+ const decoded = decodeKafkaMessageValue(msgObj.value);
5922
+ const payload: Record<string, any> = {
5923
+ key: decodeKafkaValue(msgObj.key),
5924
+ value: decoded.value,
5925
+ valueEncoding: decoded.valueEncoding,
5926
+ valueBytes: kafkaValueSize(msgObj.value),
5927
+ headers: normalizeKafkaHeadersForPayload(msgObj.headers),
5928
+ };
5929
+ if (decoded.rawValue !== undefined) {
5930
+ payload.rawValue = decoded.rawValue;
5931
+ }
5932
+ if (msgObj.partition !== undefined) {
5933
+ payload.partition = msgObj.partition;
5934
+ }
5935
+ if (msgObj.timestamp !== undefined) {
5936
+ payload.timestamp = msgObj.timestamp;
5937
+ }
5938
+ if (msgObj.offset !== undefined) {
5939
+ payload.offset = msgObj.offset;
5940
+ }
5941
+ return payload;
5942
+ }
5943
+
5944
+ function cloneKafkaTelemetryValue(value: any): any {
5945
+ if (value === undefined || value === null) return value;
5946
+ try {
5947
+ return JSON.parse(JSON.stringify(value));
5948
+ } catch {
5949
+ return value;
5950
+ }
5951
+ }
5952
+
5953
+ async function materializeKafkaRequestBody(
5954
+ cfg: KafkaJsPatchConfig,
5955
+ maskReq: MaskRequestContext,
5956
+ body: Record<string, any>,
5957
+ ): Promise<InlinePrivacyMaterializationResult> {
5958
+ return materializeInlinePrivacyValueAsync(
5959
+ 'request.body',
5960
+ sanitizeTraceValueForPrivacy(body),
5961
+ cfg,
5962
+ maskReq,
5963
+ null,
5964
+ normalizeMaskingConfig(cfg.masking),
5965
+ getRuntimePrivacyState(cfg).policy ?? null,
5966
+ );
5967
+ }
5968
+
5918
5969
  function summarizeKafkaError(error: any): Record<string, unknown> {
5919
5970
  const message =
5920
5971
  typeof error?.message === 'string'
@@ -6005,6 +6056,7 @@ function recordKafkaTraceEvent(
6005
6056
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
6006
6057
  };
6007
6058
  if (shouldDropTraceEvent(candidate)) return;
6059
+ const omitJsonBuiltinValues = shouldOmitJsonBuiltinTraceValues(candidate);
6008
6060
 
6009
6061
  const evt: TraceEventRecord = {
6010
6062
  t: alignTimestamp(typeof raw.t === 'number' ? raw.t : Date.now()),
@@ -6020,8 +6072,8 @@ function recordKafkaTraceEvent(
6020
6072
 
6021
6073
  const privacy = getRuntimePrivacyState(cfg).policy ?? null;
6022
6074
  const masking = normalizeMaskingConfig(cfg.masking);
6023
- if (raw.args !== undefined) {
6024
- evt.args = sanitizeTraceValue(applyPrivacyThenMask(
6075
+ if (!omitJsonBuiltinValues && raw.args !== undefined) {
6076
+ evt.args = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6025
6077
  'trace.args',
6026
6078
  sanitizeTraceArgsForPrivacy(raw.args),
6027
6079
  cfg,
@@ -6031,8 +6083,8 @@ function recordKafkaTraceEvent(
6031
6083
  privacy,
6032
6084
  ));
6033
6085
  }
6034
- if (raw.returnValue !== undefined) {
6035
- evt.returnValue = sanitizeTraceValue(applyPrivacyThenMask(
6086
+ if (!omitJsonBuiltinValues && raw.returnValue !== undefined) {
6087
+ evt.returnValue = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6036
6088
  'trace.returnValue',
6037
6089
  sanitizeTraceValueForPrivacy(raw.returnValue),
6038
6090
  cfg,
@@ -6042,8 +6094,8 @@ function recordKafkaTraceEvent(
6042
6094
  privacy,
6043
6095
  ));
6044
6096
  }
6045
- if (raw.error !== undefined) {
6046
- evt.error = sanitizeTraceValue(applyPrivacyThenMask(
6097
+ if (hasMeaningfulRawTraceError(raw.error)) {
6098
+ evt.error = sanitizeMaterializedTraceValue(materializeTracePrivacyValue(
6047
6099
  'trace.error',
6048
6100
  sanitizeTraceValueForPrivacy(raw.error),
6049
6101
  cfg,
@@ -6079,6 +6131,7 @@ function buildPendingKafkaTraceEvent(raw: any): PendingTraceEventRecord | null {
6079
6131
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
6080
6132
  };
6081
6133
  if (shouldDropTraceEvent(candidate)) return null;
6134
+ const omitJsonBuiltinValues = shouldOmitJsonBuiltinTraceValues(candidate);
6082
6135
 
6083
6136
  const evt: PendingTraceEventRecord = {
6084
6137
  t: alignTimestamp(typeof raw.t === 'number' ? raw.t : Date.now()),
@@ -6094,19 +6147,20 @@ function buildPendingKafkaTraceEvent(raw: any): PendingTraceEventRecord | null {
6094
6147
  if (typeof raw.sourceFile === 'string' && raw.sourceFile) {
6095
6148
  evt.__reproSourceFile = String(raw.sourceFile);
6096
6149
  }
6097
- if (raw.args !== undefined || raw.returnValue !== undefined || raw.error !== undefined) {
6150
+ const hasErrorPayload = hasMeaningfulRawTraceError(raw.error);
6151
+ if ((!omitJsonBuiltinValues && (raw.args !== undefined || raw.returnValue !== undefined)) || hasErrorPayload) {
6098
6152
  evt.__reproPending = {
6099
- ...(raw.args !== undefined
6153
+ ...(!omitJsonBuiltinValues && raw.args !== undefined
6100
6154
  ? {
6101
6155
  argsRaw: normalizeRawTraceArgs(raw.args),
6102
6156
  }
6103
6157
  : {}),
6104
- ...(raw.returnValue !== undefined
6158
+ ...(!omitJsonBuiltinValues && raw.returnValue !== undefined
6105
6159
  ? {
6106
6160
  returnValueRaw: raw.returnValue,
6107
6161
  }
6108
6162
  : {}),
6109
- ...(raw.error !== undefined
6163
+ ...(hasErrorPayload
6110
6164
  ? {
6111
6165
  errorRaw: raw.error,
6112
6166
  }
@@ -6251,14 +6305,26 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
6251
6305
  throw err;
6252
6306
  } finally {
6253
6307
  if (!sid) return;
6254
- const previewKeys = propagatedMessages
6255
- .slice(0, 10)
6256
- .map((message: any) => previewKafkaValue(message?.key))
6257
- .filter((value: string | null): value is string => value !== null);
6258
6308
  const totalValueBytes = propagatedMessages.reduce((sum: number, message: any) => {
6259
6309
  const size = kafkaValueSize(message?.value);
6260
6310
  return sum + (size ?? 0);
6261
6311
  }, 0);
6312
+ const requestKey = `KAFKA_PUBLISH ${topic}`;
6313
+ const maskReq: MaskRequestContext = {
6314
+ method: 'KAFKA_PUBLISH',
6315
+ path: `kafka://${topic}`,
6316
+ key: requestKey,
6317
+ };
6318
+ const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, {
6319
+ transport: 'kafka',
6320
+ topic,
6321
+ messageCount: propagatedMessages.length,
6322
+ totalValueBytes,
6323
+ messages: propagatedMessages.map((message: any) => buildKafkaMessagePayload(message)),
6324
+ traceId: span?.traceId ?? null,
6325
+ spanId: span?.spanId ?? null,
6326
+ parentSpanId: span?.parentSpanId ?? null,
6327
+ });
6262
6328
 
6263
6329
  const requestPayload: Record<string, any> = {
6264
6330
  rid: publishRid,
@@ -6267,22 +6333,14 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
6267
6333
  path: `kafka://${topic}`,
6268
6334
  status: threw ? 500 : 200,
6269
6335
  durMs: Date.now() - t0,
6270
- key: `KAFKA_PUBLISH ${topic}`,
6336
+ key: requestKey,
6271
6337
  headers: {
6272
6338
  transport: 'kafka',
6273
6339
  topic,
6274
6340
  messageCount: propagatedMessages.length,
6275
6341
  },
6276
- body: {
6277
- transport: 'kafka',
6278
- topic,
6279
- messageCount: propagatedMessages.length,
6280
- totalValueBytes,
6281
- keyPreview: previewKeys,
6282
- traceId: span?.traceId ?? null,
6283
- spanId: span?.spanId ?? null,
6284
- parentSpanId: span?.parentSpanId ?? null,
6285
- },
6342
+ body: bodyMaterialization.value,
6343
+ bodyMaterialization: bodyMaterialization.skipped,
6286
6344
  respBody: threw
6287
6345
  ? { error: summarizeKafkaError(error) }
6288
6346
  : summarizeKafkaProducerResult(result),
@@ -6373,30 +6431,51 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
6373
6431
  const count = Array.isArray(entry.messages) ? entry.messages.length : 0;
6374
6432
  return sum + count;
6375
6433
  }, 0);
6434
+ const totalValueBytes = patchedTopicMessages.reduce((sum: number, entry: any) => {
6435
+ const messages = Array.isArray(entry.messages) ? entry.messages : [];
6436
+ return sum + messages.reduce((messageSum: number, message: any) => {
6437
+ const size = kafkaValueSize(message?.value);
6438
+ return messageSum + (size ?? 0);
6439
+ }, 0);
6440
+ }, 0);
6441
+ const requestKey = uniqueTopics.length === 1
6442
+ ? `KAFKA_PUBLISH_BATCH ${uniqueTopics[0]}`
6443
+ : 'KAFKA_PUBLISH_BATCH';
6444
+ const requestPath = uniqueTopics.length === 1 ? `kafka://${uniqueTopics[0]}` : 'kafka://batch';
6445
+ const maskReq: MaskRequestContext = {
6446
+ method: 'KAFKA_PUBLISH_BATCH',
6447
+ path: requestPath,
6448
+ key: requestKey,
6449
+ };
6450
+ const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, {
6451
+ transport: 'kafka',
6452
+ topics: uniqueTopics,
6453
+ topicMessages: patchedTopicMessages.map((entry: any) => ({
6454
+ topic: sanitizeKafkaTopic(entry.topic),
6455
+ messages: (Array.isArray(entry.messages) ? entry.messages : [])
6456
+ .map((message: any) => buildKafkaMessagePayload(message)),
6457
+ })),
6458
+ messageCount,
6459
+ totalValueBytes,
6460
+ traceId: span?.traceId ?? null,
6461
+ spanId: span?.spanId ?? null,
6462
+ parentSpanId: span?.parentSpanId ?? null,
6463
+ });
6376
6464
  const requestPayload: Record<string, any> = {
6377
6465
  rid: publishRid,
6378
6466
  method: 'KAFKA_PUBLISH_BATCH',
6379
- url: uniqueTopics.length === 1 ? `kafka://${uniqueTopics[0]}` : 'kafka://batch',
6380
- path: uniqueTopics.length === 1 ? `kafka://${uniqueTopics[0]}` : 'kafka://batch',
6467
+ url: requestPath,
6468
+ path: requestPath,
6381
6469
  status: threw ? 500 : 200,
6382
6470
  durMs: Date.now() - t0,
6383
- key:
6384
- uniqueTopics.length === 1
6385
- ? `KAFKA_PUBLISH_BATCH ${uniqueTopics[0]}`
6386
- : 'KAFKA_PUBLISH_BATCH',
6471
+ key: requestKey,
6387
6472
  headers: {
6388
6473
  transport: 'kafka',
6389
6474
  topicCount: uniqueTopics.length,
6390
6475
  messageCount,
6391
6476
  },
6392
- body: {
6393
- transport: 'kafka',
6394
- topics: uniqueTopics,
6395
- messageCount,
6396
- traceId: span?.traceId ?? null,
6397
- spanId: span?.spanId ?? null,
6398
- parentSpanId: span?.parentSpanId ?? null,
6399
- },
6477
+ body: bodyMaterialization.value,
6478
+ bodyMaterialization: bodyMaterialization.skipped,
6400
6479
  respBody: threw
6401
6480
  ? { error: summarizeKafkaError(error) }
6402
6481
  : summarizeKafkaProducerResult(result),
@@ -6478,6 +6557,24 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
6478
6557
  throw err;
6479
6558
  } finally {
6480
6559
  if (!sid) return;
6560
+ const messagePayload = buildKafkaMessagePayload(message);
6561
+ const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, {
6562
+ transport: 'kafka',
6563
+ topic,
6564
+ partition,
6565
+ offset: message?.offset ?? null,
6566
+ timestamp: message?.timestamp ?? null,
6567
+ groupId: consumer?.__repro_group_id ?? null,
6568
+ parentRequestRid: runtime.requestRid,
6569
+ parentTraceId: runtime.traceId,
6570
+ parentSpanId: runtime.parentSpanId,
6571
+ messageKey: messagePayload.key,
6572
+ value: cloneKafkaTelemetryValue(messagePayload.value),
6573
+ rawValue: messagePayload.rawValue,
6574
+ valueEncoding: messagePayload.valueEncoding,
6575
+ valueBytes: messagePayload.valueBytes,
6576
+ message: messagePayload,
6577
+ });
6481
6578
  const requestPayload: Record<string, any> = {
6482
6579
  rid: consumeRid,
6483
6580
  method: 'KAFKA_CONSUME',
@@ -6487,19 +6584,8 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
6487
6584
  durMs: Date.now() - t0,
6488
6585
  key: `KAFKA_CONSUME ${topic}`,
6489
6586
  headers: normalizeKafkaHeadersForPayload(message?.headers),
6490
- body: {
6491
- transport: 'kafka',
6492
- topic,
6493
- partition,
6494
- offset: message?.offset ?? null,
6495
- timestamp: message?.timestamp ?? null,
6496
- groupId: consumer?.__repro_group_id ?? null,
6497
- parentRequestRid: runtime.requestRid,
6498
- parentTraceId: runtime.traceId,
6499
- parentSpanId: runtime.parentSpanId,
6500
- messageKey: previewKafkaValue(message?.key),
6501
- valueBytes: kafkaValueSize(message?.value),
6502
- },
6587
+ body: bodyMaterialization.value,
6588
+ bodyMaterialization: bodyMaterialization.skipped,
6503
6589
  respBody: threw ? { error: summarizeKafkaError(error) } : undefined,
6504
6590
  };
6505
6591
 
@@ -6580,6 +6666,20 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
6580
6666
  if (!sid) return;
6581
6667
  const firstOffset = messages.length ? messages[0]?.offset ?? null : null;
6582
6668
  const lastOffset = messages.length ? messages[messages.length - 1]?.offset ?? null : null;
6669
+ const firstMessagePayload = firstMessage ? buildKafkaMessagePayload(firstMessage) : null;
6670
+ const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, {
6671
+ transport: 'kafka',
6672
+ topic,
6673
+ groupId: consumer?.__repro_group_id ?? null,
6674
+ messageCount: messages.length,
6675
+ firstOffset,
6676
+ lastOffset,
6677
+ parentRequestRid: runtime.requestRid,
6678
+ parentTraceId: runtime.traceId,
6679
+ parentSpanId: runtime.parentSpanId,
6680
+ firstMessageKey: firstMessagePayload?.key ?? null,
6681
+ messages: messages.map((message: any) => buildKafkaMessagePayload(message)),
6682
+ });
6583
6683
  const requestPayload: Record<string, any> = {
6584
6684
  rid: consumeRid,
6585
6685
  method: 'KAFKA_CONSUME_BATCH',
@@ -6589,18 +6689,8 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
6589
6689
  durMs: Date.now() - t0,
6590
6690
  key: `KAFKA_CONSUME_BATCH ${topic}`,
6591
6691
  headers: firstMessage ? normalizeKafkaHeadersForPayload(firstMessage.headers) : {},
6592
- body: {
6593
- transport: 'kafka',
6594
- topic,
6595
- groupId: consumer?.__repro_group_id ?? null,
6596
- messageCount: messages.length,
6597
- firstOffset,
6598
- lastOffset,
6599
- parentRequestRid: runtime.requestRid,
6600
- parentTraceId: runtime.traceId,
6601
- parentSpanId: runtime.parentSpanId,
6602
- firstMessageKey: firstMessage ? previewKafkaValue(firstMessage.key) : null,
6603
- },
6692
+ body: bodyMaterialization.value,
6693
+ bodyMaterialization: bodyMaterialization.skipped,
6604
6694
  respBody: threw ? { error: summarizeKafkaError(error) } : undefined,
6605
6695
  };
6606
6696
 
@@ -7476,5 +7566,7 @@ export const __reproTestHooks = {
7476
7566
  getRuntimePrivacyState(cfg).policy = policy;
7477
7567
  },
7478
7568
  recordKafkaTraceEventAsyncForTest: recordKafkaTraceEventAsync,
7569
+ materializeInlinePrivacyValueAsyncForTest: materializeInlinePrivacyValueAsync,
7570
+ patchKafkaProducerInstanceForTest: patchKafkaProducerInstance,
7479
7571
  wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
7480
7572
  };