@reproapp/node-sdk 0.0.48 → 0.0.50

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 { createHmac, randomUUID } from 'crypto';
5
+ import { createHash, createHmac, randomUUID } from 'crypto';
6
6
  import { createRequire } from 'module';
7
7
  import { join } from 'path';
8
8
  import { pathToFileURL } from 'url';
@@ -29,6 +29,7 @@ import {
29
29
  type RuntimePrivacyTraceContext,
30
30
  } from './privacy';
31
31
  import { projectSemanticInfrastructureValue } from './semantic-serialization';
32
+ import { limitLiveCapture, limitLiveArguments, collectCaptureOmissions, type CaptureOmission } from './live-capture-limits';
32
33
 
33
34
  /**
34
35
  * ============================= IMPORTANT =============================
@@ -380,6 +381,8 @@ type PendingTraceEventRecord = TraceEventRecord & {
380
381
  };
381
382
 
382
383
  type TraceValueCaptureRef = {
384
+ complete?: boolean;
385
+ omissions?: CaptureOmission[];
383
386
  status:
384
387
  | 'stored'
385
388
  | 'truncated-for-storage'
@@ -419,6 +422,7 @@ type TraceValueBatchEntry = {
419
422
  };
420
423
 
421
424
  type TraceMaterializationConfig = {
425
+ captureMode?: 'live' | 'staging';
422
426
  tenantId: string;
423
427
  appId: string;
424
428
  appSecret?: string;
@@ -1213,6 +1217,7 @@ type NativeMongoSuppression = {
1213
1217
  };
1214
1218
 
1215
1219
  type RequestCaptureScopeState = {
1220
+ liveCapture?: boolean;
1216
1221
  requestRid: string;
1217
1222
  transport: 'http' | 'kafka' | 'amqp' | 'background';
1218
1223
  backendRequestCount: number;
@@ -1228,6 +1233,7 @@ type RequestCaptureScopeState = {
1228
1233
  privacyPolicyFingerprint?: string;
1229
1234
  privacyStatementCount: number;
1230
1235
  emittedValueDefinitionIds: Set<string>;
1236
+ traceObservationSnapshots: Map<string, any>;
1231
1237
  };
1232
1238
 
1233
1239
  function createRequestCaptureScope(
@@ -1245,6 +1251,7 @@ function createRequestCaptureScope(
1245
1251
  return {
1246
1252
  requestRid,
1247
1253
  transport,
1254
+ liveCapture: cfg?.captureMode === 'live' || (!!cfg && cfg.captureMode !== 'staging' && !!normalizeLiveModeConfig(cfg)),
1248
1255
  // Every scope represents one captured transport request. This is an
1249
1256
  // observed expectation, not a count of successful enqueue operations.
1250
1257
  backendRequestCount: 1,
@@ -1256,6 +1263,7 @@ function createRequestCaptureScope(
1256
1263
  privacyPolicyFingerprint: privacy?.fingerprint,
1257
1264
  privacyStatementCount: privacy?.statements.length ?? 0,
1258
1265
  emittedValueDefinitionIds: new Set(),
1266
+ traceObservationSnapshots: new Map(),
1259
1267
  };
1260
1268
  }
1261
1269
 
@@ -2597,6 +2605,7 @@ async function resolveBackendLiveContext(
2597
2605
  liveMode,
2598
2606
  '/v1/live-sessions/claim',
2599
2607
  {
2608
+ liveCapture: liveMode.liveCapture,
2600
2609
  clientTime: requestStart,
2601
2610
  captureMode: 'backend-only',
2602
2611
  actorId,
@@ -2710,6 +2719,7 @@ function stripReproQueryFromUrl(rawUrl: string): string {
2710
2719
  parsed.searchParams.delete(REPRO_QUERY_SESSION_PARAM);
2711
2720
  parsed.searchParams.delete(REPRO_QUERY_ACTION_PARAM);
2712
2721
  parsed.searchParams.delete(REPRO_QUERY_START_PARAM);
2722
+ parsed.searchParams.delete('__repro_mode');
2713
2723
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
2714
2724
  } catch {
2715
2725
  return rawUrl;
@@ -2723,6 +2733,7 @@ function stripReproQueryParams(req: Request): void {
2723
2733
  delete query[REPRO_QUERY_SESSION_PARAM];
2724
2734
  delete query[REPRO_QUERY_ACTION_PARAM];
2725
2735
  delete query[REPRO_QUERY_START_PARAM];
2736
+ delete query.__repro_mode;
2726
2737
  }
2727
2738
  } catch {}
2728
2739
  try {
@@ -3403,6 +3414,112 @@ function hasMeaningfulRawTraceError(error: any): boolean {
3403
3414
  return error !== undefined && error !== null;
3404
3415
  }
3405
3416
 
3417
+ const TRACE_OBSERVATION_SNAPSHOT = Symbol('repro.traceObservationSnapshot');
3418
+
3419
+ // Trace events can remain queued after user code mutates the observed objects.
3420
+ // Intern immutable semantic snapshots per request so temporal evidence stays
3421
+ // correct without retaining another full copy for repeated unchanged values.
3422
+ function internTraceObservationSnapshot(
3423
+ snapshot: any,
3424
+ captureScope?: RequestCaptureScopeState | null,
3425
+ ): any {
3426
+ if (!captureScope || snapshot === null || typeof snapshot !== 'object') {
3427
+ return snapshot;
3428
+ }
3429
+
3430
+ const serialized = stableSerializeTraceValue(snapshot);
3431
+ if (serialized === null) return snapshot;
3432
+
3433
+ const digest = createHash('sha256')
3434
+ .update('repro-trace-observation-v1\0')
3435
+ .update(serialized)
3436
+ .digest('hex');
3437
+ const existing = captureScope.traceObservationSnapshots.get(digest);
3438
+ if (existing !== undefined) return existing;
3439
+
3440
+ captureScope.traceObservationSnapshots.set(digest, snapshot);
3441
+ return snapshot;
3442
+ }
3443
+
3444
+ function snapshotTraceValueAtObservation(
3445
+ value: any,
3446
+ captureScope?: RequestCaptureScopeState | null,
3447
+ valuePath = '$',
3448
+ target = 'trace.value',
3449
+ ): any {
3450
+ try {
3451
+ if (captureScope?.liveCapture) {
3452
+ value = target === 'trace.args' && Array.isArray(value)
3453
+ ? limitLiveArguments(value)
3454
+ : limitLiveCapture(value, valuePath).value;
3455
+ }
3456
+ const snapshot = sanitizeTraceValue(
3457
+ value,
3458
+ 0,
3459
+ new WeakMap(),
3460
+ { preserveLongStrings: true, disableTruncation: true },
3461
+ valuePath,
3462
+ );
3463
+ return internTraceObservationSnapshot(snapshot, captureScope);
3464
+ } catch {
3465
+ recordRequestCaptureFailure(
3466
+ captureScope,
3467
+ 'trace.snapshot',
3468
+ target,
3469
+ 'snapshot_failed',
3470
+ );
3471
+ return makeOmittedValue('capture-snapshot-failed');
3472
+ }
3473
+ }
3474
+
3475
+ function snapshotTraceArgsAtObservation(
3476
+ values: any,
3477
+ captureScope?: RequestCaptureScopeState | null,
3478
+ ): any[] {
3479
+ const snapshot = snapshotTraceValueAtObservation(
3480
+ normalizeRawTraceArgs(values),
3481
+ captureScope,
3482
+ '$',
3483
+ 'trace.args',
3484
+ );
3485
+ return Array.isArray(snapshot) ? snapshot : [snapshot];
3486
+ }
3487
+
3488
+ function snapshotRawTraceEventAtObservation(
3489
+ raw: any,
3490
+ captureScope?: RequestCaptureScopeState | null,
3491
+ ): any {
3492
+ if (!raw || typeof raw !== 'object' || raw[TRACE_OBSERVATION_SNAPSHOT] === true) {
3493
+ return raw;
3494
+ }
3495
+
3496
+ const snapshot = { ...raw };
3497
+ if (raw.args !== undefined) {
3498
+ snapshot.args = snapshotTraceArgsAtObservation(raw.args, captureScope);
3499
+ }
3500
+ if (raw.returnValue !== undefined) {
3501
+ snapshot.returnValue = snapshotTraceValueAtObservation(
3502
+ raw.returnValue,
3503
+ captureScope,
3504
+ '$',
3505
+ 'trace.returnValue',
3506
+ );
3507
+ }
3508
+ if (hasMeaningfulRawTraceError(raw.error)) {
3509
+ snapshot.error = snapshotTraceValueAtObservation(
3510
+ raw.error,
3511
+ captureScope,
3512
+ '$',
3513
+ 'trace.error',
3514
+ );
3515
+ }
3516
+ Object.defineProperty(snapshot, TRACE_OBSERVATION_SNAPSHOT, {
3517
+ value: true,
3518
+ enumerable: false,
3519
+ });
3520
+ return snapshot;
3521
+ }
3522
+
3406
3523
  function attachTraceValueEntry(
3407
3524
  event: TraceEventRecord | PendingTraceEventRecord,
3408
3525
  entry: TraceValueBatchEntry,
@@ -3611,6 +3728,7 @@ async function materializePendingTraceEventsAsync(
3611
3728
  appSecret: params.cfg.appSecret,
3612
3729
  appName: params.cfg.appName,
3613
3730
  privacy: params.cfg.privacy,
3731
+ captureMode: params.captureScope?.liveCapture ? 'live' : params.cfg.captureMode,
3614
3732
  },
3615
3733
  maskReq: params.maskReq,
3616
3734
  masking: params.masking,
@@ -4120,6 +4238,18 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg: ReproMiddlewareConfi
4120
4238
  );
4121
4239
  }
4122
4240
 
4241
+ function isLiveCapture(cfg: ReproMiddlewareConfig, scope?: RequestCaptureScopeState | null): boolean {
4242
+ if (scope) return scope.liveCapture === true;
4243
+ return cfg.captureMode === 'live' || (cfg.captureMode !== 'staging' && getCtx().captureScope?.liveCapture === true);
4244
+ }
4245
+
4246
+ function constrainLiveCapture(value: any, cfg: ReproMiddlewareConfig, target: string, scope?: RequestCaptureScopeState | null): any {
4247
+ if (!isLiveCapture(cfg, scope)) return value;
4248
+ return target === 'trace.args' && Array.isArray(value)
4249
+ ? limitLiveArguments(value)
4250
+ : limitLiveCapture(value).value;
4251
+ }
4252
+
4123
4253
  function applyPrivacyThenMask(
4124
4254
  target: RuntimePrivacySurface,
4125
4255
  value: any,
@@ -4130,6 +4260,7 @@ function applyPrivacyThenMask(
4130
4260
  privacy: NormalizedRuntimePrivacyPolicy | null,
4131
4261
  db?: RuntimePrivacyDbContext | null,
4132
4262
  ) {
4263
+ value = constrainLiveCapture(value, cfg, target);
4133
4264
  if (shouldCapturePrivacyRaw(cfg)) {
4134
4265
  return value;
4135
4266
  }
@@ -4160,6 +4291,7 @@ async function applyPrivacyThenMaskAsync(
4160
4291
  privacy: NormalizedRuntimePrivacyPolicy | null,
4161
4292
  db?: RuntimePrivacyDbContext | null,
4162
4293
  ) {
4294
+ value = constrainLiveCapture(value, cfg, target);
4163
4295
  if (shouldCapturePrivacyRaw(cfg)) {
4164
4296
  return value;
4165
4297
  }
@@ -4239,6 +4371,7 @@ function materializeInlinePrivacyValue(
4239
4371
  db?: RuntimePrivacyDbContext | null,
4240
4372
  captureScope?: RequestCaptureScopeState | null,
4241
4373
  ): InlinePrivacyMaterializationResult {
4374
+ value = constrainLiveCapture(value, cfg, target, captureScope);
4242
4375
  if (value === undefined) {
4243
4376
  return { value };
4244
4377
  }
@@ -4278,6 +4411,7 @@ async function materializeInlinePrivacyValueAsync(
4278
4411
  db?: RuntimePrivacyDbContext | null,
4279
4412
  captureScope?: RequestCaptureScopeState | null,
4280
4413
  ): Promise<InlinePrivacyMaterializationResult> {
4414
+ value = constrainLiveCapture(value, cfg, target, captureScope);
4281
4415
  if (value === undefined) {
4282
4416
  return { value };
4283
4417
  }
@@ -5446,6 +5580,9 @@ function createCapturedValueEntry(
5446
5580
  ): { captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined {
5447
5581
  if (!__FULL_VALUE_CAPTURE_ENABLED) return undefined;
5448
5582
 
5583
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
5584
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? collectCaptureOmissions(limitedRaw) : [];
5585
+
5449
5586
  const preferredLimits: FullValueCloneLimits = {
5450
5587
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
5451
5588
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -5455,7 +5592,7 @@ function createCapturedValueEntry(
5455
5592
  const buildMaskedCandidate = (limits: FullValueCloneLimits) => {
5456
5593
  const projected = params.target === 'request.headers'
5457
5594
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
5458
- : cloneFullValueCandidate(params.rawValue, params.capture, limits);
5595
+ : cloneFullValueCandidate(limitedRaw, params.capture, limits);
5459
5596
  if (projected === undefined) return undefined;
5460
5597
  return applyPrivacyThenMask(
5461
5598
  params.target,
@@ -5476,7 +5613,7 @@ function createCapturedValueEntry(
5476
5613
  }
5477
5614
 
5478
5615
  const candidateSerialized = stableSerializeTraceValue(candidate);
5479
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
5616
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
5480
5617
  return undefined;
5481
5618
  }
5482
5619
 
@@ -5502,6 +5639,7 @@ function createCapturedValueEntry(
5502
5639
  status: 'stored',
5503
5640
  valueId,
5504
5641
  projectedKind: entry.projectedKind,
5642
+ ...(omissions.length ? { complete: false, omissions } : {}),
5505
5643
  },
5506
5644
  entry,
5507
5645
  };
@@ -5532,6 +5670,9 @@ async function createCapturedValueEntryAsync(
5532
5670
  ): Promise<{ captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined> {
5533
5671
  if (!__FULL_VALUE_CAPTURE_ENABLED) return undefined;
5534
5672
 
5673
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
5674
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? collectCaptureOmissions(limitedRaw) : [];
5675
+
5535
5676
  const preferredLimits: FullValueCloneLimits = {
5536
5677
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
5537
5678
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -5541,7 +5682,7 @@ async function createCapturedValueEntryAsync(
5541
5682
  const buildMaskedCandidate = async (limits: FullValueCloneLimits) => {
5542
5683
  const projected = params.target === 'request.headers'
5543
5684
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
5544
- : await cloneFullValueCandidateAsync(params.rawValue, params.capture, limits);
5685
+ : await cloneFullValueCandidateAsync(limitedRaw, params.capture, limits);
5545
5686
  if (projected === undefined) return undefined;
5546
5687
  return applyPrivacyThenMaskAsync(
5547
5688
  params.target,
@@ -5562,7 +5703,7 @@ async function createCapturedValueEntryAsync(
5562
5703
  }
5563
5704
 
5564
5705
  const candidateSerialized = stableSerializeTraceValue(candidate);
5565
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
5706
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
5566
5707
  return undefined;
5567
5708
  }
5568
5709
 
@@ -5588,6 +5729,7 @@ async function createCapturedValueEntryAsync(
5588
5729
  status: 'stored',
5589
5730
  valueId,
5590
5731
  projectedKind: entry.projectedKind,
5732
+ ...(omissions.length ? { complete: false, omissions } : {}),
5591
5733
  },
5592
5734
  entry,
5593
5735
  };
@@ -5789,6 +5931,8 @@ async function maybeCaptureDbValueFromCandidatesAsync(
5789
5931
  // reproMiddleware — unchanged behavior + passive per-request trace
5790
5932
  // ===================================================================
5791
5933
  export type ReproMiddlewareConfig = IngestClientConfig & {
5934
+ /** Propagated recording mode. A tagged staging request never inherits live limits. */
5935
+ captureMode?: 'live' | 'staging';
5792
5936
  /** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
5793
5937
  apiBase?: string;
5794
5938
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
@@ -5820,6 +5964,11 @@ export type ReproLiveActorDecision = ReproLiveActorArgs & {
5820
5964
  };
5821
5965
 
5822
5966
  export type ReproLiveModeConfig = {
5967
+ /** Stable chain sampling. Runtime updates apply to new chains; targets are evaluated on each claim. */
5968
+ liveCapture?: {
5969
+ sampleRate?: number;
5970
+ targets?: { actorIds?: string[]; labels?: Record<string, string>; expiresAt: number }[];
5971
+ };
5823
5972
  enabled?: boolean;
5824
5973
  backendOnly?: boolean;
5825
5974
  /**
@@ -6009,6 +6158,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6009
6158
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
6010
6159
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
6011
6160
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
6161
+ const queryCaptureMode = readRequestQueryParam(req, '__repro_mode');
6012
6162
  if (querySid || queryAid || queryTs !== null) {
6013
6163
  stripReproQueryParams(req);
6014
6164
  }
@@ -6046,21 +6196,24 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6046
6196
  res.on('close', endTrackedClientRequest);
6047
6197
  return next(); // only capture tagged requests
6048
6198
  }
6049
- const captureCfg: ReproMiddlewareConfig = backendLiveChild
6050
- ? {
6199
+ let captureCfg: ReproMiddlewareConfig = cfg;
6200
+ if (backendLiveChild) {
6201
+ captureCfg = {
6051
6202
  ...cfg,
6052
6203
  actorId: backendLiveChild.actorId,
6053
6204
  actorType: backendLiveChild.actorType,
6054
6205
  actorLabels: backendLiveChild.actorLabels,
6055
- }
6056
- : taggedActorIdentity
6057
- ? {
6058
- ...cfg,
6059
- actorId: taggedActorIdentity.actorId,
6060
- actorType: taggedActorIdentity.actorType,
6061
- actorLabels: taggedActorIdentity.actorLabels,
6062
- }
6063
- : cfg;
6206
+ };
6207
+ shareRuntimePrivacyState(cfg, captureCfg);
6208
+ } else if (taggedActorIdentity) {
6209
+ captureCfg = {
6210
+ ...cfg,
6211
+ actorId: taggedActorIdentity.actorId,
6212
+ actorType: taggedActorIdentity.actorType,
6213
+ actorLabels: taggedActorIdentity.actorLabels,
6214
+ };
6215
+ shareRuntimePrivacyState(cfg, captureCfg);
6216
+ }
6064
6217
  if (taggedActorIdentity) {
6065
6218
  post(captureCfg, sid, {
6066
6219
  entries: [{
@@ -6075,6 +6228,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6075
6228
  }],
6076
6229
  }, { priority: 'high' });
6077
6230
  }
6231
+ const captureMode = backendLiveContext || req.headers['x-repro-capture-mode'] === 'live' || queryCaptureMode === 'live'
6232
+ ? 'live' : 'staging';
6233
+ const modeConfig = { ...captureCfg, captureMode } as ReproMiddlewareConfig;
6234
+ shareRuntimePrivacyState(captureCfg, modeConfig);
6235
+ captureCfg = modeConfig;
6078
6236
  const resolvePrivacy = (): NormalizedRuntimePrivacyPolicy | null =>
6079
6237
  getRuntimePrivacyState(captureCfg).policy ?? null;
6080
6238
 
@@ -6588,17 +6746,27 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6588
6746
  evt.__reproPending = {
6589
6747
  ...(ev.args !== undefined
6590
6748
  ? {
6591
- argsRaw: normalizeRawTraceArgs(ev.args),
6749
+ argsRaw: snapshotTraceArgsAtObservation(ev.args, captureScope),
6592
6750
  }
6593
6751
  : {}),
6594
6752
  ...(ev.returnValue !== undefined
6595
6753
  ? {
6596
- returnValueRaw: ev.returnValue,
6754
+ returnValueRaw: snapshotTraceValueAtObservation(
6755
+ ev.returnValue,
6756
+ captureScope,
6757
+ '$',
6758
+ 'trace.returnValue',
6759
+ ),
6597
6760
  }
6598
6761
  : {}),
6599
6762
  ...(hasErrorPayload
6600
6763
  ? {
6601
- errorRaw: ev.error,
6764
+ errorRaw: snapshotTraceValueAtObservation(
6765
+ ev.error,
6766
+ captureScope,
6767
+ '$',
6768
+ 'trace.error',
6769
+ ),
6602
6770
  }
6603
6771
  : {}),
6604
6772
  };
@@ -7629,6 +7797,7 @@ function patchNodeFetchForPropagation() {
7629
7797
  if (runtime.aid && !hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID)) {
7630
7798
  setHttpHeader(headers, KAFKA_HEADER_ACTION_ID, runtime.aid);
7631
7799
  }
7800
+ if (runtime.sid) setHttpHeader(headers, 'x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
7632
7801
  if (runtime.traceId && !hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID)) {
7633
7802
  setHttpHeader(headers, KAFKA_HEADER_TRACE_ID, runtime.traceId);
7634
7803
  }
@@ -7683,6 +7852,7 @@ function patchNodeHttpModuleRequest(mod: any) {
7683
7852
  maybeSet(REQUEST_START_HEADER, String(alignedNow()));
7684
7853
  maybeSet(KAFKA_HEADER_SESSION_ID, runtime.sid);
7685
7854
  maybeSet(KAFKA_HEADER_ACTION_ID, runtime.aid);
7855
+ if (runtime.sid) maybeSet('x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
7686
7856
  maybeSet(KAFKA_HEADER_TRACE_ID, runtime.traceId);
7687
7857
  maybeSet(KAFKA_HEADER_PARENT_SPAN_ID, runtime.parentSpanId);
7688
7858
  maybeSet(KAFKA_HEADER_REQUEST_ID, runtime.requestRid);
@@ -7710,6 +7880,7 @@ type KafkaHeaderValue = string | Buffer | null | undefined;
7710
7880
  type KafkaHeaderMap = Record<string, KafkaHeaderValue>;
7711
7881
  type KafkaContextResolver = () => { sid?: string; aid?: string } | undefined;
7712
7882
  type KafkaPropagationContext = {
7883
+ captureMode?: 'live' | 'staging';
7713
7884
  sid: string | null;
7714
7885
  aid: string | null;
7715
7886
  traceId: string | null;
@@ -7874,6 +8045,11 @@ async function materializeKafkaRequestBody(
7874
8045
  body: Record<string, any>,
7875
8046
  captureScope?: RequestCaptureScopeState | null,
7876
8047
  ): Promise<InlinePrivacyMaterializationResult> {
8048
+ // Broker bodies have no separate full-value reference. Returning the trace
8049
+ // inline preview here would permanently replace message arrays with 20 items.
8050
+ if (shouldCapturePrivacyRaw(cfg)) {
8051
+ return { value: constrainLiveCapture(sanitizeInlinePrivacyValue(body), cfg, 'request.body', captureScope) };
8052
+ }
7877
8053
  return materializeInlinePrivacyValueAsync(
7878
8054
  'request.body',
7879
8055
  sanitizeInlinePrivacyValue(body),
@@ -7937,7 +8113,8 @@ function resolveKafkaContext(cfg: KafkaJsPatchConfig, headers: any): KafkaPropag
7937
8113
  null;
7938
8114
  const requestRid = readKafkaHeader(headers, KAFKA_HEADER_REQUEST_ID);
7939
8115
 
7940
- return { sid, aid, traceId, parentSpanId, requestRid };
8116
+ return { sid, aid, traceId, parentSpanId, requestRid,
8117
+ captureMode: readKafkaHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
7941
8118
  }
7942
8119
 
7943
8120
  function withOptionalTraceContext<T>(traceId: string | null, fn: () => Promise<T>): Promise<T> {
@@ -8094,6 +8271,8 @@ function recordKafkaTraceEvent(
8094
8271
  function buildPendingKafkaTraceEvent(raw: any): PendingTraceEventRecord | null {
8095
8272
  if (!raw || (raw.type !== 'enter' && raw.type !== 'exit')) return null;
8096
8273
 
8274
+ raw = snapshotRawTraceEventAtObservation(raw, getCtx().captureScope);
8275
+
8097
8276
  const sourceFileForLibrary =
8098
8277
  typeof raw.sourceFile === 'string' && raw.sourceFile
8099
8278
  ? String(raw.sourceFile)
@@ -8302,6 +8481,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8302
8481
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
8303
8482
  [KAFKA_HEADER_SESSION_ID]: sid,
8304
8483
  [KAFKA_HEADER_ACTION_ID]: aid,
8484
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
8305
8485
  }),
8306
8486
  };
8307
8487
  });
@@ -8434,6 +8614,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8434
8614
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
8435
8615
  [KAFKA_HEADER_SESSION_ID]: sid,
8436
8616
  [KAFKA_HEADER_ACTION_ID]: aid,
8617
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
8437
8618
  }),
8438
8619
  };
8439
8620
  }),
@@ -8550,6 +8731,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8550
8731
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
8551
8732
  const traceId = consumeRid;
8552
8733
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8734
+ captureScope.liveCapture = runtime.captureMode === 'live';
8553
8735
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8554
8736
  const rawTraceEvents: any[] = [];
8555
8737
  const maskReq: MaskRequestContext = {
@@ -8574,7 +8756,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8574
8756
  try {
8575
8757
  unsubscribe = __TRACER__.tracer.on((event: any) => {
8576
8758
  if (!event || event.traceId !== traceId) return;
8577
- rawTraceEvents.push(event);
8759
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
8578
8760
  });
8579
8761
  } catch {}
8580
8762
  }
@@ -8660,6 +8842,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8660
8842
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
8661
8843
  const traceId = consumeRid;
8662
8844
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8845
+ captureScope.liveCapture = runtime.captureMode === 'live';
8663
8846
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8664
8847
  const rawTraceEvents: any[] = [];
8665
8848
  const maskReq: MaskRequestContext = {
@@ -8684,7 +8867,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8684
8867
  try {
8685
8868
  unsubscribe = __TRACER__.tracer.on((event: any) => {
8686
8869
  if (!event || event.traceId !== traceId) return;
8687
- rawTraceEvents.push(event);
8870
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
8688
8871
  });
8689
8872
  } catch {}
8690
8873
  }
@@ -8854,6 +9037,7 @@ export function patchKafkaJs(cfg: KafkaJsPatchConfig) {
8854
9037
  type AmqpHeaderMap = Record<string, any>;
8855
9038
  type AmqpContextResolver = () => { sid?: string; aid?: string } | undefined;
8856
9039
  type AmqpPropagationContext = {
9040
+ captureMode?: 'live' | 'staging';
8857
9041
  sid: string | null;
8858
9042
  aid: string | null;
8859
9043
  traceId: string | null;
@@ -9122,7 +9306,8 @@ function resolveAmqpContext(cfg: AmqplibPatchConfig, headers: any): AmqpPropagat
9122
9306
  null;
9123
9307
  const requestRid = readAmqpHeader(headers, KAFKA_HEADER_REQUEST_ID);
9124
9308
 
9125
- return { sid, aid, traceId, parentSpanId, requestRid };
9309
+ return { sid, aid, traceId, parentSpanId, requestRid,
9310
+ captureMode: readAmqpHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
9126
9311
  }
9127
9312
 
9128
9313
  function withOptionalTraceContextSync<T>(traceId: string | null, fn: () => T): T {
@@ -9360,6 +9545,7 @@ function patchAmqplibPublishMethod(
9360
9545
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
9361
9546
  [KAFKA_HEADER_SESSION_ID]: sid,
9362
9547
  [KAFKA_HEADER_ACTION_ID]: aid,
9548
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
9363
9549
  };
9364
9550
  const shouldPropagate = hasAmqpPropagationValues(propagationValues);
9365
9551
  const callbackIndex = args.length > 0 && typeof args[args.length - 1] === 'function'
@@ -9468,6 +9654,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9468
9654
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
9469
9655
  const traceId = consumeRid;
9470
9656
  const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
9657
+ captureScope.liveCapture = runtime.captureMode === 'live';
9471
9658
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9472
9659
  const rawTraceEvents: any[] = [];
9473
9660
  const maskReq: MaskRequestContext = {
@@ -9492,7 +9679,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9492
9679
  try {
9493
9680
  unsubscribe = __TRACER__.tracer.on((event: any) => {
9494
9681
  if (!event || event.traceId !== traceId) return;
9495
- rawTraceEvents.push(event);
9682
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
9496
9683
  });
9497
9684
  } catch {}
9498
9685
  };
@@ -61,6 +61,8 @@ let backoffMs = 0;
61
61
  let criticalBackoffMs = 0;
62
62
  let shouldDeferDrain: (() => boolean) | null = null;
63
63
  let deferDrainMs = DEFAULT_DEFER_DRAIN_MS;
64
+ const throttledUntil = new Map<string, number>();
65
+ const throttleDelay = (config: IngestClientConfig) => Math.max(0, (throttledUntil.get(resolveIngestUrl(config)) ?? 0) - Date.now());
64
66
 
65
67
  export const configureIngestQueueDrain = (options: {
66
68
  shouldDefer?: (() => boolean) | null;
@@ -220,7 +222,9 @@ const sendIngestEntries = async (
220
222
  config: IngestClientConfig,
221
223
  sessionId: string,
222
224
  entries: LegacyEntry[],
223
- ): Promise<boolean> => {
225
+ ): Promise<boolean | 'throttled' | 'denied'> => {
226
+ if (throttleDelay(config) > 0) return 'throttled';
227
+ throttledUntil.delete(resolveIngestUrl(config));
224
228
  if (!sessionId || !Array.isArray(entries) || entries.length === 0) {
225
229
  return true;
226
230
  }
@@ -255,6 +259,18 @@ const sendIngestEntries = async (
255
259
  body: JSON.stringify({ events }),
256
260
  ...(controller ? { signal: controller.signal } : {}),
257
261
  });
262
+ if (response.status === 429) {
263
+ const retryAfter = response.headers?.get?.('retry-after');
264
+ const seconds = retryAfter ? Number(retryAfter) : NaN;
265
+ const date = retryAfter ? Date.parse(retryAfter) : NaN;
266
+ const delay = Number.isFinite(seconds) ? seconds * 1000 : Number.isFinite(date) ? date - Date.now() : 1000;
267
+ throttledUntil.set(resolveIngestUrl(config), Date.now() + Math.max(1000, Math.min(86400_000, delay)));
268
+ return 'throttled';
269
+ }
270
+ if (response.status === 402 || response.status === 410) {
271
+ console.warn(`[repro] Upload refused for ${sessionId}: ${response.status === 402 ? 'capture quota exhausted' : 'capture deleted, expired or failed'}. This batch will not be retried.`);
272
+ return 'denied';
273
+ }
258
274
  return response.ok;
259
275
  } catch {
260
276
  // swallow in SDK
@@ -317,7 +333,11 @@ const drainCriticalIngestQueue = async (): Promise<void> => {
317
333
  selected.batch.sessionId,
318
334
  selected.batch.entries,
319
335
  );
320
- if (ok) {
336
+ if (ok === 'throttled') {
337
+ scheduleCriticalDrain(throttleDelay(selected.batch.config));
338
+ return;
339
+ }
340
+ if (ok === true || ok === 'denied') {
321
341
  criticalIngestQueue.splice(0, selected.queueItems);
322
342
  criticalBackoffMs = 0;
323
343
  continue;
@@ -362,7 +382,11 @@ const drainIngestQueue = async (): Promise<void> => {
362
382
  }
363
383
  const batch = selected.batch;
364
384
  const ok = await sendIngestEntries(batch.config, batch.sessionId, batch.entries);
365
- if (ok) {
385
+ if (ok === 'throttled') {
386
+ scheduleDrain(throttleDelay(batch.config));
387
+ return;
388
+ }
389
+ if (ok === true || ok === 'denied') {
366
390
  ingestQueue.splice(0, selected.queueItems);
367
391
  backoffMs = 0;
368
392
  continue;