@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/README.md CHANGED
@@ -107,6 +107,7 @@ await initRepro({
107
107
  appName: "Acme Web",
108
108
  serviceName: "api",
109
109
  apiBase: process.env.REPRO_API_BASE,
110
+ ingestBase: process.env.REPRO_INGEST_BASE!,
110
111
  });
111
112
  ```
112
113
 
@@ -123,14 +124,56 @@ await initRepro({
123
124
  liveMode: {
124
125
  enabled: true,
125
126
  backendOnly: true,
126
- defineActorId: ({ req, ip }) => req.headers["x-user-id"] ?? (ip ? `ip:${ip}` : null),
127
+ // Trust only a header that your authenticated gateway overwrites.
128
+ defineActorId: ({ req }) => req.headers["x-user-id"] ?? null,
127
129
  shouldCapture: ({ path }) => path.startsWith("/api/") && !path.includes("/health"),
128
130
  actorType: "user",
129
131
  actorLabels: ({ path }) => ({ routeGroup: path.split("/").filter(Boolean)[0] ?? "api" }),
132
+ liveCapture: { sampleRate: 0.1, targets: [] },
130
133
  },
131
134
  });
132
135
  ```
133
136
 
137
+ ### Sampling, limits and runtime targets
138
+
139
+ Backend-owned live capture accepts `liveMode.liveCapture.sampleRate` from 0 to 1
140
+ (default 1 for compatibility). A chain keeps its sample decision until five minutes
141
+ of inactivity. Actor identity separates chains; three-minute child segments admit
142
+ new work. Admitted requests and their queue/DB/trace tails stay in the original
143
+ segment even when delivery finishes later. Browser-correlated captures use the
144
+ browser's admission decision, rather than sampling a second time.
145
+
146
+ Keep the `liveCapture` object in your runtime configuration and replace its `targets`
147
+ array when a support case starts or ends. Example target entries:
148
+
149
+ ```ts
150
+ const liveCapture = {
151
+ sampleRate: 0.1,
152
+ targets: [
153
+ { actorIds: ['user-123'], expiresAt: Date.now() + 3600000 },
154
+ { labels: { routeGroup: 'reports' }, expiresAt: Date.now() + 3600000 },
155
+ ],
156
+ };
157
+ // Pass this object as liveMode.liveCapture; update liveCapture.targets at runtime.
158
+ ```
159
+
160
+ Targets override sampling, including rate 0, but never quota admission. All labels
161
+ in one target must match; actor IDs and label selectors are alternatives. Use a
162
+ trusted identity source, not arbitrary client-controlled headers.
163
+
164
+ Live business values have depth 10, 100 keys per object, and 100 entries per array,
165
+ Map, Set or native collection. Partial captures carry explicit omitted path/reason/
166
+ count metadata. `captureRaw: true` bypasses privacy processing, not live limits.
167
+ Staging business values are unchanged. Both modes project recognized runtime
168
+ infrastructure (connections, pools, framework/configuration machinery) into compact
169
+ descriptions; business documents, queries, results and application payloads remain
170
+ evidence. Unknown objects are treated as business data.
171
+
172
+ HTTP 429 honors `Retry-After` without dropping critical uploads. Quota refusal (402)
173
+ and deleted/failed capture refusal (410) stop retrying that refused batch; business
174
+ requests continue. Quota-enabled installations must use the configured collector,
175
+ not the legacy direct API evidence endpoints.
176
+
134
177
  ## Manual Wiring
135
178
 
136
179
  Use the lower-level exports when auto-attach is not appropriate or when you need
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ import type { Schema } from 'mongoose';
28
28
  import { type IngestClientConfig } from './ingest/client';
29
29
  import { type NormalizedRuntimePrivacyPolicy, type RuntimePrivacyConfig, type RuntimePrivacyDbContext, type RuntimePrivacySurface } from './privacy';
30
30
  import { projectSemanticInfrastructureValue } from './semantic-serialization';
31
+ import { type CaptureOmission } from './live-capture-limits';
31
32
  type SpanContext = {
32
33
  traceId: string | null;
33
34
  spanId: string | number | null;
@@ -88,6 +89,8 @@ type PendingTraceEventRecord = TraceEventRecord & {
88
89
  __reproTraceValueEntries?: TraceValueBatchEntry[];
89
90
  };
90
91
  type TraceValueCaptureRef = {
92
+ complete?: boolean;
93
+ omissions?: CaptureOmission[];
91
94
  status: 'stored' | 'truncated-for-storage' | 'skipped-non-meaningful' | 'failed-policy';
92
95
  valueId?: string;
93
96
  projectedKind?: string;
@@ -107,6 +110,7 @@ type TraceValueBatchEntry = {
107
110
  eventIndex?: number;
108
111
  };
109
112
  type TraceMaterializationConfig = {
113
+ captureMode?: 'live' | 'staging';
110
114
  tenantId: string;
111
115
  appId: string;
112
116
  appSecret?: string;
@@ -284,6 +288,7 @@ type NativeMongoSuppression = {
284
288
  ownerStore?: Ctx;
285
289
  };
286
290
  type RequestCaptureScopeState = {
291
+ liveCapture?: boolean;
287
292
  requestRid: string;
288
293
  transport: 'http' | 'kafka' | 'amqp' | 'background';
289
294
  backendRequestCount: number;
@@ -299,6 +304,7 @@ type RequestCaptureScopeState = {
299
304
  privacyPolicyFingerprint?: string;
300
305
  privacyStatementCount: number;
301
306
  emittedValueDefinitionIds: Set<string>;
307
+ traceObservationSnapshots: Map<string, any>;
302
308
  };
303
309
  type BackendLiveTimingConfig = {
304
310
  sessionMaxDurationMs: number;
@@ -341,6 +347,8 @@ declare function getEventTraceValueEntries(event: TraceEventRecord | PendingTrac
341
347
  declare function estimateTraceBatchSerializedBytes(batch: TraceEventRecord[], requestRid: string, actionId: string | null | undefined, batchIndex: number, previouslyEmittedValueIds?: ReadonlySet<string>): number;
342
348
  declare function chunkTraceEventsForTransport(events: TraceEventRecord[], requestRid: string, actionId: string | null | undefined): TraceEventRecord[][];
343
349
  export type ReproMiddlewareConfig = IngestClientConfig & {
350
+ /** Propagated recording mode. A tagged staging request never inherits live limits. */
351
+ captureMode?: 'live' | 'staging';
344
352
  /** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
345
353
  apiBase?: string;
346
354
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
@@ -369,6 +377,15 @@ export type ReproLiveActorDecision = ReproLiveActorArgs & {
369
377
  actorLabels?: Record<string, string>;
370
378
  };
371
379
  export type ReproLiveModeConfig = {
380
+ /** Stable chain sampling. Runtime updates apply to new chains; targets are evaluated on each claim. */
381
+ liveCapture?: {
382
+ sampleRate?: number;
383
+ targets?: {
384
+ actorIds?: string[];
385
+ labels?: Record<string, string>;
386
+ expiresAt: number;
387
+ }[];
388
+ };
372
389
  enabled?: boolean;
373
390
  backendOnly?: boolean;
374
391
  /**
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ const url_1 = require("url");
10
10
  const client_1 = require("./ingest/client");
11
11
  const privacy_1 = require("./privacy");
12
12
  const semantic_serialization_1 = require("./semantic-serialization");
13
+ const live_capture_limits_1 = require("./live-capture-limits");
13
14
  /**
14
15
  * ============================= IMPORTANT =============================
15
16
  * We snapshot Mongoose core methods BEFORE tracer init, then re-apply them
@@ -828,6 +829,7 @@ function createRequestCaptureScope(requestRid, transport, cfg) {
828
829
  return {
829
830
  requestRid,
830
831
  transport,
832
+ liveCapture: cfg?.captureMode === 'live' || (!!cfg && cfg.captureMode !== 'staging' && !!normalizeLiveModeConfig(cfg)),
831
833
  // Every scope represents one captured transport request. This is an
832
834
  // observed expectation, not a count of successful enqueue operations.
833
835
  backendRequestCount: 1,
@@ -839,6 +841,7 @@ function createRequestCaptureScope(requestRid, transport, cfg) {
839
841
  privacyPolicyFingerprint: privacy?.fingerprint,
840
842
  privacyStatementCount: privacy?.statements.length ?? 0,
841
843
  emittedValueDefinitionIds: new Set(),
844
+ traceObservationSnapshots: new Map(),
842
845
  };
843
846
  }
844
847
  function recordRequestCaptureFailure(scope, stage, target, reason) {
@@ -2053,6 +2056,7 @@ async function resolveBackendLiveContext(cfg, req, requestStart) {
2053
2056
  }
2054
2057
  const { actorId, actorType, actorLabels } = identity;
2055
2058
  const claim = await postLiveApiJson(cfg, liveMode, '/v1/live-sessions/claim', {
2059
+ liveCapture: liveMode.liveCapture,
2056
2060
  clientTime: requestStart,
2057
2061
  captureMode: 'backend-only',
2058
2062
  actorId,
@@ -2155,6 +2159,7 @@ function stripReproQueryFromUrl(rawUrl) {
2155
2159
  parsed.searchParams.delete(REPRO_QUERY_SESSION_PARAM);
2156
2160
  parsed.searchParams.delete(REPRO_QUERY_ACTION_PARAM);
2157
2161
  parsed.searchParams.delete(REPRO_QUERY_START_PARAM);
2162
+ parsed.searchParams.delete('__repro_mode');
2158
2163
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
2159
2164
  }
2160
2165
  catch {
@@ -2168,6 +2173,7 @@ function stripReproQueryParams(req) {
2168
2173
  delete query[REPRO_QUERY_SESSION_PARAM];
2169
2174
  delete query[REPRO_QUERY_ACTION_PARAM];
2170
2175
  delete query[REPRO_QUERY_START_PARAM];
2176
+ delete query.__repro_mode;
2171
2177
  }
2172
2178
  }
2173
2179
  catch { }
@@ -2810,6 +2816,66 @@ function normalizeRawTraceArgs(values) {
2810
2816
  function hasMeaningfulRawTraceError(error) {
2811
2817
  return error !== undefined && error !== null;
2812
2818
  }
2819
+ const TRACE_OBSERVATION_SNAPSHOT = Symbol('repro.traceObservationSnapshot');
2820
+ // Trace events can remain queued after user code mutates the observed objects.
2821
+ // Intern immutable semantic snapshots per request so temporal evidence stays
2822
+ // correct without retaining another full copy for repeated unchanged values.
2823
+ function internTraceObservationSnapshot(snapshot, captureScope) {
2824
+ if (!captureScope || snapshot === null || typeof snapshot !== 'object') {
2825
+ return snapshot;
2826
+ }
2827
+ const serialized = stableSerializeTraceValue(snapshot);
2828
+ if (serialized === null)
2829
+ return snapshot;
2830
+ const digest = (0, crypto_1.createHash)('sha256')
2831
+ .update('repro-trace-observation-v1\0')
2832
+ .update(serialized)
2833
+ .digest('hex');
2834
+ const existing = captureScope.traceObservationSnapshots.get(digest);
2835
+ if (existing !== undefined)
2836
+ return existing;
2837
+ captureScope.traceObservationSnapshots.set(digest, snapshot);
2838
+ return snapshot;
2839
+ }
2840
+ function snapshotTraceValueAtObservation(value, captureScope, valuePath = '$', target = 'trace.value') {
2841
+ try {
2842
+ if (captureScope?.liveCapture) {
2843
+ value = target === 'trace.args' && Array.isArray(value)
2844
+ ? (0, live_capture_limits_1.limitLiveArguments)(value)
2845
+ : (0, live_capture_limits_1.limitLiveCapture)(value, valuePath).value;
2846
+ }
2847
+ const snapshot = sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true }, valuePath);
2848
+ return internTraceObservationSnapshot(snapshot, captureScope);
2849
+ }
2850
+ catch {
2851
+ recordRequestCaptureFailure(captureScope, 'trace.snapshot', target, 'snapshot_failed');
2852
+ return makeOmittedValue('capture-snapshot-failed');
2853
+ }
2854
+ }
2855
+ function snapshotTraceArgsAtObservation(values, captureScope) {
2856
+ const snapshot = snapshotTraceValueAtObservation(normalizeRawTraceArgs(values), captureScope, '$', 'trace.args');
2857
+ return Array.isArray(snapshot) ? snapshot : [snapshot];
2858
+ }
2859
+ function snapshotRawTraceEventAtObservation(raw, captureScope) {
2860
+ if (!raw || typeof raw !== 'object' || raw[TRACE_OBSERVATION_SNAPSHOT] === true) {
2861
+ return raw;
2862
+ }
2863
+ const snapshot = { ...raw };
2864
+ if (raw.args !== undefined) {
2865
+ snapshot.args = snapshotTraceArgsAtObservation(raw.args, captureScope);
2866
+ }
2867
+ if (raw.returnValue !== undefined) {
2868
+ snapshot.returnValue = snapshotTraceValueAtObservation(raw.returnValue, captureScope, '$', 'trace.returnValue');
2869
+ }
2870
+ if (hasMeaningfulRawTraceError(raw.error)) {
2871
+ snapshot.error = snapshotTraceValueAtObservation(raw.error, captureScope, '$', 'trace.error');
2872
+ }
2873
+ Object.defineProperty(snapshot, TRACE_OBSERVATION_SNAPSHOT, {
2874
+ value: true,
2875
+ enumerable: false,
2876
+ });
2877
+ return snapshot;
2878
+ }
2813
2879
  function attachTraceValueEntry(event, entry) {
2814
2880
  const current = event[TRACE_FULL_VALUE_ENTRY_SYMBOL];
2815
2881
  if (Array.isArray(current)) {
@@ -2962,6 +3028,7 @@ async function materializePendingTraceEventsAsync(events, params) {
2962
3028
  appSecret: params.cfg.appSecret,
2963
3029
  appName: params.cfg.appName,
2964
3030
  privacy: params.cfg.privacy,
3031
+ captureMode: params.captureScope?.liveCapture ? 'live' : params.cfg.captureMode,
2965
3032
  },
2966
3033
  maskReq: params.maskReq,
2967
3034
  masking: params.masking,
@@ -3383,7 +3450,20 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg) {
3383
3450
  }
3384
3451
  throw new Error(`Runtime privacy policy failed to load during startup from ${apiBase} for env ${state.environment} (${lastFailure}).`);
3385
3452
  }
3453
+ function isLiveCapture(cfg, scope) {
3454
+ if (scope)
3455
+ return scope.liveCapture === true;
3456
+ return cfg.captureMode === 'live' || (cfg.captureMode !== 'staging' && getCtx().captureScope?.liveCapture === true);
3457
+ }
3458
+ function constrainLiveCapture(value, cfg, target, scope) {
3459
+ if (!isLiveCapture(cfg, scope))
3460
+ return value;
3461
+ return target === 'trace.args' && Array.isArray(value)
3462
+ ? (0, live_capture_limits_1.limitLiveArguments)(value)
3463
+ : (0, live_capture_limits_1.limitLiveCapture)(value).value;
3464
+ }
3386
3465
  function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db) {
3466
+ value = constrainLiveCapture(value, cfg, target);
3387
3467
  if (shouldCapturePrivacyRaw(cfg)) {
3388
3468
  return value;
3389
3469
  }
@@ -3404,6 +3484,7 @@ function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy,
3404
3484
  return applyMasking(target, transformed, req, trace, masking);
3405
3485
  }
3406
3486
  async function applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db) {
3487
+ value = constrainLiveCapture(value, cfg, target);
3407
3488
  if (shouldCapturePrivacyRaw(cfg)) {
3408
3489
  return value;
3409
3490
  }
@@ -3450,6 +3531,7 @@ function limitRawInlinePrivacyValue(target, value) {
3450
3531
  return limited.skipped ? limited : null;
3451
3532
  }
3452
3533
  function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
3534
+ value = constrainLiveCapture(value, cfg, target, captureScope);
3453
3535
  if (value === undefined) {
3454
3536
  return { value };
3455
3537
  }
@@ -3473,6 +3555,7 @@ function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking,
3473
3555
  }
3474
3556
  }
3475
3557
  async function materializeInlinePrivacyValueAsync(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
3558
+ value = constrainLiveCapture(value, cfg, target, captureScope);
3476
3559
  if (value === undefined) {
3477
3560
  return { value };
3478
3561
  }
@@ -4449,6 +4532,8 @@ function boundTraceEventsForTransport(events) {
4449
4532
  function createCapturedValueEntry(params) {
4450
4533
  if (!__FULL_VALUE_CAPTURE_ENABLED)
4451
4534
  return undefined;
4535
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
4536
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? (0, live_capture_limits_1.collectCaptureOmissions)(limitedRaw) : [];
4452
4537
  const preferredLimits = {
4453
4538
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
4454
4539
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -4458,7 +4543,7 @@ function createCapturedValueEntry(params) {
4458
4543
  const buildMaskedCandidate = (limits) => {
4459
4544
  const projected = params.target === 'request.headers'
4460
4545
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
4461
- : cloneFullValueCandidate(params.rawValue, params.capture, limits);
4546
+ : cloneFullValueCandidate(limitedRaw, params.capture, limits);
4462
4547
  if (projected === undefined)
4463
4548
  return undefined;
4464
4549
  return applyPrivacyThenMask(params.target, projected, params.capture.runtimeConfig, params.capture.maskReq, params.capture.trace, params.capture.masking, params.capture.privacy, params.capture.db);
@@ -4469,7 +4554,7 @@ function createCapturedValueEntry(params) {
4469
4554
  return { captureRef: { status: 'skipped-non-meaningful' } };
4470
4555
  }
4471
4556
  const candidateSerialized = stableSerializeTraceValue(candidate);
4472
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
4557
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
4473
4558
  return undefined;
4474
4559
  }
4475
4560
  const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
@@ -4489,6 +4574,7 @@ function createCapturedValueEntry(params) {
4489
4574
  status: 'stored',
4490
4575
  valueId,
4491
4576
  projectedKind: entry.projectedKind,
4577
+ ...(omissions.length ? { complete: false, omissions } : {}),
4492
4578
  },
4493
4579
  entry,
4494
4580
  };
@@ -4496,6 +4582,8 @@ function createCapturedValueEntry(params) {
4496
4582
  async function createCapturedValueEntryAsync(params) {
4497
4583
  if (!__FULL_VALUE_CAPTURE_ENABLED)
4498
4584
  return undefined;
4585
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
4586
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? (0, live_capture_limits_1.collectCaptureOmissions)(limitedRaw) : [];
4499
4587
  const preferredLimits = {
4500
4588
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
4501
4589
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -4505,7 +4593,7 @@ async function createCapturedValueEntryAsync(params) {
4505
4593
  const buildMaskedCandidate = async (limits) => {
4506
4594
  const projected = params.target === 'request.headers'
4507
4595
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
4508
- : await cloneFullValueCandidateAsync(params.rawValue, params.capture, limits);
4596
+ : await cloneFullValueCandidateAsync(limitedRaw, params.capture, limits);
4509
4597
  if (projected === undefined)
4510
4598
  return undefined;
4511
4599
  return applyPrivacyThenMaskAsync(params.target, projected, params.capture.runtimeConfig, params.capture.maskReq, params.capture.trace, params.capture.masking, params.capture.privacy, params.capture.db);
@@ -4516,7 +4604,7 @@ async function createCapturedValueEntryAsync(params) {
4516
4604
  return { captureRef: { status: 'skipped-non-meaningful' } };
4517
4605
  }
4518
4606
  const candidateSerialized = stableSerializeTraceValue(candidate);
4519
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
4607
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
4520
4608
  return undefined;
4521
4609
  }
4522
4610
  const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
@@ -4536,6 +4624,7 @@ async function createCapturedValueEntryAsync(params) {
4536
4624
  status: 'stored',
4537
4625
  valueId,
4538
4626
  projectedKind: entry.projectedKind,
4627
+ ...(omissions.length ? { complete: false, omissions } : {}),
4539
4628
  },
4540
4629
  entry,
4541
4630
  };
@@ -4738,6 +4827,7 @@ function reproMiddleware(cfg) {
4738
4827
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4739
4828
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
4740
4829
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
4830
+ const queryCaptureMode = readRequestQueryParam(req, '__repro_mode');
4741
4831
  if (querySid || queryAid || queryTs !== null) {
4742
4832
  stripReproQueryParams(req);
4743
4833
  }
@@ -4774,21 +4864,25 @@ function reproMiddleware(cfg) {
4774
4864
  res.on('close', endTrackedClientRequest);
4775
4865
  return next(); // only capture tagged requests
4776
4866
  }
4777
- const captureCfg = backendLiveChild
4778
- ? {
4867
+ let captureCfg = cfg;
4868
+ if (backendLiveChild) {
4869
+ captureCfg = {
4779
4870
  ...cfg,
4780
4871
  actorId: backendLiveChild.actorId,
4781
4872
  actorType: backendLiveChild.actorType,
4782
4873
  actorLabels: backendLiveChild.actorLabels,
4783
- }
4784
- : taggedActorIdentity
4785
- ? {
4786
- ...cfg,
4787
- actorId: taggedActorIdentity.actorId,
4788
- actorType: taggedActorIdentity.actorType,
4789
- actorLabels: taggedActorIdentity.actorLabels,
4790
- }
4791
- : cfg;
4874
+ };
4875
+ shareRuntimePrivacyState(cfg, captureCfg);
4876
+ }
4877
+ else if (taggedActorIdentity) {
4878
+ captureCfg = {
4879
+ ...cfg,
4880
+ actorId: taggedActorIdentity.actorId,
4881
+ actorType: taggedActorIdentity.actorType,
4882
+ actorLabels: taggedActorIdentity.actorLabels,
4883
+ };
4884
+ shareRuntimePrivacyState(cfg, captureCfg);
4885
+ }
4792
4886
  if (taggedActorIdentity) {
4793
4887
  post(captureCfg, sid, {
4794
4888
  entries: [{
@@ -4803,6 +4897,11 @@ function reproMiddleware(cfg) {
4803
4897
  }],
4804
4898
  }, { priority: 'high' });
4805
4899
  }
4900
+ const captureMode = backendLiveContext || req.headers['x-repro-capture-mode'] === 'live' || queryCaptureMode === 'live'
4901
+ ? 'live' : 'staging';
4902
+ const modeConfig = { ...captureCfg, captureMode };
4903
+ shareRuntimePrivacyState(captureCfg, modeConfig);
4904
+ captureCfg = modeConfig;
4806
4905
  const resolvePrivacy = () => getRuntimePrivacyState(captureCfg).policy ?? null;
4807
4906
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
4808
4907
  const rid = nextRequestId(requestEpochMs);
@@ -5262,17 +5361,17 @@ function reproMiddleware(cfg) {
5262
5361
  evt.__reproPending = {
5263
5362
  ...(ev.args !== undefined
5264
5363
  ? {
5265
- argsRaw: normalizeRawTraceArgs(ev.args),
5364
+ argsRaw: snapshotTraceArgsAtObservation(ev.args, captureScope),
5266
5365
  }
5267
5366
  : {}),
5268
5367
  ...(ev.returnValue !== undefined
5269
5368
  ? {
5270
- returnValueRaw: ev.returnValue,
5369
+ returnValueRaw: snapshotTraceValueAtObservation(ev.returnValue, captureScope, '$', 'trace.returnValue'),
5271
5370
  }
5272
5371
  : {}),
5273
5372
  ...(hasErrorPayload
5274
5373
  ? {
5275
- errorRaw: ev.error,
5374
+ errorRaw: snapshotTraceValueAtObservation(ev.error, captureScope, '$', 'trace.error'),
5276
5375
  }
5277
5376
  : {}),
5278
5377
  };
@@ -6227,6 +6326,8 @@ function patchNodeFetchForPropagation() {
6227
6326
  if (runtime.aid && !hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID)) {
6228
6327
  setHttpHeader(headers, KAFKA_HEADER_ACTION_ID, runtime.aid);
6229
6328
  }
6329
+ if (runtime.sid)
6330
+ setHttpHeader(headers, 'x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
6230
6331
  if (runtime.traceId && !hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID)) {
6231
6332
  setHttpHeader(headers, KAFKA_HEADER_TRACE_ID, runtime.traceId);
6232
6333
  }
@@ -6277,6 +6378,8 @@ function patchNodeHttpModuleRequest(mod) {
6277
6378
  maybeSet(REQUEST_START_HEADER, String(alignedNow()));
6278
6379
  maybeSet(KAFKA_HEADER_SESSION_ID, runtime.sid);
6279
6380
  maybeSet(KAFKA_HEADER_ACTION_ID, runtime.aid);
6381
+ if (runtime.sid)
6382
+ maybeSet('x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
6280
6383
  maybeSet(KAFKA_HEADER_TRACE_ID, runtime.traceId);
6281
6384
  maybeSet(KAFKA_HEADER_PARENT_SPAN_ID, runtime.parentSpanId);
6282
6385
  maybeSet(KAFKA_HEADER_REQUEST_ID, runtime.requestRid);
@@ -6453,6 +6556,11 @@ function cloneKafkaTelemetryValue(value) {
6453
6556
  }
6454
6557
  }
6455
6558
  async function materializeKafkaRequestBody(cfg, maskReq, body, captureScope) {
6559
+ // Broker bodies have no separate full-value reference. Returning the trace
6560
+ // inline preview here would permanently replace message arrays with 20 items.
6561
+ if (shouldCapturePrivacyRaw(cfg)) {
6562
+ return { value: constrainLiveCapture(sanitizeInlinePrivacyValue(body), cfg, 'request.body', captureScope) };
6563
+ }
6456
6564
  return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null, undefined, captureScope);
6457
6565
  }
6458
6566
  function summarizeKafkaError(error) {
@@ -6497,7 +6605,8 @@ function resolveKafkaContext(cfg, headers) {
6497
6605
  readKafkaHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
6498
6606
  null;
6499
6607
  const requestRid = readKafkaHeader(headers, KAFKA_HEADER_REQUEST_ID);
6500
- return { sid, aid, traceId, parentSpanId, requestRid };
6608
+ return { sid, aid, traceId, parentSpanId, requestRid,
6609
+ captureMode: readKafkaHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
6501
6610
  }
6502
6611
  function withOptionalTraceContext(traceId, fn) {
6503
6612
  const tracerWithTrace = __TRACER__?.withTrace;
@@ -6621,6 +6730,7 @@ function recordKafkaTraceEvent(raw, sink, cfg, maskReq) {
6621
6730
  function buildPendingKafkaTraceEvent(raw) {
6622
6731
  if (!raw || (raw.type !== 'enter' && raw.type !== 'exit'))
6623
6732
  return null;
6733
+ raw = snapshotRawTraceEventAtObservation(raw, getCtx().captureScope);
6624
6734
  const sourceFileForLibrary = typeof raw.sourceFile === 'string' && raw.sourceFile
6625
6735
  ? String(raw.sourceFile)
6626
6736
  : null;
@@ -6796,6 +6906,7 @@ function patchKafkaProducerInstance(producer, cfg) {
6796
6906
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
6797
6907
  [KAFKA_HEADER_SESSION_ID]: sid,
6798
6908
  [KAFKA_HEADER_ACTION_ID]: aid,
6909
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
6799
6910
  }),
6800
6911
  };
6801
6912
  });
@@ -6918,6 +7029,7 @@ function patchKafkaProducerInstance(producer, cfg) {
6918
7029
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
6919
7030
  [KAFKA_HEADER_SESSION_ID]: sid,
6920
7031
  [KAFKA_HEADER_ACTION_ID]: aid,
7032
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
6921
7033
  }),
6922
7034
  };
6923
7035
  }),
@@ -7030,6 +7142,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
7030
7142
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
7031
7143
  const traceId = consumeRid;
7032
7144
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
7145
+ captureScope.liveCapture = runtime.captureMode === 'live';
7033
7146
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7034
7147
  const rawTraceEvents = [];
7035
7148
  const maskReq = {
@@ -7054,7 +7167,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
7054
7167
  unsubscribe = __TRACER__.tracer.on((event) => {
7055
7168
  if (!event || event.traceId !== traceId)
7056
7169
  return;
7057
- rawTraceEvents.push(event);
7170
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7058
7171
  });
7059
7172
  }
7060
7173
  catch { }
@@ -7144,6 +7257,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
7144
7257
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
7145
7258
  const traceId = consumeRid;
7146
7259
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
7260
+ captureScope.liveCapture = runtime.captureMode === 'live';
7147
7261
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7148
7262
  const rawTraceEvents = [];
7149
7263
  const maskReq = {
@@ -7168,7 +7282,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
7168
7282
  unsubscribe = __TRACER__.tracer.on((event) => {
7169
7283
  if (!event || event.traceId !== traceId)
7170
7284
  return;
7171
- rawTraceEvents.push(event);
7285
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7172
7286
  });
7173
7287
  }
7174
7288
  catch { }
@@ -7581,7 +7695,8 @@ function resolveAmqpContext(cfg, headers) {
7581
7695
  readAmqpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
7582
7696
  null;
7583
7697
  const requestRid = readAmqpHeader(headers, KAFKA_HEADER_REQUEST_ID);
7584
- return { sid, aid, traceId, parentSpanId, requestRid };
7698
+ return { sid, aid, traceId, parentSpanId, requestRid,
7699
+ captureMode: readAmqpHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
7585
7700
  }
7586
7701
  function withOptionalTraceContextSync(traceId, fn) {
7587
7702
  const tracerWithTrace = __TRACER__?.withTrace;
@@ -7756,6 +7871,7 @@ function patchAmqplibPublishMethod(channel, cfg, methodName) {
7756
7871
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
7757
7872
  [KAFKA_HEADER_SESSION_ID]: sid,
7758
7873
  [KAFKA_HEADER_ACTION_ID]: aid,
7874
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
7759
7875
  };
7760
7876
  const shouldPropagate = hasAmqpPropagationValues(propagationValues);
7761
7877
  const callbackIndex = args.length > 0 && typeof args[args.length - 1] === 'function'
@@ -7861,6 +7977,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
7861
7977
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
7862
7978
  const traceId = consumeRid;
7863
7979
  const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
7980
+ captureScope.liveCapture = runtime.captureMode === 'live';
7864
7981
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7865
7982
  const rawTraceEvents = [];
7866
7983
  const maskReq = {
@@ -7886,7 +8003,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
7886
8003
  unsubscribe = __TRACER__.tracer.on((event) => {
7887
8004
  if (!event || event.traceId !== traceId)
7888
8005
  return;
7889
- rawTraceEvents.push(event);
8006
+ rawTraceEvents.push(snapshotRawTraceEventAtObservation(event, captureScope));
7890
8007
  });
7891
8008
  }
7892
8009
  catch { }
@@ -47,6 +47,8 @@ let backoffMs = 0;
47
47
  let criticalBackoffMs = 0;
48
48
  let shouldDeferDrain = null;
49
49
  let deferDrainMs = DEFAULT_DEFER_DRAIN_MS;
50
+ const throttledUntil = new Map();
51
+ const throttleDelay = (config) => Math.max(0, (throttledUntil.get(resolveIngestUrl(config)) ?? 0) - Date.now());
50
52
  const configureIngestQueueDrain = (options) => {
51
53
  shouldDeferDrain = options.shouldDefer ?? null;
52
54
  if (Number.isFinite(options.deferMs) && Number(options.deferMs) >= 0) {
@@ -166,6 +168,9 @@ const postIngestEntries = async (config, sessionId, entries) => {
166
168
  };
167
169
  exports.postIngestEntries = postIngestEntries;
168
170
  const sendIngestEntries = async (config, sessionId, entries) => {
171
+ if (throttleDelay(config) > 0)
172
+ return 'throttled';
173
+ throttledUntil.delete(resolveIngestUrl(config));
169
174
  if (!sessionId || !Array.isArray(entries) || entries.length === 0) {
170
175
  return true;
171
176
  }
@@ -197,6 +202,18 @@ const sendIngestEntries = async (config, sessionId, entries) => {
197
202
  body: JSON.stringify({ events }),
198
203
  ...(controller ? { signal: controller.signal } : {}),
199
204
  });
205
+ if (response.status === 429) {
206
+ const retryAfter = response.headers?.get?.('retry-after');
207
+ const seconds = retryAfter ? Number(retryAfter) : NaN;
208
+ const date = retryAfter ? Date.parse(retryAfter) : NaN;
209
+ const delay = Number.isFinite(seconds) ? seconds * 1000 : Number.isFinite(date) ? date - Date.now() : 1000;
210
+ throttledUntil.set(resolveIngestUrl(config), Date.now() + Math.max(1000, Math.min(86400000, delay)));
211
+ return 'throttled';
212
+ }
213
+ if (response.status === 402 || response.status === 410) {
214
+ console.warn(`[repro] Upload refused for ${sessionId}: ${response.status === 402 ? 'capture quota exhausted' : 'capture deleted, expired or failed'}. This batch will not be retried.`);
215
+ return 'denied';
216
+ }
200
217
  return response.ok;
201
218
  }
202
219
  catch {
@@ -257,7 +274,11 @@ const drainCriticalIngestQueue = async () => {
257
274
  return;
258
275
  }
259
276
  const ok = await sendIngestEntries(selected.batch.config, selected.batch.sessionId, selected.batch.entries);
260
- if (ok) {
277
+ if (ok === 'throttled') {
278
+ scheduleCriticalDrain(throttleDelay(selected.batch.config));
279
+ return;
280
+ }
281
+ if (ok === true || ok === 'denied') {
261
282
  criticalIngestQueue.splice(0, selected.queueItems);
262
283
  criticalBackoffMs = 0;
263
284
  continue;
@@ -298,7 +319,11 @@ const drainIngestQueue = async () => {
298
319
  }
299
320
  const batch = selected.batch;
300
321
  const ok = await sendIngestEntries(batch.config, batch.sessionId, batch.entries);
301
- if (ok) {
322
+ if (ok === 'throttled') {
323
+ scheduleDrain(throttleDelay(batch.config));
324
+ return;
325
+ }
326
+ if (ok === true || ok === 'denied') {
302
327
  ingestQueue.splice(0, selected.queueItems);
303
328
  backoffMs = 0;
304
329
  continue;
@@ -0,0 +1,19 @@
1
+ export type CaptureOmission = {
2
+ reason: 'max-depth' | 'max-keys' | 'max-entries' | 'accessor' | 'uninspectable';
3
+ path: string;
4
+ limit?: number;
5
+ originalCount?: number;
6
+ capturedCount: number;
7
+ };
8
+ export declare const LIVE_CAPTURE_LIMITS: Readonly<{
9
+ maxDepth: 10;
10
+ maxKeys: 100;
11
+ maxEntries: 100;
12
+ }>;
13
+ /** Snapshot only data descriptors: capture must never execute application getters. */
14
+ export declare function limitLiveCapture(value: unknown, rootPath?: string): {
15
+ value: any;
16
+ omissions: CaptureOmission[];
17
+ };
18
+ export declare function collectCaptureOmissions(value: any): CaptureOmission[];
19
+ export declare function limitLiveArguments(args: any[]): any[];