@reproapp/node-sdk 0.0.46 → 0.0.48

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
@@ -2,7 +2,7 @@ import type { Request, Response, NextFunction } from 'express';
2
2
  import type { Schema, Model, Query } from 'mongoose';
3
3
  import * as mongoose from 'mongoose';
4
4
  import { AsyncLocalStorage } from 'async_hooks';
5
- import { randomUUID } from 'crypto';
5
+ import { createHmac, randomUUID } from 'crypto';
6
6
  import { createRequire } from 'module';
7
7
  import { join } from 'path';
8
8
  import { pathToFileURL } from 'url';
@@ -28,6 +28,7 @@ import {
28
28
  type RuntimePrivacySurface,
29
29
  type RuntimePrivacyTraceContext,
30
30
  } from './privacy';
31
+ import { projectSemanticInfrastructureValue } from './semantic-serialization';
31
32
 
32
33
  /**
33
34
  * ============================= IMPORTANT =============================
@@ -379,7 +380,11 @@ type PendingTraceEventRecord = TraceEventRecord & {
379
380
  };
380
381
 
381
382
  type TraceValueCaptureRef = {
382
- status: 'stored' | 'truncated-for-storage' | 'skipped-non-meaningful';
383
+ status:
384
+ | 'stored'
385
+ | 'truncated-for-storage'
386
+ | 'skipped-non-meaningful'
387
+ | 'failed-policy';
383
388
  valueId?: string;
384
389
  projectedKind?: string;
385
390
  };
@@ -414,6 +419,7 @@ type TraceValueBatchEntry = {
414
419
  };
415
420
 
416
421
  type TraceMaterializationConfig = {
422
+ tenantId: string;
417
423
  appId: string;
418
424
  appSecret?: string;
419
425
  appName?: string;
@@ -427,6 +433,7 @@ type TraceMaterializationWorkerPayload = {
427
433
  masking: NormalizedMaskingConfig | null;
428
434
  privacy: NormalizedRuntimePrivacyPolicy | null;
429
435
  fullValueCaptureEnabled: boolean;
436
+ valueNamespace?: string;
430
437
  };
431
438
 
432
439
  type TraceMaterializationWorkerRequest = {
@@ -1212,20 +1219,70 @@ type RequestCaptureScopeState = {
1212
1219
  traceBatchCount: number;
1213
1220
  dbChangeCount: number;
1214
1221
  emailEventCount: number;
1222
+ captureFailures: Array<{
1223
+ stage: string;
1224
+ target: string;
1225
+ reason: string;
1226
+ }>;
1227
+ privacyMode: 'policy' | 'fallback' | 'raw';
1228
+ privacyPolicyFingerprint?: string;
1229
+ privacyStatementCount: number;
1230
+ emittedValueDefinitionIds: Set<string>;
1215
1231
  };
1216
1232
 
1217
1233
  function createRequestCaptureScope(
1218
1234
  requestRid: string,
1219
1235
  transport: RequestCaptureScopeState['transport'],
1236
+ cfg?: ReproMiddlewareConfig,
1220
1237
  ): RequestCaptureScopeState {
1238
+ const privacy = cfg ? getRuntimePrivacyState(cfg).policy ?? null : null;
1239
+ const privacyMode =
1240
+ cfg && shouldCapturePrivacyRaw(cfg)
1241
+ ? 'raw'
1242
+ : privacy
1243
+ ? 'policy'
1244
+ : 'fallback';
1221
1245
  return {
1222
1246
  requestRid,
1223
1247
  transport,
1224
- backendRequestCount: 0,
1248
+ // Every scope represents one captured transport request. This is an
1249
+ // observed expectation, not a count of successful enqueue operations.
1250
+ backendRequestCount: 1,
1225
1251
  traceBatchCount: 0,
1226
1252
  dbChangeCount: 0,
1227
1253
  emailEventCount: 0,
1254
+ captureFailures: [],
1255
+ privacyMode,
1256
+ privacyPolicyFingerprint: privacy?.fingerprint,
1257
+ privacyStatementCount: privacy?.statements.length ?? 0,
1258
+ emittedValueDefinitionIds: new Set(),
1259
+ };
1260
+ }
1261
+
1262
+ function recordRequestCaptureFailure(
1263
+ scope: RequestCaptureScopeState | null | undefined,
1264
+ stage: string,
1265
+ target: string,
1266
+ reason: string,
1267
+ ): void {
1268
+ if (!scope) return;
1269
+ const normalized = {
1270
+ stage: String(stage || 'capture').slice(0, 80),
1271
+ target: String(target || 'unknown').slice(0, 120),
1272
+ reason: String(reason || 'capture_failed').slice(0, 120),
1228
1273
  };
1274
+ if (
1275
+ scope.captureFailures.some((failure) =>
1276
+ failure.stage === normalized.stage &&
1277
+ failure.target === normalized.target &&
1278
+ failure.reason === normalized.reason,
1279
+ )
1280
+ ) {
1281
+ return;
1282
+ }
1283
+ if (scope.captureFailures.length < 25) {
1284
+ scope.captureFailures.push(normalized);
1285
+ }
1229
1286
  }
1230
1287
 
1231
1288
  function buildRequestCaptureManifestPayload(
@@ -1255,6 +1312,20 @@ function buildRequestCaptureManifestPayload(
1255
1312
  to: expectedTraceBatches - 1,
1256
1313
  }
1257
1314
  : undefined,
1315
+ privacy: {
1316
+ mode: scope.privacyMode,
1317
+ status:
1318
+ scope.captureFailures.length > 0
1319
+ ? 'failed'
1320
+ : scope.privacyMode === 'raw'
1321
+ ? 'bypassed'
1322
+ : 'applied',
1323
+ policyFingerprint: scope.privacyPolicyFingerprint,
1324
+ statementCount: scope.privacyStatementCount,
1325
+ },
1326
+ captureFailures: scope.captureFailures.length
1327
+ ? scope.captureFailures.map((failure) => ({ ...failure }))
1328
+ : undefined,
1258
1329
  closedAt,
1259
1330
  };
1260
1331
  }
@@ -1439,12 +1510,6 @@ const SDK_BACKGROUND_MAX_DEFER_MS = (() => {
1439
1510
  return Number.POSITIVE_INFINITY;
1440
1511
  })();
1441
1512
 
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
1513
  const TRACE_MATERIALIZE_OFFTHREAD_ENABLED = (() => {
1449
1514
  const env = String(process.env.REPRO_TRACE_MATERIALIZE_OFFTHREAD || '').trim().toLowerCase();
1450
1515
  if (!env) return true;
@@ -1774,24 +1839,9 @@ function scheduleSdkBackgroundDrain(delayMs = 0): void {
1774
1839
  }
1775
1840
 
1776
1841
  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()
1842
+ await Promise.resolve()
1784
1843
  .then(work)
1785
1844
  .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
1845
  }
1796
1846
 
1797
1847
  async function drainQueuedIngestBeforeBackgroundWork(): Promise<void> {
@@ -2783,7 +2833,11 @@ const TRACE_MAX_EVENTS_PER_REQUEST = (() => {
2783
2833
  const TRACE_MAX_BATCHES_PER_REQUEST = (() => {
2784
2834
  const env = Number(process.env.REPRO_TRACE_MAX_BATCHES_PER_REQUEST);
2785
2835
  if (Number.isFinite(env) && env > 0) return Math.trunc(env);
2786
- return 8;
2836
+ // Byte-based splitting can legitimately produce one batch per event when
2837
+ // every event owns a distinct large value. The event limit remains the
2838
+ // hard upper bound; deriving this from the count-only batch size silently
2839
+ // dropped otherwise valid events after ten batches.
2840
+ return TRACE_MAX_EVENTS_PER_REQUEST;
2787
2841
  })();
2788
2842
  const TRACE_FLUSH_DELAY_MS = 20;
2789
2843
  // Choose how to order trace events in payloads.
@@ -3135,6 +3189,17 @@ function sanitizeTraceValue(
3135
3189
  const mongoPlaceholder = describeMongoPlaceholder(value);
3136
3190
  if (mongoPlaceholder) return mongoPlaceholder;
3137
3191
 
3192
+ const semanticProjection = projectSemanticInfrastructureValue(value);
3193
+ if (semanticProjection) {
3194
+ return sanitizeTraceValue(
3195
+ semanticProjection.value,
3196
+ depth,
3197
+ seen,
3198
+ options,
3199
+ valuePath,
3200
+ );
3201
+ }
3202
+
3138
3203
  if (isThenable(value)) {
3139
3204
  return { __type: 'Promise', state: 'pending' };
3140
3205
  }
@@ -3357,6 +3422,8 @@ async function materializePendingTraceEvents(
3357
3422
  maskReq: MaskRequestContext;
3358
3423
  masking: NormalizedMaskingConfig | null;
3359
3424
  resolvePrivacy: () => NormalizedRuntimePrivacyPolicy | null;
3425
+ captureScope?: RequestCaptureScopeState | null;
3426
+ valueNamespace?: string;
3360
3427
  },
3361
3428
  ): Promise<TraceEventRecord[]> {
3362
3429
  for (const evt of events) {
@@ -3374,6 +3441,7 @@ async function materializePendingTraceEvents(
3374
3441
  trace: candidate,
3375
3442
  masking: params.masking,
3376
3443
  privacy: activePrivacy,
3444
+ valueNamespace: params.valueNamespace,
3377
3445
  };
3378
3446
  const traceValueMetadata = {
3379
3447
  fn: evt.fn,
@@ -3403,7 +3471,7 @@ async function materializePendingTraceEvents(
3403
3471
  previewValue: evt.args,
3404
3472
  capture: captureContext,
3405
3473
  metadata: traceValueMetadata,
3406
- });
3474
+ }, params.captureScope);
3407
3475
  if (argsValueCapture?.entry) {
3408
3476
  attachTraceValueEntry(evt, argsValueCapture.entry);
3409
3477
  }
@@ -3432,7 +3500,7 @@ async function materializePendingTraceEvents(
3432
3500
  previewValue: evt.returnValue,
3433
3501
  capture: captureContext,
3434
3502
  metadata: traceValueMetadata,
3435
- });
3503
+ }, params.captureScope);
3436
3504
  if (returnValueCapture?.entry) {
3437
3505
  attachTraceValueEntry(evt, returnValueCapture.entry);
3438
3506
  }
@@ -3461,7 +3529,7 @@ async function materializePendingTraceEvents(
3461
3529
  previewValue: evt.error,
3462
3530
  capture: captureContext,
3463
3531
  metadata: traceValueMetadata,
3464
- });
3532
+ }, params.captureScope);
3465
3533
  if (errorValueCapture?.entry) {
3466
3534
  attachTraceValueEntry(evt, errorValueCapture.entry);
3467
3535
  }
@@ -3479,7 +3547,20 @@ function stripPendingTraceValues(
3479
3547
  reason: string,
3480
3548
  ): TraceEventRecord[] {
3481
3549
  for (const evt of events) {
3482
- if (!evt.__reproPending) continue;
3550
+ const pending = evt.__reproPending;
3551
+ if (!pending) continue;
3552
+ if (pending.argsRaw !== undefined) {
3553
+ evt.args = makeOmittedValue(reason);
3554
+ evt.argsValueCapture = { status: 'failed-policy' };
3555
+ }
3556
+ if (pending.returnValueRaw !== undefined) {
3557
+ evt.returnValue = makeOmittedValue(reason);
3558
+ evt.returnValueCapture = { status: 'failed-policy' };
3559
+ }
3560
+ if (pending.errorRaw !== undefined) {
3561
+ evt.error = makeOmittedValue(reason);
3562
+ evt.errorValueCapture = { status: 'failed-policy' };
3563
+ }
3483
3564
  delete evt.__reproPending;
3484
3565
  }
3485
3566
  return events;
@@ -3496,6 +3577,8 @@ async function materializePendingTraceEventsAsync(
3496
3577
  maskReq: MaskRequestContext;
3497
3578
  masking: NormalizedMaskingConfig | null;
3498
3579
  resolvePrivacy: () => NormalizedRuntimePrivacyPolicy | null;
3580
+ captureScope?: RequestCaptureScopeState | null;
3581
+ valueNamespace?: string;
3499
3582
  },
3500
3583
  ): Promise<TraceEventRecord[]> {
3501
3584
  if (!events.some((evt) => evt.__reproPending)) {
@@ -3510,6 +3593,12 @@ async function materializePendingTraceEventsAsync(
3510
3593
  resolvePrivacy: () => privacy,
3511
3594
  });
3512
3595
  } catch {
3596
+ recordRequestCaptureFailure(
3597
+ params.captureScope ?? getCtx().captureScope,
3598
+ 'privacy.trace',
3599
+ 'trace.values',
3600
+ 'main_thread_materialization_failed',
3601
+ );
3513
3602
  return stripPendingTraceValues(events, 'main_thread_failed');
3514
3603
  }
3515
3604
  }
@@ -3517,6 +3606,7 @@ async function materializePendingTraceEventsAsync(
3517
3606
  return await materializePendingTraceEventsOffThread(events, {
3518
3607
  events,
3519
3608
  cfg: {
3609
+ tenantId: params.cfg.tenantId,
3520
3610
  appId: params.cfg.appId,
3521
3611
  appSecret: params.cfg.appSecret,
3522
3612
  appName: params.cfg.appName,
@@ -3526,18 +3616,27 @@ async function materializePendingTraceEventsAsync(
3526
3616
  masking: params.masking,
3527
3617
  privacy,
3528
3618
  fullValueCaptureEnabled: __FULL_VALUE_CAPTURE_ENABLED,
3619
+ valueNamespace: params.valueNamespace,
3529
3620
  });
3530
3621
  } catch (err) {
3531
- if (isTraceMaterializerTimeoutError(err)) {
3532
- return stripPendingTraceValues(events, 'worker_timeout');
3533
- }
3534
3622
  try {
3535
3623
  return await materializePendingTraceEvents(events, {
3536
3624
  ...params,
3537
3625
  resolvePrivacy: () => privacy,
3538
3626
  });
3539
3627
  } catch {
3540
- return stripPendingTraceValues(events, 'worker_failed');
3628
+ recordRequestCaptureFailure(
3629
+ params.captureScope ?? getCtx().captureScope,
3630
+ 'privacy.trace',
3631
+ 'trace.values',
3632
+ isTraceMaterializerTimeoutError(err)
3633
+ ? 'worker_timeout_fallback_failed'
3634
+ : 'worker_and_fallback_failed',
3635
+ );
3636
+ return stripPendingTraceValues(
3637
+ events,
3638
+ isTraceMaterializerTimeoutError(err) ? 'worker_timeout_fallback_failed' : 'worker_failed',
3639
+ );
3541
3640
  }
3542
3641
  }
3543
3642
  }
@@ -3553,6 +3652,7 @@ export async function __materializePendingTraceEventsForWorker(
3553
3652
  maskReq: payload.maskReq,
3554
3653
  masking: payload.masking,
3555
3654
  resolvePrivacy: () => payload.privacy,
3655
+ valueNamespace: payload.valueNamespace,
3556
3656
  });
3557
3657
  return materialized.map((event) => {
3558
3658
  const entries = getEventTraceValueEntries(event);
@@ -4137,6 +4237,7 @@ function materializeInlinePrivacyValue(
4137
4237
  masking: NormalizedMaskingConfig | null,
4138
4238
  privacy: NormalizedRuntimePrivacyPolicy | null,
4139
4239
  db?: RuntimePrivacyDbContext | null,
4240
+ captureScope?: RequestCaptureScopeState | null,
4140
4241
  ): InlinePrivacyMaterializationResult {
4141
4242
  if (value === undefined) {
4142
4243
  return { value };
@@ -4149,6 +4250,12 @@ function materializeInlinePrivacyValue(
4149
4250
  const valueAfterPrivacy = applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db);
4150
4251
  return { value: valueAfterPrivacy };
4151
4252
  } catch {
4253
+ recordRequestCaptureFailure(
4254
+ captureScope ?? getCtx().captureScope,
4255
+ 'privacy.inline',
4256
+ target,
4257
+ 'privacy_transform_failed',
4258
+ );
4152
4259
  return {
4153
4260
  value: undefined,
4154
4261
  skipped: {
@@ -4169,6 +4276,7 @@ async function materializeInlinePrivacyValueAsync(
4169
4276
  masking: NormalizedMaskingConfig | null,
4170
4277
  privacy: NormalizedRuntimePrivacyPolicy | null,
4171
4278
  db?: RuntimePrivacyDbContext | null,
4279
+ captureScope?: RequestCaptureScopeState | null,
4172
4280
  ): Promise<InlinePrivacyMaterializationResult> {
4173
4281
  if (value === undefined) {
4174
4282
  return { value };
@@ -4181,6 +4289,12 @@ async function materializeInlinePrivacyValueAsync(
4181
4289
  const valueAfterPrivacy = await applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db);
4182
4290
  return { value: valueAfterPrivacy };
4183
4291
  } catch {
4292
+ recordRequestCaptureFailure(
4293
+ captureScope ?? getCtx().captureScope,
4294
+ 'privacy.inline',
4295
+ target,
4296
+ 'privacy_transform_failed',
4297
+ );
4184
4298
  return {
4185
4299
  value: undefined,
4186
4300
  skipped: {
@@ -4347,6 +4461,7 @@ type FullValueCaptureContext = {
4347
4461
  masking: NormalizedMaskingConfig | null;
4348
4462
  privacy: NormalizedRuntimePrivacyPolicy | null;
4349
4463
  db?: RuntimePrivacyDbContext | null;
4464
+ valueNamespace?: string;
4350
4465
  };
4351
4466
 
4352
4467
  const TRACE_FULL_VALUE_ENTRY_SYMBOL = Symbol('repro.traceFullValueEntries');
@@ -4752,6 +4867,18 @@ function cloneFullValueCandidate(
4752
4867
  const mongoPlaceholder = describeMongoPlaceholder(value);
4753
4868
  if (mongoPlaceholder) return undefined;
4754
4869
 
4870
+ const semanticProjection = projectSemanticInfrastructureValue(value);
4871
+ if (semanticProjection) {
4872
+ return cloneFullValueCandidate(
4873
+ semanticProjection.value,
4874
+ ctx,
4875
+ limits,
4876
+ depth,
4877
+ seen,
4878
+ valuePath,
4879
+ );
4880
+ }
4881
+
4755
4882
  if (isThenable(value)) return undefined;
4756
4883
  if (shouldSkipFullValueObject(value)) return undefined;
4757
4884
 
@@ -4979,6 +5106,18 @@ async function cloneFullValueCandidateAsync(
4979
5106
  const mongoPlaceholder = describeMongoPlaceholder(value);
4980
5107
  if (mongoPlaceholder) return undefined;
4981
5108
 
5109
+ const semanticProjection = projectSemanticInfrastructureValue(value);
5110
+ if (semanticProjection) {
5111
+ return cloneFullValueCandidateAsync(
5112
+ semanticProjection.value,
5113
+ ctx,
5114
+ limits,
5115
+ depth,
5116
+ seen,
5117
+ valuePath,
5118
+ );
5119
+ }
5120
+
4982
5121
  if (isThenable(value)) return undefined;
4983
5122
  if (shouldSkipFullValueObject(value)) return undefined;
4984
5123
 
@@ -5125,12 +5264,18 @@ function getEventTraceValueEntries(event: TraceEventRecord | PendingTraceEventRe
5125
5264
  return undefined;
5126
5265
  }
5127
5266
 
5128
- function collectBatchTraceValueEntries(batch: TraceEventRecord[], batchIndex: number): TraceValueBatchEntry[] {
5267
+ function collectBatchTraceValueEntries(
5268
+ batch: TraceEventRecord[],
5269
+ batchIndex: number,
5270
+ emittedValueIds: Set<string> = new Set(),
5271
+ ): TraceValueBatchEntry[] {
5129
5272
  const collected: TraceValueBatchEntry[] = [];
5130
5273
  batch.forEach((event, eventIndex) => {
5131
5274
  const entries = getEventTraceValueEntries(event);
5132
5275
  if (!Array.isArray(entries) || !entries.length) return;
5133
5276
  entries.forEach((entry) => {
5277
+ if (emittedValueIds.has(entry.id)) return;
5278
+ emittedValueIds.add(entry.id);
5134
5279
  collected.push({
5135
5280
  ...entry,
5136
5281
  batchIndex,
@@ -5149,13 +5294,44 @@ function serializedByteLength(value: any): number {
5149
5294
  }
5150
5295
  }
5151
5296
 
5297
+ function contentAddressedValueId(
5298
+ target: TraceValueBatchEntry['target'],
5299
+ candidateSerialized: string,
5300
+ capture: FullValueCaptureContext,
5301
+ ): string {
5302
+ const config = capture.runtimeConfig;
5303
+ const signingKey = String(
5304
+ config.appSecret ||
5305
+ `${config.tenantId || 'tenant'}:${config.appId || 'app'}`,
5306
+ );
5307
+ const namespace = String(
5308
+ capture.valueNamespace ||
5309
+ `${config.tenantId || 'tenant'}:${config.appId || 'app'}`,
5310
+ );
5311
+ const digest = createHmac('sha256', signingKey)
5312
+ .update('repro-captured-value-v1\0')
5313
+ .update(namespace)
5314
+ .update('\0')
5315
+ .update(target)
5316
+ .update('\0')
5317
+ .update(candidateSerialized)
5318
+ .digest('hex')
5319
+ .slice(0, 40);
5320
+ return `rv1_${digest}`;
5321
+ }
5322
+
5152
5323
  function estimateTraceBatchSerializedBytes(
5153
5324
  batch: TraceEventRecord[],
5154
5325
  requestRid: string,
5155
5326
  actionId: string | null | undefined,
5156
5327
  batchIndex: number,
5328
+ previouslyEmittedValueIds: ReadonlySet<string> = new Set(),
5157
5329
  ): number {
5158
- const traceValues = collectBatchTraceValueEntries(batch, batchIndex);
5330
+ const traceValues = collectBatchTraceValueEntries(
5331
+ batch,
5332
+ batchIndex,
5333
+ new Set(previouslyEmittedValueIds),
5334
+ );
5159
5335
  return serializedByteLength({
5160
5336
  actionId: actionId ?? null,
5161
5337
  trace: batch,
@@ -5179,12 +5355,30 @@ function chunkTraceEventsForTransport(
5179
5355
  const boundedEvents = boundTraceEventsForTransport(events);
5180
5356
  const countBatches = chunkArray(boundedEvents, TRACE_BATCH_SIZE);
5181
5357
  const sizedBatches: TraceEventRecord[][] = [];
5358
+ const emittedValueIds = new Set<string>();
5182
5359
  let current: TraceEventRecord[] = [];
5183
5360
 
5361
+ const commitCurrent = (): void => {
5362
+ if (!current.length) return;
5363
+ collectBatchTraceValueEntries(
5364
+ current,
5365
+ sizedBatches.length,
5366
+ emittedValueIds,
5367
+ );
5368
+ sizedBatches.push(current);
5369
+ current = [];
5370
+ };
5371
+
5184
5372
  for (const event of countBatches.flat()) {
5185
5373
  if (sizedBatches.length >= TRACE_MAX_BATCHES_PER_REQUEST) {
5186
5374
  break;
5187
5375
  }
5376
+ if (current.length >= TRACE_BATCH_SIZE) {
5377
+ commitCurrent();
5378
+ if (sizedBatches.length >= TRACE_MAX_BATCHES_PER_REQUEST) {
5379
+ break;
5380
+ }
5381
+ }
5188
5382
  const candidate = current.concat(event);
5189
5383
  const batchIndex = sizedBatches.length;
5190
5384
  const estimatedBytes = estimateTraceBatchSerializedBytes(
@@ -5192,15 +5386,15 @@ function chunkTraceEventsForTransport(
5192
5386
  requestRid,
5193
5387
  actionId,
5194
5388
  batchIndex,
5389
+ emittedValueIds,
5195
5390
  );
5196
5391
 
5197
5392
  if (
5198
5393
  current.length > 0 &&
5199
5394
  estimatedBytes > TRACE_BATCH_MAX_SERIALIZED_BYTES
5200
5395
  ) {
5201
- sizedBatches.push(current);
5396
+ commitCurrent();
5202
5397
  if (sizedBatches.length >= TRACE_MAX_BATCHES_PER_REQUEST) {
5203
- current = [];
5204
5398
  break;
5205
5399
  }
5206
5400
  current = [event];
@@ -5214,7 +5408,7 @@ function chunkTraceEventsForTransport(
5214
5408
  current.length > 0 &&
5215
5409
  sizedBatches.length < TRACE_MAX_BATCHES_PER_REQUEST
5216
5410
  ) {
5217
- sizedBatches.push(current);
5411
+ commitCurrent();
5218
5412
  }
5219
5413
 
5220
5414
  return sizedBatches;
@@ -5286,7 +5480,11 @@ function createCapturedValueEntry(
5286
5480
  return undefined;
5287
5481
  }
5288
5482
 
5289
- const valueId = randomUUID();
5483
+ const valueId = contentAddressedValueId(
5484
+ params.target,
5485
+ candidateSerialized,
5486
+ params.capture,
5487
+ );
5290
5488
  const projectedKind =
5291
5489
  candidate && typeof candidate === 'object'
5292
5490
  ? ((candidate as Record<string, any>).__kind || (candidate as Record<string, any>).__type)
@@ -5368,7 +5566,11 @@ async function createCapturedValueEntryAsync(
5368
5566
  return undefined;
5369
5567
  }
5370
5568
 
5371
- const valueId = randomUUID();
5569
+ const valueId = contentAddressedValueId(
5570
+ params.target,
5571
+ candidateSerialized,
5572
+ params.capture,
5573
+ );
5372
5574
  const projectedKind =
5373
5575
  candidate && typeof candidate === 'object'
5374
5576
  ? ((candidate as Record<string, any>).__kind || (candidate as Record<string, any>).__type)
@@ -5391,6 +5593,18 @@ async function createCapturedValueEntryAsync(
5391
5593
  };
5392
5594
  }
5393
5595
 
5596
+ function appendCapturedValueDefinition(
5597
+ sink: TraceValueBatchEntry[],
5598
+ entry: TraceValueBatchEntry | undefined,
5599
+ captureScope?: RequestCaptureScopeState | null,
5600
+ ): void {
5601
+ if (!entry) return;
5602
+ const emitted = captureScope?.emittedValueDefinitionIds;
5603
+ if (emitted?.has(entry.id)) return;
5604
+ emitted?.add(entry.id);
5605
+ sink.push(entry);
5606
+ }
5607
+
5394
5608
  function maybeCaptureRequestValue(
5395
5609
  params: {
5396
5610
  target: 'request.headers' | 'request.body' | 'request.params' | 'request.query' | 'response.body';
@@ -5402,9 +5616,7 @@ function maybeCaptureRequestValue(
5402
5616
  ): TraceValueCaptureRef | undefined {
5403
5617
  const result = createCapturedValueEntry(params);
5404
5618
  if (!result) return undefined;
5405
- if (result.entry) {
5406
- sink.push(result.entry);
5407
- }
5619
+ appendCapturedValueDefinition(sink, result.entry);
5408
5620
  return result.captureRef;
5409
5621
  }
5410
5622
 
@@ -5416,12 +5628,22 @@ async function maybeCaptureRequestValueAsync(
5416
5628
  capture: FullValueCaptureContext;
5417
5629
  },
5418
5630
  sink: TraceValueBatchEntry[],
5631
+ captureScope?: RequestCaptureScopeState | null,
5419
5632
  ): Promise<TraceValueCaptureRef | undefined> {
5420
- const result = await createCapturedValueEntryAsync(params);
5421
- if (!result) return undefined;
5422
- if (result.entry) {
5423
- sink.push(result.entry);
5633
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5634
+ try {
5635
+ result = await createCapturedValueEntryAsync(params);
5636
+ } catch {
5637
+ recordRequestCaptureFailure(
5638
+ captureScope ?? getCtx().captureScope,
5639
+ 'privacy.full_value',
5640
+ params.target,
5641
+ 'privacy_transform_failed',
5642
+ );
5643
+ return { status: 'failed-policy' };
5424
5644
  }
5645
+ if (!result) return undefined;
5646
+ appendCapturedValueDefinition(sink, result.entry, captureScope);
5425
5647
  return result.captureRef;
5426
5648
  }
5427
5649
 
@@ -5445,8 +5667,19 @@ async function maybeCaptureTraceValueAsync(
5445
5667
  capture: FullValueCaptureContext;
5446
5668
  metadata?: Partial<TraceValueBatchEntry>;
5447
5669
  },
5670
+ captureScope?: RequestCaptureScopeState | null,
5448
5671
  ): Promise<{ captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined> {
5449
- return await createCapturedValueEntryAsync(params);
5672
+ try {
5673
+ return await createCapturedValueEntryAsync(params);
5674
+ } catch {
5675
+ recordRequestCaptureFailure(
5676
+ captureScope ?? getCtx().captureScope,
5677
+ 'privacy.full_value',
5678
+ params.target,
5679
+ 'privacy_transform_failed',
5680
+ );
5681
+ return { captureRef: { status: 'failed-policy' } };
5682
+ }
5450
5683
  }
5451
5684
 
5452
5685
  function maybeCaptureDbValue(
@@ -5460,9 +5693,7 @@ function maybeCaptureDbValue(
5460
5693
  ): TraceValueCaptureRef | undefined {
5461
5694
  const result = createCapturedValueEntry(params);
5462
5695
  if (!result) return undefined;
5463
- if (result.entry) {
5464
- sink.push(result.entry);
5465
- }
5696
+ appendCapturedValueDefinition(sink, result.entry);
5466
5697
  return result.captureRef;
5467
5698
  }
5468
5699
 
@@ -5474,12 +5705,22 @@ async function maybeCaptureDbValueAsync(
5474
5705
  capture: FullValueCaptureContext;
5475
5706
  },
5476
5707
  sink: TraceValueBatchEntry[],
5708
+ captureScope?: RequestCaptureScopeState | null,
5477
5709
  ): Promise<TraceValueCaptureRef | undefined> {
5478
- const result = await createCapturedValueEntryAsync(params);
5479
- if (!result) return undefined;
5480
- if (result.entry) {
5481
- sink.push(result.entry);
5710
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5711
+ try {
5712
+ result = await createCapturedValueEntryAsync(params);
5713
+ } catch {
5714
+ recordRequestCaptureFailure(
5715
+ captureScope ?? getCtx().captureScope,
5716
+ 'privacy.full_value',
5717
+ params.target,
5718
+ 'privacy_transform_failed',
5719
+ );
5720
+ return { status: 'failed-policy' };
5482
5721
  }
5722
+ if (!result) return undefined;
5723
+ appendCapturedValueDefinition(sink, result.entry, captureScope);
5483
5724
  return result.captureRef;
5484
5725
  }
5485
5726
 
@@ -5511,18 +5752,30 @@ async function maybeCaptureDbValueFromCandidatesAsync(
5511
5752
  capture: FullValueCaptureContext;
5512
5753
  },
5513
5754
  sink: TraceValueBatchEntry[],
5755
+ captureScope?: RequestCaptureScopeState | null,
5514
5756
  ): Promise<TraceValueCaptureRef | undefined> {
5515
5757
  let skippedRef: TraceValueCaptureRef | undefined;
5516
5758
  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
- });
5759
+ let result: Awaited<ReturnType<typeof createCapturedValueEntryAsync>>;
5760
+ try {
5761
+ result = await createCapturedValueEntryAsync({
5762
+ target: params.target,
5763
+ rawValue,
5764
+ previewValue: params.previewValue,
5765
+ capture: params.capture,
5766
+ });
5767
+ } catch {
5768
+ recordRequestCaptureFailure(
5769
+ captureScope ?? getCtx().captureScope,
5770
+ 'privacy.full_value',
5771
+ params.target,
5772
+ 'privacy_transform_failed',
5773
+ );
5774
+ return { status: 'failed-policy' };
5775
+ }
5523
5776
  if (!result) continue;
5524
5777
  if (result.entry) {
5525
- sink.push(result.entry);
5778
+ appendCapturedValueDefinition(sink, result.entry, captureScope);
5526
5779
  return result.captureRef;
5527
5780
  }
5528
5781
  if (!skippedRef) {
@@ -5827,7 +6080,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
5827
6080
 
5828
6081
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
5829
6082
  const rid = nextRequestId(requestEpochMs);
5830
- const captureScope = createRequestCaptureScope(rid, 'http');
6083
+ const captureScope = createRequestCaptureScope(rid, 'http', captureCfg);
5831
6084
  emitRequestCaptureStart(captureCfg, sid, aid, captureScope, requestEpochMs);
5832
6085
  const t0 = requestStartRaw;
5833
6086
  const url = (req as any).originalUrl || req.url || '/';
@@ -6029,9 +6282,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6029
6282
  endpointTraceCtx,
6030
6283
  masking,
6031
6284
  activePrivacy,
6285
+ undefined,
6286
+ captureScope,
6032
6287
  );
6033
6288
  const requestBody = requestBodyMaterialization.value;
6034
- const requestParams = await applyPrivacyThenMaskAsync(
6289
+ const requestParamsMaterialization = await materializeInlinePrivacyValueAsync(
6035
6290
  'request.params',
6036
6291
  sanitizeRequestSnapshot((req as any).params),
6037
6292
  captureCfg,
@@ -6039,8 +6294,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6039
6294
  endpointTraceCtx,
6040
6295
  masking,
6041
6296
  activePrivacy,
6297
+ undefined,
6298
+ captureScope,
6042
6299
  );
6043
- const requestQuery = await applyPrivacyThenMaskAsync(
6300
+ const requestParams = requestParamsMaterialization.value;
6301
+ const requestQueryMaterialization = await materializeInlinePrivacyValueAsync(
6044
6302
  'request.query',
6045
6303
  sanitizeRequestSnapshot((req as any).query),
6046
6304
  captureCfg,
@@ -6048,8 +6306,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6048
6306
  endpointTraceCtx,
6049
6307
  masking,
6050
6308
  activePrivacy,
6309
+ undefined,
6310
+ captureScope,
6051
6311
  );
6052
- const maskedHeaders = await applyPrivacyThenMaskAsync(
6312
+ const requestQuery = requestQueryMaterialization.value;
6313
+ const requestHeadersMaterialization = await materializeInlinePrivacyValueAsync(
6053
6314
  'request.headers',
6054
6315
  requestHeaders,
6055
6316
  captureCfg,
@@ -6057,7 +6318,10 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6057
6318
  endpointTraceCtx,
6058
6319
  masking,
6059
6320
  activePrivacy,
6321
+ undefined,
6322
+ captureScope,
6060
6323
  );
6324
+ const maskedHeaders = requestHeadersMaterialization.value;
6061
6325
  const responseBodyMaterialization = capturedBody === undefined
6062
6326
  ? { value: undefined }
6063
6327
  : limitRawInlinePrivacyValue('response.body', capturedBody)
@@ -6069,6 +6333,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6069
6333
  endpointTraceCtx,
6070
6334
  masking,
6071
6335
  activePrivacy,
6336
+ undefined,
6337
+ captureScope,
6072
6338
  );
6073
6339
  const responseBody = responseBodyMaterialization.value;
6074
6340
  const requestValueEntries: TraceValueBatchEntry[] = [];
@@ -6085,8 +6351,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6085
6351
  trace: endpointTraceCtx,
6086
6352
  masking,
6087
6353
  privacy: activePrivacy,
6354
+ valueNamespace: sid,
6088
6355
  },
6089
- }, requestValueEntries);
6356
+ }, requestValueEntries, captureScope);
6090
6357
  const paramsValueCapture = await maybeCaptureRequestValueAsync({
6091
6358
  target: 'request.params',
6092
6359
  rawValue: (req as any).params,
@@ -6098,8 +6365,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6098
6365
  trace: endpointTraceCtx,
6099
6366
  masking,
6100
6367
  privacy: activePrivacy,
6368
+ valueNamespace: sid,
6101
6369
  },
6102
- }, requestValueEntries);
6370
+ }, requestValueEntries, captureScope);
6103
6371
  const queryValueCapture = await maybeCaptureRequestValueAsync({
6104
6372
  target: 'request.query',
6105
6373
  rawValue: (req as any).query,
@@ -6111,8 +6379,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6111
6379
  trace: endpointTraceCtx,
6112
6380
  masking,
6113
6381
  privacy: activePrivacy,
6382
+ valueNamespace: sid,
6114
6383
  },
6115
- }, requestValueEntries);
6384
+ }, requestValueEntries, captureScope);
6116
6385
  const headersValueCapture = await maybeCaptureRequestValueAsync({
6117
6386
  target: 'request.headers',
6118
6387
  rawValue: req.headers,
@@ -6124,8 +6393,9 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6124
6393
  trace: endpointTraceCtx,
6125
6394
  masking,
6126
6395
  privacy: activePrivacy,
6396
+ valueNamespace: sid,
6127
6397
  },
6128
- }, requestValueEntries);
6398
+ }, requestValueEntries, captureScope);
6129
6399
  const respBodyValueCapture = capturedBody === undefined
6130
6400
  ? undefined
6131
6401
  : await maybeCaptureRequestValueAsync({
@@ -6139,15 +6409,16 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6139
6409
  trace: endpointTraceCtx,
6140
6410
  masking,
6141
6411
  privacy: activePrivacy,
6412
+ valueNamespace: sid,
6142
6413
  },
6143
- }, requestValueEntries);
6414
+ }, requestValueEntries, captureScope);
6144
6415
 
6145
6416
  const responseDurationMs =
6146
6417
  finishedAt !== null && Number.isFinite(finishedAt)
6147
6418
  ? Math.max(0, finishedAt - t0)
6148
6419
  : Date.now() - t0;
6149
6420
  const requestPayload: Record<string, any> = {
6150
- rid,
6421
+ rid,
6151
6422
  method: req.method,
6152
6423
  url,
6153
6424
  path,
@@ -6201,9 +6472,43 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6201
6472
  t: requestEpochMs,
6202
6473
  }]
6203
6474
  });
6204
- captureScope.backendRequestCount += 1;
6205
6475
  } catch {
6206
- // never break user code
6476
+ recordRequestCaptureFailure(
6477
+ captureScope,
6478
+ 'request.capture',
6479
+ 'backend_request',
6480
+ 'request_materialization_failed',
6481
+ );
6482
+ // Preserve non-sensitive request metadata so the manifest cannot
6483
+ // silently turn a failed policy capture into a zero-request scope.
6484
+ post(captureCfg, sid, {
6485
+ entries: [{
6486
+ actionId: aid,
6487
+ request: {
6488
+ rid,
6489
+ method: req.method,
6490
+ url,
6491
+ path,
6492
+ status: res.statusCode,
6493
+ durMs:
6494
+ finishedAt !== null && Number.isFinite(finishedAt)
6495
+ ? Math.max(0, finishedAt - t0)
6496
+ : Date.now() - t0,
6497
+ key,
6498
+ bodyMaterialization: {
6499
+ status: 'skipped',
6500
+ reason: 'request_materialization_failed',
6501
+ target: 'request.body',
6502
+ },
6503
+ respBodyMaterialization: {
6504
+ status: 'skipped',
6505
+ reason: 'request_materialization_failed',
6506
+ target: 'response.body',
6507
+ },
6508
+ },
6509
+ t: requestEpochMs,
6510
+ }],
6511
+ });
6207
6512
  }
6208
6513
  };
6209
6514
 
@@ -6326,6 +6631,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6326
6631
  maskReq,
6327
6632
  masking,
6328
6633
  resolvePrivacy,
6634
+ captureScope,
6635
+ valueNamespace: sid,
6329
6636
  });
6330
6637
  const baseEvents = balanceTraceEvents(materializedEvents.slice());
6331
6638
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
@@ -6344,16 +6651,30 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6344
6651
  }
6345
6652
 
6346
6653
  } catch {
6654
+ recordRequestCaptureFailure(
6655
+ captureScope,
6656
+ 'trace.capture',
6657
+ 'trace.batches',
6658
+ 'trace_capture_failed',
6659
+ );
6347
6660
  captureScope.traceBatchCount = 0;
6348
6661
  traceBatches = [];
6349
- } finally {
6350
- emitRequestCaptureManifest(traceBatches.length);
6351
6662
  }
6352
6663
 
6664
+ // Finalize policy/materialization status before publishing the
6665
+ // high-priority manifest. The manifest must lead its evidence
6666
+ // on the wire so a large trace queue cannot starve completion.
6667
+ emitRequestCaptureManifest(traceBatches.length);
6668
+
6353
6669
  try {
6670
+ const emittedValueIds = new Set<string>();
6354
6671
  for (let i = 0; i < traceBatches.length; i++) {
6355
6672
  const batch = traceBatches[i];
6356
- const traceValues = collectBatchTraceValueEntries(batch, i);
6673
+ const traceValues = collectBatchTraceValueEntries(
6674
+ batch,
6675
+ i,
6676
+ emittedValueIds,
6677
+ );
6357
6678
 
6358
6679
  post(captureCfg, sid, {
6359
6680
  entries: [{
@@ -6370,7 +6691,12 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6370
6691
  });
6371
6692
  }
6372
6693
  } catch {
6373
- // never break user code
6694
+ recordRequestCaptureFailure(
6695
+ captureScope,
6696
+ 'trace.enqueue',
6697
+ 'trace.batches',
6698
+ 'trace_enqueue_failed',
6699
+ );
6374
6700
  }
6375
6701
  };
6376
6702
  }
@@ -6958,12 +7284,6 @@ function dehydrateComplexValue(value: any) {
6958
7284
  }
6959
7285
  } catch {}
6960
7286
 
6961
- const ctor = (value as any)?.constructor?.name;
6962
- if (ctor && ctor !== 'Object') {
6963
- const plain = safeJson(value);
6964
- if (plain !== undefined) return plain;
6965
- }
6966
-
6967
7287
  return value;
6968
7288
  }
6969
7289
 
@@ -6971,6 +7291,11 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
6971
7291
  const ctx = getCtx() as Ctx;
6972
7292
  const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
6973
7293
  if (!shouldCaptureDbSpan(spanContext)) return;
7294
+ if (sid && ctx.captureScope) {
7295
+ // Count the observed database operation before asynchronous privacy
7296
+ // materialization. A later failure must remain visible to completeness.
7297
+ ctx.captureScope.dbChangeCount += 1;
7298
+ }
6974
7299
  const runCtx: Ctx = {
6975
7300
  sid: ctx.sid,
6976
7301
  aid: ctx.aid,
@@ -6982,7 +7307,14 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
6982
7307
  };
6983
7308
  scheduleSdkBackgroundWork(async () => {
6984
7309
  await als.run(runCtx, async () => {
6985
- await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => undefined);
7310
+ await emitDbQueryAsync(cfg, sid, aid, payload).catch(() => {
7311
+ recordRequestCaptureFailure(
7312
+ runCtx.captureScope,
7313
+ 'db.capture',
7314
+ 'db.change',
7315
+ 'db_capture_failed',
7316
+ );
7317
+ });
6986
7318
  });
6987
7319
  }, { priority: 'high' });
6988
7320
  }
@@ -7026,6 +7358,7 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
7026
7358
  masking: null,
7027
7359
  privacy,
7028
7360
  db: dbContext,
7361
+ valueNamespace: sid,
7029
7362
  };
7030
7363
  const dbValues: TraceValueBatchEntry[] = [];
7031
7364
  const pkValueCapture = pkMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
@@ -7033,37 +7366,37 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
7033
7366
  rawValue: payload.pk,
7034
7367
  previewValue: pkPreview,
7035
7368
  capture,
7036
- }, dbValues);
7369
+ }, dbValues, ctx.captureScope);
7037
7370
  const beforeValueCapture = beforeMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
7038
7371
  target: 'db.before',
7039
7372
  rawValue: payload.before,
7040
7373
  previewValue: beforePreview,
7041
7374
  capture,
7042
- }, dbValues);
7375
+ }, dbValues, ctx.captureScope);
7043
7376
  const afterValueCapture = afterMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
7044
7377
  target: 'db.after',
7045
7378
  rawValue: payload.after,
7046
7379
  previewValue: afterPreview,
7047
7380
  capture,
7048
- }, dbValues);
7381
+ }, dbValues, ctx.captureScope);
7049
7382
  const queryValueCapture = queryMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
7050
7383
  target: 'db.query',
7051
7384
  rawValue: querySource,
7052
7385
  previewValue: queryPreview,
7053
7386
  capture,
7054
- }, dbValues);
7387
+ }, dbValues, ctx.captureScope);
7055
7388
  const resultMetaValueCapture = resultMetaMaterialization.skipped ? undefined : await maybeCaptureDbValueFromCandidatesAsync({
7056
7389
  target: 'db.resultMeta',
7057
7390
  rawValues: buildResultMetaFullValueCandidates(payload.resultMetaRaw, resultMetaSource),
7058
7391
  previewValue: resultMetaPreview,
7059
7392
  capture,
7060
- }, dbValues);
7393
+ }, dbValues, ctx.captureScope);
7061
7394
  const errorValueCapture = errorMaterialization.skipped ? undefined : await maybeCaptureDbValueAsync({
7062
7395
  target: 'db.error',
7063
7396
  rawValue: payload.error,
7064
7397
  previewValue: errorPreview,
7065
7398
  capture,
7066
- }, dbValues);
7399
+ }, dbValues, ctx.captureScope);
7067
7400
  const dbEntry = attachSpanContext({
7068
7401
  type: payload.type ?? 'mongo',
7069
7402
  collection: payload.collection,
@@ -7097,8 +7430,13 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
7097
7430
  t: payload.t,
7098
7431
  }]
7099
7432
  });
7100
- if (enqueued && ctx.captureScope) {
7101
- ctx.captureScope.dbChangeCount += 1;
7433
+ if (!enqueued) {
7434
+ recordRequestCaptureFailure(
7435
+ ctx.captureScope,
7436
+ 'db.enqueue',
7437
+ 'db.change',
7438
+ 'db_enqueue_failed',
7439
+ );
7102
7440
  }
7103
7441
  }
7104
7442
 
@@ -7534,6 +7872,7 @@ async function materializeKafkaRequestBody(
7534
7872
  cfg: ReproMiddlewareConfig,
7535
7873
  maskReq: MaskRequestContext,
7536
7874
  body: Record<string, any>,
7875
+ captureScope?: RequestCaptureScopeState | null,
7537
7876
  ): Promise<InlinePrivacyMaterializationResult> {
7538
7877
  return materializeInlinePrivacyValueAsync(
7539
7878
  'request.body',
@@ -7543,6 +7882,8 @@ async function materializeKafkaRequestBody(
7543
7882
  null,
7544
7883
  normalizeMaskingConfig(cfg.masking),
7545
7884
  getRuntimePrivacyState(cfg).policy ?? null,
7885
+ undefined,
7886
+ captureScope,
7546
7887
  );
7547
7888
  }
7548
7889
 
@@ -7660,6 +8001,7 @@ function recordKafkaTraceEvent(
7660
8001
  trace: candidate,
7661
8002
  masking,
7662
8003
  privacy,
8004
+ valueNamespace: getCtx().sid,
7663
8005
  };
7664
8006
  const traceValueMetadata = {
7665
8007
  fn: evt.fn,
@@ -7828,6 +8170,7 @@ async function recordKafkaTraceEventAsync(
7828
8170
  maskReq,
7829
8171
  masking,
7830
8172
  resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
8173
+ valueNamespace: getCtx().sid,
7831
8174
  });
7832
8175
  sink.push(...materializedEvents);
7833
8176
  }
@@ -7848,10 +8191,11 @@ function buildKafkaTraceEntries(
7848
8191
  requestRid,
7849
8192
  actionId,
7850
8193
  );
8194
+ const emittedValueIds = new Set<string>();
7851
8195
  return batches.map((batch, index) => ({
7852
8196
  actionId: actionId ?? null,
7853
8197
  trace: batch,
7854
- traceValues: collectBatchTraceValueEntries(batch, index),
8198
+ traceValues: collectBatchTraceValueEntries(batch, index, emittedValueIds),
7855
8199
  traceBatch: {
7856
8200
  rid: requestRid,
7857
8201
  index,
@@ -7882,22 +8226,32 @@ function scheduleKafkaTraceFlush(
7882
8226
  const evt = buildPendingKafkaTraceEvent(rawTraceEvent);
7883
8227
  if (evt) pendingEvents.push(evt);
7884
8228
  }
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);
8229
+ let entries: LegacyEntry[] = [];
8230
+ try {
8231
+ if (pendingEvents.length) {
8232
+ const masking = normalizeMaskingConfig(cfg.masking);
8233
+ const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
8234
+ const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
8235
+ cfg,
8236
+ maskReq,
8237
+ masking,
8238
+ resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
8239
+ captureScope,
8240
+ valueNamespace: sid,
8241
+ });
8242
+ entries = buildKafkaTraceEntries(actionId, requestRid, materializedEvents);
8243
+ }
8244
+ } catch {
8245
+ recordRequestCaptureFailure(
8246
+ captureScope,
8247
+ 'trace.capture',
8248
+ 'trace.batches',
8249
+ 'trace_capture_failed',
8250
+ );
8251
+ }
7895
8252
  if (captureScope) {
7896
8253
  captureScope.traceBatchCount = entries.length;
7897
8254
  }
7898
- if (entries.length) {
7899
- post(cfg, sid, { entries });
7900
- }
7901
8255
  if (captureScope) {
7902
8256
  post(cfg, sid, {
7903
8257
  entries: [{
@@ -7909,7 +8263,10 @@ function scheduleKafkaTraceFlush(
7909
8263
  ),
7910
8264
  t: alignedNow(),
7911
8265
  }],
7912
- });
8266
+ }, { priority: 'high' });
8267
+ }
8268
+ if (entries.length) {
8269
+ post(cfg, sid, { entries });
7913
8270
  }
7914
8271
  });
7915
8272
  }, { priority: 'high' });
@@ -7928,7 +8285,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
7928
8285
  const aid = ctx.aid ?? fallback.aid ?? null;
7929
8286
  const span = captureSpanContextFromTracer();
7930
8287
  const publishRid = nextRequestId(alignedNow());
7931
- const captureScope = createRequestCaptureScope(publishRid, 'kafka');
8288
+ const captureScope = createRequestCaptureScope(publishRid, 'kafka', cfg);
7932
8289
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7933
8290
  const payloadObj = payload && typeof payload === 'object' ? payload : {};
7934
8291
  const topic = sanitizeKafkaTopic((payloadObj as any).topic);
@@ -7987,7 +8344,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
7987
8344
  traceId: span?.traceId ?? null,
7988
8345
  spanId: span?.spanId ?? null,
7989
8346
  parentSpanId: span?.parentSpanId ?? null,
7990
- });
8347
+ }, captureScope);
7991
8348
 
7992
8349
  const requestPayload: Record<string, any> = {
7993
8350
  rid: publishRid,
@@ -8018,7 +8375,6 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8018
8375
  },
8019
8376
  ],
8020
8377
  });
8021
- captureScope.backendRequestCount += 1;
8022
8378
  post(cfg, sid, {
8023
8379
  entries: [{
8024
8380
  actionId: aid ?? null,
@@ -8043,7 +8399,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8043
8399
  const aid = ctx.aid ?? fallback.aid ?? null;
8044
8400
  const span = captureSpanContextFromTracer();
8045
8401
  const publishRid = nextRequestId(alignedNow());
8046
- const captureScope = createRequestCaptureScope(publishRid, 'kafka');
8402
+ const captureScope = createRequestCaptureScope(publishRid, 'kafka', cfg);
8047
8403
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8048
8404
 
8049
8405
  const payloadObj = payload && typeof payload === 'object' ? payload : {};
@@ -8137,7 +8493,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8137
8493
  traceId: span?.traceId ?? null,
8138
8494
  spanId: span?.spanId ?? null,
8139
8495
  parentSpanId: span?.parentSpanId ?? null,
8140
- });
8496
+ }, captureScope);
8141
8497
  const requestPayload: Record<string, any> = {
8142
8498
  rid: publishRid,
8143
8499
  method: 'KAFKA_PUBLISH_BATCH',
@@ -8167,7 +8523,6 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8167
8523
  },
8168
8524
  ],
8169
8525
  });
8170
- captureScope.backendRequestCount += 1;
8171
8526
  post(cfg, sid, {
8172
8527
  entries: [{
8173
8528
  actionId: aid ?? null,
@@ -8194,7 +8549,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8194
8549
  const aid = runtime.aid;
8195
8550
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
8196
8551
  const traceId = consumeRid;
8197
- const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
8552
+ const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8198
8553
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8199
8554
  const rawTraceEvents: any[] = [];
8200
8555
  const maskReq: MaskRequestContext = {
@@ -8266,7 +8621,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8266
8621
  valueEncoding: messagePayload.valueEncoding,
8267
8622
  valueBytes: messagePayload.valueBytes,
8268
8623
  message: messagePayload,
8269
- });
8624
+ }, captureScope);
8270
8625
  const requestPayload: Record<string, any> = {
8271
8626
  rid: consumeRid,
8272
8627
  method: 'KAFKA_CONSUME',
@@ -8288,7 +8643,6 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8288
8643
  t: alignedNow(),
8289
8644
  }],
8290
8645
  });
8291
- captureScope.backendRequestCount += 1;
8292
8646
  scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
8293
8647
  }
8294
8648
  };
@@ -8305,7 +8659,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8305
8659
  const aid = runtime.aid;
8306
8660
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
8307
8661
  const traceId = consumeRid;
8308
- const captureScope = createRequestCaptureScope(consumeRid, 'kafka');
8662
+ const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8309
8663
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8310
8664
  const rawTraceEvents: any[] = [];
8311
8665
  const maskReq: MaskRequestContext = {
@@ -8375,7 +8729,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8375
8729
  parentSpanId: runtime.parentSpanId,
8376
8730
  firstMessageKey: firstMessagePayload?.key ?? null,
8377
8731
  messages: messages.map((message: any) => buildKafkaMessagePayload(message)),
8378
- });
8732
+ }, captureScope);
8379
8733
  const requestPayload: Record<string, any> = {
8380
8734
  rid: consumeRid,
8381
8735
  method: 'KAFKA_CONSUME_BATCH',
@@ -8397,7 +8751,6 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8397
8751
  t: alignedNow(),
8398
8752
  }],
8399
8753
  });
8400
- captureScope.backendRequestCount += 1;
8401
8754
  scheduleKafkaTraceFlush(cfg, kafkaCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
8402
8755
  }
8403
8756
  };
@@ -8820,24 +9173,28 @@ function scheduleAmqpTraceFlush(
8820
9173
  }
8821
9174
 
8822
9175
  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 });
9176
+ try {
9177
+ if (pendingEvents.length) {
9178
+ const masking = normalizeMaskingConfig(cfg.masking);
9179
+ const preparedEvents = preparePendingTraceEventsForFlush(pendingEvents);
9180
+ const materializedEvents = await materializePendingTraceEventsAsync(preparedEvents, {
9181
+ cfg,
9182
+ maskReq,
9183
+ masking,
9184
+ resolvePrivacy: () => getRuntimePrivacyState(cfg).policy ?? null,
9185
+ captureScope,
9186
+ });
9187
+ entries = buildAmqpTraceEntries(actionId, requestRid, materializedEvents);
8836
9188
  }
8837
- } else {
8838
- captureScope.traceBatchCount = 0;
9189
+ } catch {
9190
+ recordRequestCaptureFailure(
9191
+ captureScope,
9192
+ 'trace.capture',
9193
+ 'trace.batches',
9194
+ 'trace_capture_failed',
9195
+ );
8839
9196
  }
8840
-
9197
+ captureScope.traceBatchCount = entries.length;
8841
9198
  post(cfg, sid, {
8842
9199
  entries: [{
8843
9200
  actionId: actionId ?? null,
@@ -8848,7 +9205,10 @@ function scheduleAmqpTraceFlush(
8848
9205
  ),
8849
9206
  t: alignedNow(),
8850
9207
  }],
8851
- });
9208
+ }, { priority: 'high' });
9209
+ if (entries.length) {
9210
+ post(cfg, sid, { entries });
9211
+ }
8852
9212
  });
8853
9213
  }, { priority: 'high' });
8854
9214
  }
@@ -8920,7 +9280,12 @@ function scheduleAmqpPublishCapture(params: {
8920
9280
  };
8921
9281
 
8922
9282
  scheduleSdkBackgroundWork(async () => {
8923
- const bodyMaterialization = await materializeKafkaRequestBody(params.cfg, maskReq, body);
9283
+ const bodyMaterialization = await materializeKafkaRequestBody(
9284
+ params.cfg,
9285
+ maskReq,
9286
+ body,
9287
+ params.captureScope,
9288
+ );
8924
9289
  const requestPayload: Record<string, any> = {
8925
9290
  rid: params.publishRid,
8926
9291
  method: params.method,
@@ -8951,7 +9316,6 @@ function scheduleAmqpPublishCapture(params: {
8951
9316
  t: alignedNow(),
8952
9317
  }],
8953
9318
  });
8954
- params.captureScope.backendRequestCount += 1;
8955
9319
  post(params.cfg, params.sid!, {
8956
9320
  entries: [{
8957
9321
  actionId: params.aid ?? null,
@@ -9013,7 +9377,7 @@ function patchAmqplibPublishMethod(
9013
9377
  : `amqp://queue/${queue}`;
9014
9378
  const key = `${method} ${methodName === 'publish' ? `${exchange || 'default'}/${routingKey}` : queue}`;
9015
9379
  const content = methodName === 'publish' ? args[2] : args[1];
9016
- const captureScope = createRequestCaptureScope(publishRid, 'amqp');
9380
+ const captureScope = createRequestCaptureScope(publishRid, 'amqp', cfg);
9017
9381
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9018
9382
  const t0 = Date.now();
9019
9383
  let finished = false;
@@ -9103,7 +9467,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9103
9467
  const deliveryTag = message?.fields?.deliveryTag ?? alignedNow();
9104
9468
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
9105
9469
  const traceId = consumeRid;
9106
- const captureScope = createRequestCaptureScope(consumeRid, 'amqp');
9470
+ const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
9107
9471
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9108
9472
  const rawTraceEvents: any[] = [];
9109
9473
  const maskReq: MaskRequestContext = {
@@ -9188,7 +9552,12 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9188
9552
  };
9189
9553
 
9190
9554
  scheduleSdkBackgroundWork(async () => {
9191
- const bodyMaterialization = await materializeKafkaRequestBody(cfg, maskReq, body);
9555
+ const bodyMaterialization = await materializeKafkaRequestBody(
9556
+ cfg,
9557
+ maskReq,
9558
+ body,
9559
+ captureScope,
9560
+ );
9192
9561
  const requestPayload: Record<string, any> = {
9193
9562
  rid: consumeRid,
9194
9563
  method: 'AMQP_CONSUME',
@@ -9210,7 +9579,6 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9210
9579
  t: alignedNow(),
9211
9580
  }],
9212
9581
  });
9213
- captureScope.backendRequestCount += 1;
9214
9582
  scheduleAmqpTraceFlush(cfg, amqpCtx, aid, consumeRid, rawTraceEvents, maskReq, captureScope);
9215
9583
  }, { priority: 'high' });
9216
9584
  };
@@ -11526,5 +11894,6 @@ export const __reproTestHooks = {
11526
11894
  estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
11527
11895
  sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
11528
11896
  sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
11897
+ projectSemanticInfrastructureValueForTest: projectSemanticInfrastructureValue,
11529
11898
  getEventTraceValueEntriesForTest: getEventTraceValueEntries,
11530
11899
  };