@reproapp/node-sdk 0.0.46 → 0.0.47

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
@@ -379,7 +379,11 @@ type PendingTraceEventRecord = TraceEventRecord & {
379
379
  };
380
380
 
381
381
  type TraceValueCaptureRef = {
382
- status: 'stored' | 'truncated-for-storage' | 'skipped-non-meaningful';
382
+ status:
383
+ | 'stored'
384
+ | 'truncated-for-storage'
385
+ | 'skipped-non-meaningful'
386
+ | 'failed-policy';
383
387
  valueId?: string;
384
388
  projectedKind?: string;
385
389
  };
@@ -1212,22 +1216,70 @@ type RequestCaptureScopeState = {
1212
1216
  traceBatchCount: number;
1213
1217
  dbChangeCount: number;
1214
1218
  emailEventCount: number;
1219
+ captureFailures: Array<{
1220
+ stage: string;
1221
+ target: string;
1222
+ reason: string;
1223
+ }>;
1224
+ privacyMode: 'policy' | 'fallback' | 'raw';
1225
+ privacyPolicyFingerprint?: string;
1226
+ privacyStatementCount: number;
1215
1227
  };
1216
1228
 
1217
1229
  function createRequestCaptureScope(
1218
1230
  requestRid: string,
1219
1231
  transport: RequestCaptureScopeState['transport'],
1232
+ cfg?: ReproMiddlewareConfig,
1220
1233
  ): RequestCaptureScopeState {
1234
+ const privacy = cfg ? getRuntimePrivacyState(cfg).policy ?? null : null;
1235
+ const privacyMode =
1236
+ cfg && shouldCapturePrivacyRaw(cfg)
1237
+ ? 'raw'
1238
+ : privacy
1239
+ ? 'policy'
1240
+ : 'fallback';
1221
1241
  return {
1222
1242
  requestRid,
1223
1243
  transport,
1224
- backendRequestCount: 0,
1244
+ // Every scope represents one captured transport request. This is an
1245
+ // observed expectation, not a count of successful enqueue operations.
1246
+ backendRequestCount: 1,
1225
1247
  traceBatchCount: 0,
1226
1248
  dbChangeCount: 0,
1227
1249
  emailEventCount: 0,
1250
+ captureFailures: [],
1251
+ privacyMode,
1252
+ privacyPolicyFingerprint: privacy?.fingerprint,
1253
+ privacyStatementCount: privacy?.statements.length ?? 0,
1228
1254
  };
1229
1255
  }
1230
1256
 
1257
+ function recordRequestCaptureFailure(
1258
+ scope: RequestCaptureScopeState | null | undefined,
1259
+ stage: string,
1260
+ target: string,
1261
+ reason: string,
1262
+ ): void {
1263
+ if (!scope) return;
1264
+ const normalized = {
1265
+ stage: String(stage || 'capture').slice(0, 80),
1266
+ target: String(target || 'unknown').slice(0, 120),
1267
+ reason: String(reason || 'capture_failed').slice(0, 120),
1268
+ };
1269
+ if (
1270
+ scope.captureFailures.some((failure) =>
1271
+ failure.stage === normalized.stage &&
1272
+ failure.target === normalized.target &&
1273
+ failure.reason === normalized.reason,
1274
+ )
1275
+ ) {
1276
+ return;
1277
+ }
1278
+ if (scope.captureFailures.length < 25) {
1279
+ scope.captureFailures.push(normalized);
1280
+ }
1281
+ }
1282
+
1231
1283
  function buildRequestCaptureManifestPayload(
1232
1284
  scope: RequestCaptureScopeState,
1233
1285
  traceBatchTotal: number,
@@ -1255,6 +1307,20 @@ function buildRequestCaptureManifestPayload(
1255
1307
  to: expectedTraceBatches - 1,
1256
1308
  }
1257
1309
  : undefined,
1310
+ privacy: {
1311
+ mode: scope.privacyMode,
1312
+ status:
1313
+ scope.captureFailures.length > 0
1314
+ ? 'failed'
1315
+ : scope.privacyMode === 'raw'
1316
+ ? 'bypassed'
1317
+ : 'applied',
1318
+ policyFingerprint: scope.privacyPolicyFingerprint,
1319
+ statementCount: scope.privacyStatementCount,
1320
+ },
1321
+ captureFailures: scope.captureFailures.length
1322
+ ? scope.captureFailures.map((failure) => ({ ...failure }))
1323
+ : undefined,
1258
1324
  closedAt,
1259
1325
  };
1260
1326
  }
@@ -1439,12 +1505,6 @@ const SDK_BACKGROUND_MAX_DEFER_MS = (() => {
1439
1505
  return Number.POSITIVE_INFINITY;
1440
1506
  })();
1441
1507
 
1442
- const SDK_BACKGROUND_WORK_TIMEOUT_MS = (() => {
1443
- const env = Number(process.env.REPRO_SDK_BACKGROUND_WORK_TIMEOUT_MS);
1444
- if (Number.isFinite(env) && env >= 0) return Math.trunc(env);
1445
- return 20000;
1446
- })();
1447
-
1448
1508
  const TRACE_MATERIALIZE_OFFTHREAD_ENABLED = (() => {
1449
1509
  const env = String(process.env.REPRO_TRACE_MATERIALIZE_OFFTHREAD || '').trim().toLowerCase();
1450
1510
  if (!env) return true;
@@ -1774,24 +1834,9 @@ function scheduleSdkBackgroundDrain(delayMs = 0): void {
1774
1834
  }
1775
1835
 
1776
1836
  async function runSdkBackgroundWork(work: () => Promise<void> | void): Promise<void> {
1777
- if (!Number.isFinite(SDK_BACKGROUND_WORK_TIMEOUT_MS) || SDK_BACKGROUND_WORK_TIMEOUT_MS <= 0) {
1778
- await work();
1779
- return;
1780
- }
1781
-
1782
- let timeout: NodeJS.Timeout | null = null;
1783
- const workPromise = Promise.resolve()
1837
+ await Promise.resolve()
1784
1838
  .then(work)
1785
1839
  .catch(() => undefined);
1786
- const timeoutPromise = new Promise<void>((resolve) => {
1787
- timeout = setTimeout(resolve, SDK_BACKGROUND_WORK_TIMEOUT_MS);
1788
- try { (timeout as any).unref?.(); } catch {}
1789
- });
1790
-
1791
- await Promise.race([workPromise, timeoutPromise]);
1792
- if (timeout) {
1793
- try { clearTimeout(timeout); } catch {}
1794
- }
1795
1840
  }
1796
1841
 
1797
1842
  async function drainQueuedIngestBeforeBackgroundWork(): Promise<void> {
@@ -2783,7 +2828,7 @@ const TRACE_MAX_EVENTS_PER_REQUEST = (() => {
2783
2828
  const TRACE_MAX_BATCHES_PER_REQUEST = (() => {
2784
2829
  const env = Number(process.env.REPRO_TRACE_MAX_BATCHES_PER_REQUEST);
2785
2830
  if (Number.isFinite(env) && env > 0) return Math.trunc(env);
2786
- return 8;
2831
+ return Math.ceil(TRACE_MAX_EVENTS_PER_REQUEST / TRACE_BATCH_SIZE);
2787
2832
  })();
2788
2833
  const TRACE_FLUSH_DELAY_MS = 20;
2789
2834
  // Choose how to order trace events in payloads.
@@ -3357,6 +3402,7 @@ async function materializePendingTraceEvents(
3357
3402
  maskReq: MaskRequestContext;
3358
3403
  masking: NormalizedMaskingConfig | null;
3359
3404
  resolvePrivacy: () => NormalizedRuntimePrivacyPolicy | null;
3405
+ captureScope?: RequestCaptureScopeState | null;
3360
3406
  },
3361
3407
  ): Promise<TraceEventRecord[]> {
3362
3408
  for (const evt of events) {
@@ -3403,7 +3449,7 @@ async function materializePendingTraceEvents(
3403
3449
  previewValue: evt.args,
3404
3450
  capture: captureContext,
3405
3451
  metadata: traceValueMetadata,
3406
- });
3452
+ }, params.captureScope);
3407
3453
  if (argsValueCapture?.entry) {
3408
3454
  attachTraceValueEntry(evt, argsValueCapture.entry);
3409
3455
  }
@@ -3432,7 +3478,7 @@ async function materializePendingTraceEvents(
3432
3478
  previewValue: evt.returnValue,
3433
3479
  capture: captureContext,
3434
3480
  metadata: traceValueMetadata,
3435
- });
3481
+ }, params.captureScope);
3436
3482
  if (returnValueCapture?.entry) {
3437
3483
  attachTraceValueEntry(evt, returnValueCapture.entry);
3438
3484
  }
@@ -3461,7 +3507,7 @@ async function materializePendingTraceEvents(
3461
3507
  previewValue: evt.error,
3462
3508
  capture: captureContext,
3463
3509
  metadata: traceValueMetadata,
3464
- });
3510
+ }, params.captureScope);
3465
3511
  if (errorValueCapture?.entry) {
3466
3512
  attachTraceValueEntry(evt, errorValueCapture.entry);
3467
3513
  }
@@ -3479,7 +3525,20 @@ function stripPendingTraceValues(
3479
3525
  reason: string,
3480
3526
  ): TraceEventRecord[] {
3481
3527
  for (const evt of events) {
3482
- if (!evt.__reproPending) continue;
3528
+ const pending = evt.__reproPending;
3529
+ if (!pending) continue;
3530
+ if (pending.argsRaw !== undefined) {
3531
+ evt.args = makeOmittedValue(reason);
3532
+ evt.argsValueCapture = { status: 'failed-policy' };
3533
+ }
3534
+ if (pending.returnValueRaw !== undefined) {
3535
+ evt.returnValue = makeOmittedValue(reason);
3536
+ evt.returnValueCapture = { status: 'failed-policy' };
3537
+ }
3538
+ if (pending.errorRaw !== undefined) {
3539
+ evt.error = makeOmittedValue(reason);
3540
+ evt.errorValueCapture = { status: 'failed-policy' };
3541
+ }
3483
3542
  delete evt.__reproPending;
3484
3543
  }
3485
3544
  return events;
@@ -3496,6 +3555,7 @@ async function materializePendingTraceEventsAsync(
3496
3555
  maskReq: MaskRequestContext;
3497
3556
  masking: NormalizedMaskingConfig | null;
3498
3557
  resolvePrivacy: () => NormalizedRuntimePrivacyPolicy | null;
3558
+ captureScope?: RequestCaptureScopeState | null;
3499
3559
  },
3500
3560
  ): Promise<TraceEventRecord[]> {
3501
3561
  if (!events.some((evt) => evt.__reproPending)) {
@@ -3510,6 +3570,12 @@ async function materializePendingTraceEventsAsync(
3510
3570
  resolvePrivacy: () => privacy,
3511
3571
  });
3512
3572
  } catch {
3573
+ recordRequestCaptureFailure(
3574
+ params.captureScope ?? getCtx().captureScope,
3575
+ 'privacy.trace',
3576
+ 'trace.values',
3577
+ 'main_thread_materialization_failed',
3578
+ );
3513
3579
  return stripPendingTraceValues(events, 'main_thread_failed');
3514
3580
  }
3515
3581
  }
@@ -3528,16 +3594,24 @@ async function materializePendingTraceEventsAsync(
3528
3594
  fullValueCaptureEnabled: __FULL_VALUE_CAPTURE_ENABLED,
3529
3595
  });
3530
3596
  } catch (err) {
3531
- if (isTraceMaterializerTimeoutError(err)) {
3532
- return stripPendingTraceValues(events, 'worker_timeout');
3533
- }
3534
3597
  try {
3535
3598
  return await materializePendingTraceEvents(events, {
3536
3599
  ...params,
3537
3600
  resolvePrivacy: () => privacy,
3538
3601
  });
3539
3602
  } catch {
3540
- return stripPendingTraceValues(events, 'worker_failed');
3603
+ recordRequestCaptureFailure(
3604
+ params.captureScope ?? getCtx().captureScope,
3605
+ 'privacy.trace',
3606
+ 'trace.values',
3607
+ isTraceMaterializerTimeoutError(err)
3608
+ ? 'worker_timeout_fallback_failed'
3609
+ : 'worker_and_fallback_failed',
3610
+ );
3611
+ return stripPendingTraceValues(
3612
+ events,
3613
+ isTraceMaterializerTimeoutError(err) ? 'worker_timeout_fallback_failed' : 'worker_failed',
3614
+ );
3541
3615
  }
3542
3616
  }
3543
3617
  }
@@ -4137,6 +4211,7 @@ function materializeInlinePrivacyValue(
4137
4211
  masking: NormalizedMaskingConfig | null,
4138
4212
  privacy: NormalizedRuntimePrivacyPolicy | null,
4139
4213
  db?: RuntimePrivacyDbContext | null,
4214
+ captureScope?: RequestCaptureScopeState | null,
4140
4215
  ): InlinePrivacyMaterializationResult {
4141
4216
  if (value === undefined) {
4142
4217
  return { value };
@@ -4149,6 +4224,12 @@ function materializeInlinePrivacyValue(
4149
4224
  const valueAfterPrivacy = applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db);
4150
4225
  return { value: valueAfterPrivacy };
4151
4226
  } catch {
4227
+ recordRequestCaptureFailure(
4228
+ captureScope ?? getCtx().captureScope,
4229
+ 'privacy.inline',
4230
+ target,
4231
+ 'privacy_transform_failed',
4232
+ );
4152
4233
  return {
4153
4234
  value: undefined,
4154
4235
  skipped: {
@@ -4169,6 +4250,7 @@ async function materializeInlinePrivacyValueAsync(
4169
4250
  masking: NormalizedMaskingConfig | null,
4170
4251
  privacy: NormalizedRuntimePrivacyPolicy | null,
4171
4252
  db?: RuntimePrivacyDbContext | null,
4253
+ captureScope?: RequestCaptureScopeState | null,
4172
4254
  ): Promise<InlinePrivacyMaterializationResult> {
4173
4255
  if (value === undefined) {
4174
4256
  return { value };
@@ -4181,6 +4263,12 @@ async function materializeInlinePrivacyValueAsync(
4181
4263
  const valueAfterPrivacy = await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db);
4182
4264
  return { value: valueAfterPrivacy };
4183
4265
  } catch {
4266
+ recordRequestCaptureFailure(
4267
+ captureScope ?? getCtx().captureScope,
4268
+ 'privacy.inline',
4269
+ target,
4270
+ 'privacy_transform_failed',
4271
+ );
4184
4272
  return {
4185
4273
  value: undefined,
4186
4274
  skipped: {
@@ -5185,6 +5273,13 @@ function chunkTraceEventsForTransport(
5185
5273
  if (sizedBatches.length >= TRACE_MAX_BATCHES_PER_REQUEST) {
5186
5274
  break;
5187
5275
  }
5276
+ if (current.length >= TRACE_BATCH_SIZE) {
5277
+ sizedBatches.push(current);
5278
+ current = [];
5279
+ if (sizedBatches.length >= TRACE_MAX_BATCHES_PER_REQUEST) {
5280
+ break;
5281
+ }
5282
+ }
5188
5283
  const candidate = current.concat(event);
5189
5284
  const batchIndex = sizedBatches.length;
5190
5285
  const estimatedBytes = estimateTraceBatchSerializedBytes(
@@ -5416,8 +5511,20 @@ async function maybeCaptureRequestValueAsync(
5416
5511
  capture: FullValueCaptureContext;
5417
5512
  },
5418
5513
  sink: TraceValueBatchEntry[],
5514
+ captureScope?: RequestCaptureScopeState | null,
5419
5515
  ): Promise<TraceValueCaptureRef | undefined> {
5420
- const result = await createCapturedValueEntryAsync(params);
5516
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5517
+ try {
5518
+ result = await createCapturedValueEntryAsync(params);
5519
+ } catch {
5520
+ recordRequestCaptureFailure(
5521
+ captureScope ?? getCtx().captureScope,
5522
+ 'privacy.full_value',
5523
+ params.target,
5524
+ 'privacy_transform_failed',
5525
+ );
5526
+ return { status: 'failed-policy' };
5527
+ }
5421
5528
  if (!result) return undefined;
5422
5529
  if (result.entry) {
5423
5530
  sink.push(result.entry);
@@ -5445,8 +5552,19 @@ async function maybeCaptureTraceValueAsync(
5445
5552
  capture: FullValueCaptureContext;
5446
5553
  metadata?: Partial<TraceValueBatchEntry>;
5447
5554
  },
5555
+ captureScope?: RequestCaptureScopeState | null,
5448
5556
  ): Promise<{ captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined> {
5449
- return await createCapturedValueEntryAsync(params);
5557
+ try {
5558
+ return await createCapturedValueEntryAsync(params);
5559
+ } catch {
5560
+ recordRequestCaptureFailure(
5561
+ captureScope ?? getCtx().captureScope,
5562
+ 'privacy.full_value',
5563
+ params.target,
5564
+ 'privacy_transform_failed',
5565
+ );
5566
+ return { captureRef: { status: 'failed-policy' } };
5567
+ }
5450
5568
  }
5451
5569
 
5452
5570
  function maybeCaptureDbValue(
@@ -5474,8 +5592,20 @@ async function maybeCaptureDbValueAsync(
5474
5592
  capture: FullValueCaptureContext;
5475
5593
  },
5476
5594
  sink: TraceValueBatchEntry[],
5595
+ captureScope?: RequestCaptureScopeState | null,
5477
5596
  ): Promise<TraceValueCaptureRef | undefined> {
5478
- const result = await createCapturedValueEntryAsync(params);
5597
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5598
+ try {
5599
+ result = await createCapturedValueEntryAsync(params);
5600
+ } catch {
5601
+ recordRequestCaptureFailure(
5602
+ captureScope ?? getCtx().captureScope,
5603
+ 'privacy.full_value',
5604
+ params.target,
5605
+ 'privacy_transform_failed',
5606
+ );
5607
+ return { status: 'failed-policy' };
5608
+ }
5479
5609
  if (!result) return undefined;
5480
5610
  if (result.entry) {
5481
5611
  sink.push(result.entry);
@@ -5511,15 +5641,27 @@ async function maybeCaptureDbValueFromCandidatesAsync(
5511
5641
  capture: FullValueCaptureContext;
5512
5642
  },
5513
5643
  sink: TraceValueBatchEntry[],
5644
+ captureScope?: RequestCaptureScopeState | null,
5514
5645
  ): Promise<TraceValueCaptureRef | undefined> {
5515
5646
  let skippedRef: TraceValueCaptureRef | undefined;
5516
5647
  for (const rawValue of params.rawValues) {
5517
- const result = await createCapturedValueEntryAsync({
5518
- target: params.target,
5519
- rawValue,
5520
- previewValue: params.previewValue,
5521
- capture: params.capture,
5522
- });
5648
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5649
+ try {
5650
+ result = await createCapturedValueEntryAsync({
5651
+ target: params.target,
5652
+ rawValue,
5653
+ previewValue: params.previewValue,
5654
+ capture: params.capture,
5655
+ });
5656
+ } catch {
5657
+ recordRequestCaptureFailure(
5658
+ captureScope ?? getCtx().captureScope,
5659
+ 'privacy.full_value',
5660
+ params.target,
5661
+ 'privacy_transform_failed',
5662
+ );
5663
+ return { status: 'failed-policy' };
5664
+ }
5523
5665
  if (!result) continue;
5524
5666
  if (result.entry) {
5525
5667
  sink.push(result.entry);
@@ -5827,7 +5969,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5827
5969
 
5828
5970
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
5829
5971
  const rid = nextRequestId(requestEpochMs);
5830
- const captureScope = createRequestCaptureScope(rid, 'http');
5972
+ const captureScope = createRequestCaptureScope(rid, 'http', captureCfg);
5831
5973
  emitRequestCaptureStart(captureCfg, sid, aid, captureScope, requestEpochMs);
5832
5974
  const t0 = requestStartRaw;
5833
5975
  const url = (req as any).originalUrl || req.url || '/';
@@ -6029,9 +6171,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6029
6171
  endpointTraceCtx,
6030
6172
  masking,
6031
6173
  activePrivacy,
6174
+ undefined,
6175
+ captureScope,
6032
6176
  );
6033
6177
  const requestBody = requestBodyMaterialization.value;
6034
- const requestParams = await applyPrivacyThenMaskAsync(
6178
+ const requestParamsMaterialization = await materializeInlinePrivacyValueAsync(
6035
6179
  'request.params',
6036
6180
  sanitizeRequestSnapshot((req as any).params),
6037
6181
  captureCfg,
@@ -6039,8 +6183,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6039
6183
  endpointTraceCtx,
6040
6184
  masking,
6041
6185
  activePrivacy,
6186
+ undefined,
6187
+ captureScope,
6042
6188
  );
6043
- const requestQuery = await applyPrivacyThenMaskAsync(
6189
+ const requestParams = requestParamsMaterialization.value;
6190
+ const requestQueryMaterialization = await materializeInlinePrivacyValueAsync(
6044
6191
  'request.query',
6045
6192
  sanitizeRequestSnapshot((req as any).query),
6046
6193
  captureCfg,
@@ -6048,8 +6195,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6048
6195
  endpointTraceCtx,
6049
6196
  masking,
6050
6197
  activePrivacy,
6198
+ undefined,
6199
+ captureScope,
6051
6200
  );
6052
- const maskedHeaders = await applyPrivacyThenMaskAsync(
6201
+ const requestQuery = requestQueryMaterialization.value;
6202
+ const requestHeadersMaterialization = await materializeInlinePrivacyValueAsync(
6053
6203
  'request.headers',
6054
6204
  requestHeaders,
6055
6205
  captureCfg,
@@ -6057,7 +6207,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6057
6207
  endpointTraceCtx,
6058
6208
  masking,
6059
6209
  activePrivacy,
6210
+ undefined,
6211
+ captureScope,
6060
6212
  );
6213
+ const maskedHeaders = requestHeadersMaterialization.value;
6061
6214
  const responseBodyMaterialization = capturedBody === undefined
6062
6215
  ? { value: undefined }
6063
6216
  : limitRawInlinePrivacyValue('response.body', capturedBody)
@@ -6069,6 +6222,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6069
6222
  endpointTraceCtx,
6070
6223
  masking,
6071
6224
  activePrivacy,
6225
+ undefined,
6226
+ captureScope,
6072
6227
  );
6073
6228
  const responseBody = responseBodyMaterialization.value;
6074
6229
  const requestValueEntries: TraceValueBatchEntry[] = [];
@@ -6086,7 +6241,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6086
6241
  masking,
6087
6242
  privacy: activePrivacy,
6088
6243
  },
6089
- }, requestValueEntries);
6244
+ }, requestValueEntries, captureScope);
6090
6245
  const paramsValueCapture = await maybeCaptureRequestValueAsync({
6091
6246
  target: 'request.params',
6092
6247
  rawValue: (req as any).params,
@@ -6099,7 +6254,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6099
6254
  masking,
6100
6255
  privacy: activePrivacy,
6101
6256
  },
6102
- }, requestValueEntries);
6257
+ }, requestValueEntries, captureScope);
6103
6258
  const queryValueCapture = await maybeCaptureRequestValueAsync({
6104
6259
  target: 'request.query',
6105
6260
  rawValue: (req as any).query,
@@ -6112,7 +6267,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6112
6267
  masking,
6113
6268
  privacy: activePrivacy,
6114
6269
  },
6115
- }, requestValueEntries);
6270
+ }, requestValueEntries, captureScope);
6116
6271
  const headersValueCapture = await maybeCaptureRequestValueAsync({
6117
6272
  target: 'request.headers',
6118
6273
  rawValue: req.headers,
@@ -6125,7 +6280,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6125
6280
  masking,
6126
6281
  privacy: activePrivacy,
6127
6282
  },
6128
- }, requestValueEntries);
6283
+ }, requestValueEntries, captureScope);
6129
6284
  const respBodyValueCapture = capturedBody === undefined
6130
6285
  ? undefined
6131
6286
  : await maybeCaptureRequestValueAsync({
@@ -6140,7 +6295,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6140
6295
  masking,
6141
6296
  privacy: activePrivacy,
6142
6297
  },
6143
- }, requestValueEntries);
6298
+ }, requestValueEntries, captureScope);
6144
6299
 
6145
6300
  const responseDurationMs =
6146
6301
  finishedAt !== null && Number.isFinite(finishedAt)
@@ -6201,9 +6356,43 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6201
6356
  t: requestEpochMs,
6202
6357
  }]
6203
6358
  });
6204
- captureScope.backendRequestCount += 1;
6205
6359
  } catch {
6206
- // never break user code
6360
+ recordRequestCaptureFailure(
6361
+ captureScope,
6362
+ 'request.capture',
6363
+ 'backend_request',
6364
+ 'request_materialization_failed',
6365
+ );
6366
+ // Preserve non-sensitive request metadata so the manifest cannot
6367
+ // silently turn a failed policy capture into a zero-request scope.
6368
+ post(captureCfg, sid, {
6369
+ entries: [{
6370
+ actionId: aid,
6371
+ request: {
6372
+ rid,
6373
+ method: req.method,
6374
+ url,
6375
+ path,
6376
+ status: res.statusCode,
6377
+ durMs:
6378
+ finishedAt !== null && Number.isFinite(finishedAt)
6379
+ ? Math.max(0, finishedAt - t0)
6380
+ : Date.now() - t0,
6381
+ key,
6382
+ bodyMaterialization: {
6383
+ status: 'skipped',
6384
+ reason: 'request_materialization_failed',
6385
+ target: 'request.body',
6386
+ },
6387
+ respBodyMaterialization: {
6388
+ status: 'skipped',
6389
+ reason: 'request_materialization_failed',
6390
+ target: 'response.body',
6391
+ },
6392
+ },
6393
+ t: requestEpochMs,
6394
+ }],
6395
+ });
6207
6396
  }
6208
6397
  };
6209
6398
 
@@ -6326,6 +6515,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6326
6515
  maskReq,
6327
6516
  masking,
6328
6517
  resolvePrivacy,
6518
+ captureScope,
6329
6519
  });
6330
6520
  const baseEvents = balanceTraceEvents(materializedEvents.slice());
6331
6521
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
@@ -6344,12 +6534,21 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6344
6534
  }
6345
6535
 
6346
6536
  } catch {
6537
+ recordRequestCaptureFailure(
6538
+ captureScope,
6539
+ 'trace.capture',
6540
+ 'trace.batches',
6541
+ 'trace_capture_failed',
6542
+ );
6347
6543
  captureScope.traceBatchCount = 0;
6348
6544
  traceBatches = [];
6349
- } finally {
6350
- emitRequestCaptureManifest(traceBatches.length);
6351
6545
  }
6352
6546
 
6547
+ // Finalize policy/materialization status before publishing the
6548
+ // high-priority manifest. The manifest must lead its evidence
6549
+ // on the wire so a large trace queue cannot starve completion.
6550
+ emitRequestCaptureManifest(traceBatches.length);
6551
+
6353
6552
  try {
6354
6553
  for (let i = 0; i < traceBatches.length; i++) {
6355
6554
  const batch = traceBatches[i];
@@ -6370,7 +6569,12 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6370
6569
  });
6371
6570
  }
6372
6571
  } catch {
6373
- // never break user code
6572
+ recordRequestCaptureFailure(
6573
+ captureScope,
6574
+ 'trace.enqueue',
6575
+ 'trace.batches',
6576
+ 'trace_enqueue_failed',
6577
+ );
6374
6578
  }
6375
6579
  };
6376
6580
  }
@@ -6971,6 +7175,11 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
6971
7175
  const ctx = getCtx() as Ctx;
6972
7176
  const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
6973
7177
  if (!shouldCaptureDbSpan(spanContext)) return;
7178
+ if (sid && ctx.captureScope) {
7179
+ // Count the observed database operation before asynchronous privacy
7180
+ // materialization. A later failure must remain visible to completeness.
7181
+ ctx.captureScope.dbChangeCount += 1;
7182
+ }
6974
7183
  const runCtx: Ctx = {
6975
7184
  sid: ctx.sid,
6976
7185
  aid: ctx.aid,
@@ -6982,7 +7191,14 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
6982
7191
  };
6983
7192
  scheduleSdkBackgroundWork(async () => {
6984
7193
  await als.run(runCtx, async () => {
6985
- await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => undefined);
7194
+ await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => {
7195
+ recordRequestCaptureFailure(
7196
+ runCtx.captureScope,
7197
+ 'db.capture',
7198
+ 'db.change',
7199
+ 'db_capture_failed',
7200
+ );
7201
+ });
6986
7202
  });
6987
7203
  }, { priority: 'high' });
6988
7204
  }
@@ -7097,8 +7313,13 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
7097
7313
  t: payload.t,
7098
7314
  }]
7099
7315
  });
7100
- if (enqueued && ctx.captureScope) {
7101
- ctx.captureScope.dbChangeCount += 1;
7316
+ if (!enqueued) {
7317
+ recordRequestCaptureFailure(
7318
+ ctx.captureScope,
7319
+ 'db.enqueue',
7320
+ 'db.change',
7321
+ 'db_enqueue_failed',
7322
+ );
7102
7323
  }
7103
7324
  }
7104
7325
 
@@ -7534,6 +7755,7 @@ async function materializeKafkaRequestBody(
7534
7755
  cfg: ReproMiddlewareConfig,
7535
7756
  maskReq: MaskRequestContext,
7536
7757
  body: Record<string, any>,
7758
+ captureScope?: RequestCaptureScopeState | null,
7537
7759
  ): Promise<InlinePrivacyMaterializationResult> {
7538
7760
  return materializeInlinePrivacyValueAsync(
7539
7761
  'request.body',
@@ -7543,6 +7765,8 @@ async function materializeKafkaRequestBody(
7543
7765
  null,
7544
7766
  normalizeMaskingConfig(cfg.masking),
7545
7767
  getRuntimePrivacyState(cfg).policy ?? null,
7768
+ undefined,
7769
+ captureScope,
7546
7770
  );
7547
7771
  }
7548
7772
 
@@ -7882,22 +8106,31 @@ function scheduleKafkaTraceFlush(
7882
8106
  const evt = buildPendingKafkaTraceEvent(rawTraceEvent);
7883
8107
  if (evt) pendingEvents.push(evt);
7884
8108
  }
7885
- if (!pendingEvents.length) return;
7886
- const masking = normalizeMaskingConfig(cfg.masking);
7887
- const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
7888
- const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
7889
- cfg,
7890
- maskReq,
7891
- masking,
7892
- resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
7893
- });
7894
- const entries = buildKafkaTraceEntries(actionId, requestRid, materializedEvents);
8109
+ let entries: LegacyEntry[] = [];
8110
+ try {
8111
+ if (pendingEvents.length) {
8112
+ const masking = normalizeMaskingConfig(cfg.masking);
8113
+ const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
8114
+ const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
8115
+ cfg,
8116
+ maskReq,
8117
+ masking,
8118
+ resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
8119
+ captureScope,
8120
+ });
8121
+ entries = buildKafkaTraceEntries(actionId, requestRid, materializedEvents);
8122
+ }
8123
+ } catch {
8124
+ recordRequestCaptureFailure(
8125
+ captureScope,
8126
+ 'trace.capture',
8127
+ 'trace.batches',
8128
+ 'trace_capture_failed',
8129
+ );
8130
+ }
7895
8131
  if (captureScope) {
7896
8132
  captureScope.traceBatchCount = entries.length;
7897
8133
  }
7898
- if (entries.length) {
7899
- post(cfg, sid, { entries });
7900
- }
7901
8134
  if (captureScope) {
7902
8135
  post(cfg, sid, {
7903
8136
  entries: [{
@@ -7909,7 +8142,10 @@ function scheduleKafkaTraceFlush(
7909
8142
  ),
7910
8143
  t: alignedNow(),
7911
8144
  }],
7912
- });
8145
+ }, { priority: 'high' });
8146
+ }
8147
+ if (entries.length) {
8148
+ post(cfg, sid, { entries });
7913
8149
  }
7914
8150
  });
7915
8151
  }, { priority: 'high' });
@@ -7928,7 +8164,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
7928
8164
  const aid = ctx.aid ?? fallback.aid ?? null;
7929
8165
  const span = captureSpanContextFromTracer();
7930
8166
  const publishRid = nextRequestId(alignedNow());
7931
- const captureScope = createRequestCaptureScope(publishRid, 'kafka');
8167
+ const captureScope = createRequestCaptureScope(publishRid, 'kafka', cfg);
7932
8168
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7933
8169
  const payloadObj = payload && typeof payload === 'object' ? payload : {};
7934
8170
  const topic = sanitizeKafkaTopic((payloadObj as any).topic);
@@ -7987,7 +8223,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
7987
8223
  traceId: span?.traceId ?? null,
7988
8224
  spanId: span?.spanId ?? null,
7989
8225
  parentSpanId: span?.parentSpanId ?? null,
7990
- });
8226
+ }, captureScope);
7991
8227
 
7992
8228
  const requestPayload: Record<string, any> = {
7993
8229
  rid: publishRid,
@@ -8018,7 +8254,6 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8018
8254
  },
8019
8255
  ],
8020
8256
  });
8021
- captureScope.backendRequestCount += 1;
8022
8257
  post(cfg, sid, {
8023
8258
  entries: [{
8024
8259
  actionId: aid ?? null,
@@ -8043,7 +8278,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8043
8278
  const aid = ctx.aid ?? fallback.aid ?? null;
8044
8279
  const span = captureSpanContextFromTracer();
8045
8280
  const publishRid = nextRequestId(alignedNow());
8046
- const captureScope = createRequestCaptureScope(publishRid, 'kafka');
8281
+ const captureScope = createRequestCaptureScope(publishRid, 'kafka', cfg);
8047
8282
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8048
8283
 
8049
8284
  const payloadObj = payload && typeof payload === 'object' ? payload : {};
@@ -8137,7 +8372,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8137
8372
  traceId: span?.traceId ?? null,
8138
8373
  spanId: span?.spanId ?? null,
8139
8374
  parentSpanId: span?.parentSpanId ?? null,
8140
- });
8375
+ }, captureScope);
8141
8376
  const requestPayload: Record<string, any> = {
8142
8377
  rid: publishRid,
8143
8378
  method: 'KAFKA_PUBLISH_BATCH',
@@ -8167,7 +8402,6 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8167
8402
  },
8168
8403
  ],
8169
8404
  });
8170
- captureScope.backendRequestCount += 1;
8171
8405
  post(cfg, sid, {
8172
8406
  entries: [{
8173
8407
  actionId: aid ?? null,
@@ -8194,7 +8428,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8194
8428
  const aid = runtime.aid;
8195
8429
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
8196
8430
  const traceId = consumeRid;
8197
- const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
8431
+ const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8198
8432
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8199
8433
  const rawTraceEvents: any[] = [];
8200
8434
  const maskReq: MaskRequestContext = {
@@ -8266,7 +8500,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8266
8500
  valueEncoding: messagePayload.valueEncoding,
8267
8501
  valueBytes: messagePayload.valueBytes,
8268
8502
  message: messagePayload,
8269
- });
8503
+ }, captureScope);
8270
8504
  const requestPayload: Record<string, any> = {
8271
8505
  rid: consumeRid,
8272
8506
  method: 'KAFKA_CONSUME',
@@ -8288,7 +8522,6 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8288
8522
  t: alignedNow(),
8289
8523
  }],
8290
8524
  });
8291
- captureScope.backendRequestCount += 1;
8292
8525
  scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
8293
8526
  }
8294
8527
  };
@@ -8305,7 +8538,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8305
8538
  const aid = runtime.aid;
8306
8539
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
8307
8540
  const traceId = consumeRid;
8308
- const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
8541
+ const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8309
8542
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8310
8543
  const rawTraceEvents: any[] = [];
8311
8544
  const maskReq: MaskRequestContext = {
@@ -8375,7 +8608,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8375
8608
  parentSpanId: runtime.parentSpanId,
8376
8609
  firstMessageKey: firstMessagePayload?.key ?? null,
8377
8610
  messages: messages.map((message: any) => buildKafkaMessagePayload(message)),
8378
- });
8611
+ }, captureScope);
8379
8612
  const requestPayload: Record<string, any> = {
8380
8613
  rid: consumeRid,
8381
8614
  method: 'KAFKA_CONSUME_BATCH',
@@ -8397,7 +8630,6 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8397
8630
  t: alignedNow(),
8398
8631
  }],
8399
8632
  });
8400
- captureScope.backendRequestCount += 1;
8401
8633
  scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
8402
8634
  }
8403
8635
  };
@@ -8820,24 +9052,28 @@ function scheduleAmqpTraceFlush(
8820
9052
  }
8821
9053
 
8822
9054
  let entries: LegacyEntry[] = [];
8823
- if (pendingEvents.length) {
8824
- const masking = normalizeMaskingConfig(cfg.masking);
8825
- const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
8826
- const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
8827
- cfg,
8828
- maskReq,
8829
- masking,
8830
- resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
8831
- });
8832
- entries = buildAmqpTraceEntries(actionId, requestRid, materializedEvents);
8833
- captureScope.traceBatchCount = entries.length;
8834
- if (entries.length) {
8835
- post(cfg, sid, { entries });
9055
+ try {
9056
+ if (pendingEvents.length) {
9057
+ const masking = normalizeMaskingConfig(cfg.masking);
9058
+ const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
9059
+ const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
9060
+ cfg,
9061
+ maskReq,
9062
+ masking,
9063
+ resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
9064
+ captureScope,
9065
+ });
9066
+ entries = buildAmqpTraceEntries(actionId, requestRid, materializedEvents);
8836
9067
  }
8837
- } else {
8838
- captureScope.traceBatchCount = 0;
9068
+ } catch {
9069
+ recordRequestCaptureFailure(
9070
+ captureScope,
9071
+ 'trace.capture',
9072
+ 'trace.batches',
9073
+ 'trace_capture_failed',
9074
+ );
8839
9075
  }
8840
-
9076
+ captureScope.traceBatchCount = entries.length;
8841
9077
  post(cfg, sid, {
8842
9078
  entries: [{
8843
9079
  actionId: actionId ?? null,
@@ -8848,7 +9084,10 @@ function scheduleAmqpTraceFlush(
8848
9084
  ),
8849
9085
  t: alignedNow(),
8850
9086
  }],
8851
- });
9087
+ }, { priority: 'high' });
9088
+ if (entries.length) {
9089
+ post(cfg, sid, { entries });
9090
+ }
8852
9091
  });
8853
9092
  }, { priority: 'high' });
8854
9093
  }
@@ -8920,7 +9159,12 @@ function scheduleAmqpPublishCapture(params: {
8920
9159
  };
8921
9160
 
8922
9161
  scheduleSdkBackgroundWork(async () => {
8923
- const bodyMaterialization = await materializeKafkaRequestBody(params.cfg, maskReq, body);
9162
+ const bodyMaterialization = await materializeKafkaRequestBody(
9163
+ params.cfg,
9164
+ maskReq,
9165
+ body,
9166
+ params.captureScope,
9167
+ );
8924
9168
  const requestPayload: Record<string, any> = {
8925
9169
  rid: params.publishRid,
8926
9170
  method: params.method,
@@ -8951,7 +9195,6 @@ function scheduleAmqpPublishCapture(params: {
8951
9195
  t: alignedNow(),
8952
9196
  }],
8953
9197
  });
8954
- params.captureScope.backendRequestCount += 1;
8955
9198
  post(params.cfg, params.sid!, {
8956
9199
  entries: [{
8957
9200
  actionId: params.aid ?? null,
@@ -9013,7 +9256,7 @@ function patchAmqplibPublishMethod(
9013
9256
  : `amqp://queue/${queue}`;
9014
9257
  const key = `${method} ${methodName === 'publish' ? `${exchange || 'default'}/${routingKey}` : queue}`;
9015
9258
  const content = methodName === 'publish' ? args[2] : args[1];
9016
- const captureScope = createRequestCaptureScope(publishRid, 'amqp');
9259
+ const captureScope = createRequestCaptureScope(publishRid, 'amqp', cfg);
9017
9260
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9018
9261
  const t0 = Date.now();
9019
9262
  let finished = false;
@@ -9103,7 +9346,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9103
9346
  const deliveryTag = message?.fields?.deliveryTag ?? alignedNow();
9104
9347
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
9105
9348
  const traceId = consumeRid;
9106
- const captureScope = createRequestCaptureScope(consumeRid, 'amqp');
9349
+ const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
9107
9350
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9108
9351
  const rawTraceEvents: any[] = [];
9109
9352
  const maskReq: MaskRequestContext = {
@@ -9188,7 +9431,12 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9188
9431
  };
9189
9432
 
9190
9433
  scheduleSdkBackgroundWork(async () => {
9191
- const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, body);
9434
+ const bodyMaterialization = await materializeKafkaRequestBody(
9435
+ cfg,
9436
+ maskReq,
9437
+ body,
9438
+ captureScope,
9439
+ );
9192
9440
  const requestPayload: Record<string, any> = {
9193
9441
  rid: consumeRid,
9194
9442
  method: 'AMQP_CONSUME',
@@ -9210,7 +9458,6 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9210
9458
  t: alignedNow(),
9211
9459
  }],
9212
9460
  });
9213
- captureScope.backendRequestCount += 1;
9214
9461
  scheduleAmqpTraceFlush(cfg, amqpCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
9215
9462
  }, { priority: 'high' });
9216
9463
  };