@reproapp/node-sdk 0.0.49 → 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;
@@ -342,6 +347,8 @@ declare function getEventTraceValueEntries(event: TraceEventRecord | PendingTrac
342
347
  declare function estimateTraceBatchSerializedBytes(batch: TraceEventRecord[], requestRid: string, actionId: string | null | undefined, batchIndex: number, previouslyEmittedValueIds?: ReadonlySet<string>): number;
343
348
  declare function chunkTraceEventsForTransport(events: TraceEventRecord[], requestRid: string, actionId: string | null | undefined): TraceEventRecord[][];
344
349
  export type ReproMiddlewareConfig = IngestClientConfig & {
350
+ /** Propagated recording mode. A tagged staging request never inherits live limits. */
351
+ captureMode?: 'live' | 'staging';
345
352
  /** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
346
353
  apiBase?: string;
347
354
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
@@ -370,6 +377,15 @@ export type ReproLiveActorDecision = ReproLiveActorArgs & {
370
377
  actorLabels?: Record<string, string>;
371
378
  };
372
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
+ };
373
389
  enabled?: boolean;
374
390
  backendOnly?: boolean;
375
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,
@@ -2054,6 +2056,7 @@ async function resolveBackendLiveContext(cfg, req, requestStart) {
2054
2056
  }
2055
2057
  const { actorId, actorType, actorLabels } = identity;
2056
2058
  const claim = await postLiveApiJson(cfg, liveMode, '/v1/live-sessions/claim', {
2059
+ liveCapture: liveMode.liveCapture,
2057
2060
  clientTime: requestStart,
2058
2061
  captureMode: 'backend-only',
2059
2062
  actorId,
@@ -2156,6 +2159,7 @@ function stripReproQueryFromUrl(rawUrl) {
2156
2159
  parsed.searchParams.delete(REPRO_QUERY_SESSION_PARAM);
2157
2160
  parsed.searchParams.delete(REPRO_QUERY_ACTION_PARAM);
2158
2161
  parsed.searchParams.delete(REPRO_QUERY_START_PARAM);
2162
+ parsed.searchParams.delete('__repro_mode');
2159
2163
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
2160
2164
  }
2161
2165
  catch {
@@ -2169,6 +2173,7 @@ function stripReproQueryParams(req) {
2169
2173
  delete query[REPRO_QUERY_SESSION_PARAM];
2170
2174
  delete query[REPRO_QUERY_ACTION_PARAM];
2171
2175
  delete query[REPRO_QUERY_START_PARAM];
2176
+ delete query.__repro_mode;
2172
2177
  }
2173
2178
  }
2174
2179
  catch { }
@@ -2834,6 +2839,11 @@ function internTraceObservationSnapshot(snapshot, captureScope) {
2834
2839
  }
2835
2840
  function snapshotTraceValueAtObservation(value, captureScope, valuePath = '$', target = 'trace.value') {
2836
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
+ }
2837
2847
  const snapshot = sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true }, valuePath);
2838
2848
  return internTraceObservationSnapshot(snapshot, captureScope);
2839
2849
  }
@@ -3018,6 +3028,7 @@ async function materializePendingTraceEventsAsync(events, params) {
3018
3028
  appSecret: params.cfg.appSecret,
3019
3029
  appName: params.cfg.appName,
3020
3030
  privacy: params.cfg.privacy,
3031
+ captureMode: params.captureScope?.liveCapture ? 'live' : params.cfg.captureMode,
3021
3032
  },
3022
3033
  maskReq: params.maskReq,
3023
3034
  masking: params.masking,
@@ -3439,7 +3450,20 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg) {
3439
3450
  }
3440
3451
  throw new Error(`Runtime privacy policy failed to load during startup from ${apiBase} for env ${state.environment} (${lastFailure}).`);
3441
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
+ }
3442
3465
  function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db) {
3466
+ value = constrainLiveCapture(value, cfg, target);
3443
3467
  if (shouldCapturePrivacyRaw(cfg)) {
3444
3468
  return value;
3445
3469
  }
@@ -3460,6 +3484,7 @@ function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy,
3460
3484
  return applyMasking(target, transformed, req, trace, masking);
3461
3485
  }
3462
3486
  async function applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db) {
3487
+ value = constrainLiveCapture(value, cfg, target);
3463
3488
  if (shouldCapturePrivacyRaw(cfg)) {
3464
3489
  return value;
3465
3490
  }
@@ -3506,6 +3531,7 @@ function limitRawInlinePrivacyValue(target, value) {
3506
3531
  return limited.skipped ? limited : null;
3507
3532
  }
3508
3533
  function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
3534
+ value = constrainLiveCapture(value, cfg, target, captureScope);
3509
3535
  if (value === undefined) {
3510
3536
  return { value };
3511
3537
  }
@@ -3529,6 +3555,7 @@ function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking,
3529
3555
  }
3530
3556
  }
3531
3557
  async function materializeInlinePrivacyValueAsync(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
3558
+ value = constrainLiveCapture(value, cfg, target, captureScope);
3532
3559
  if (value === undefined) {
3533
3560
  return { value };
3534
3561
  }
@@ -4505,6 +4532,8 @@ function boundTraceEventsForTransport(events) {
4505
4532
  function createCapturedValueEntry(params) {
4506
4533
  if (!__FULL_VALUE_CAPTURE_ENABLED)
4507
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) : [];
4508
4537
  const preferredLimits = {
4509
4538
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
4510
4539
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -4514,7 +4543,7 @@ function createCapturedValueEntry(params) {
4514
4543
  const buildMaskedCandidate = (limits) => {
4515
4544
  const projected = params.target === 'request.headers'
4516
4545
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
4517
- : cloneFullValueCandidate(params.rawValue, params.capture, limits);
4546
+ : cloneFullValueCandidate(limitedRaw, params.capture, limits);
4518
4547
  if (projected === undefined)
4519
4548
  return undefined;
4520
4549
  return applyPrivacyThenMask(params.target, projected, params.capture.runtimeConfig, params.capture.maskReq, params.capture.trace, params.capture.masking, params.capture.privacy, params.capture.db);
@@ -4525,7 +4554,7 @@ function createCapturedValueEntry(params) {
4525
4554
  return { captureRef: { status: 'skipped-non-meaningful' } };
4526
4555
  }
4527
4556
  const candidateSerialized = stableSerializeTraceValue(candidate);
4528
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
4557
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
4529
4558
  return undefined;
4530
4559
  }
4531
4560
  const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
@@ -4545,6 +4574,7 @@ function createCapturedValueEntry(params) {
4545
4574
  status: 'stored',
4546
4575
  valueId,
4547
4576
  projectedKind: entry.projectedKind,
4577
+ ...(omissions.length ? { complete: false, omissions } : {}),
4548
4578
  },
4549
4579
  entry,
4550
4580
  };
@@ -4552,6 +4582,8 @@ function createCapturedValueEntry(params) {
4552
4582
  async function createCapturedValueEntryAsync(params) {
4553
4583
  if (!__FULL_VALUE_CAPTURE_ENABLED)
4554
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) : [];
4555
4587
  const preferredLimits = {
4556
4588
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
4557
4589
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -4561,7 +4593,7 @@ async function createCapturedValueEntryAsync(params) {
4561
4593
  const buildMaskedCandidate = async (limits) => {
4562
4594
  const projected = params.target === 'request.headers'
4563
4595
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
4564
- : await cloneFullValueCandidateAsync(params.rawValue, params.capture, limits);
4596
+ : await cloneFullValueCandidateAsync(limitedRaw, params.capture, limits);
4565
4597
  if (projected === undefined)
4566
4598
  return undefined;
4567
4599
  return applyPrivacyThenMaskAsync(params.target, projected, params.capture.runtimeConfig, params.capture.maskReq, params.capture.trace, params.capture.masking, params.capture.privacy, params.capture.db);
@@ -4572,7 +4604,7 @@ async function createCapturedValueEntryAsync(params) {
4572
4604
  return { captureRef: { status: 'skipped-non-meaningful' } };
4573
4605
  }
4574
4606
  const candidateSerialized = stableSerializeTraceValue(candidate);
4575
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
4607
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
4576
4608
  return undefined;
4577
4609
  }
4578
4610
  const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
@@ -4592,6 +4624,7 @@ async function createCapturedValueEntryAsync(params) {
4592
4624
  status: 'stored',
4593
4625
  valueId,
4594
4626
  projectedKind: entry.projectedKind,
4627
+ ...(omissions.length ? { complete: false, omissions } : {}),
4595
4628
  },
4596
4629
  entry,
4597
4630
  };
@@ -4794,6 +4827,7 @@ function reproMiddleware(cfg) {
4794
4827
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4795
4828
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
4796
4829
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
4830
+ const queryCaptureMode = readRequestQueryParam(req, '__repro_mode');
4797
4831
  if (querySid || queryAid || queryTs !== null) {
4798
4832
  stripReproQueryParams(req);
4799
4833
  }
@@ -4863,6 +4897,11 @@ function reproMiddleware(cfg) {
4863
4897
  }],
4864
4898
  }, { priority: 'high' });
4865
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;
4866
4905
  const resolvePrivacy = () => getRuntimePrivacyState(captureCfg).policy ?? null;
4867
4906
  const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
4868
4907
  const rid = nextRequestId(requestEpochMs);
@@ -6287,6 +6326,8 @@ function patchNodeFetchForPropagation() {
6287
6326
  if (runtime.aid && !hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID)) {
6288
6327
  setHttpHeader(headers, KAFKA_HEADER_ACTION_ID, runtime.aid);
6289
6328
  }
6329
+ if (runtime.sid)
6330
+ setHttpHeader(headers, 'x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
6290
6331
  if (runtime.traceId && !hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID)) {
6291
6332
  setHttpHeader(headers, KAFKA_HEADER_TRACE_ID, runtime.traceId);
6292
6333
  }
@@ -6337,6 +6378,8 @@ function patchNodeHttpModuleRequest(mod) {
6337
6378
  maybeSet(REQUEST_START_HEADER, String(alignedNow()));
6338
6379
  maybeSet(KAFKA_HEADER_SESSION_ID, runtime.sid);
6339
6380
  maybeSet(KAFKA_HEADER_ACTION_ID, runtime.aid);
6381
+ if (runtime.sid)
6382
+ maybeSet('x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
6340
6383
  maybeSet(KAFKA_HEADER_TRACE_ID, runtime.traceId);
6341
6384
  maybeSet(KAFKA_HEADER_PARENT_SPAN_ID, runtime.parentSpanId);
6342
6385
  maybeSet(KAFKA_HEADER_REQUEST_ID, runtime.requestRid);
@@ -6513,6 +6556,11 @@ function cloneKafkaTelemetryValue(value) {
6513
6556
  }
6514
6557
  }
6515
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
+ }
6516
6564
  return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null, undefined, captureScope);
6517
6565
  }
6518
6566
  function summarizeKafkaError(error) {
@@ -6557,7 +6605,8 @@ function resolveKafkaContext(cfg, headers) {
6557
6605
  readKafkaHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
6558
6606
  null;
6559
6607
  const requestRid = readKafkaHeader(headers, KAFKA_HEADER_REQUEST_ID);
6560
- 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' };
6561
6610
  }
6562
6611
  function withOptionalTraceContext(traceId, fn) {
6563
6612
  const tracerWithTrace = __TRACER__?.withTrace;
@@ -6857,6 +6906,7 @@ function patchKafkaProducerInstance(producer, cfg) {
6857
6906
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
6858
6907
  [KAFKA_HEADER_SESSION_ID]: sid,
6859
6908
  [KAFKA_HEADER_ACTION_ID]: aid,
6909
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
6860
6910
  }),
6861
6911
  };
6862
6912
  });
@@ -6979,6 +7029,7 @@ function patchKafkaProducerInstance(producer, cfg) {
6979
7029
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
6980
7030
  [KAFKA_HEADER_SESSION_ID]: sid,
6981
7031
  [KAFKA_HEADER_ACTION_ID]: aid,
7032
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
6982
7033
  }),
6983
7034
  };
6984
7035
  }),
@@ -7091,6 +7142,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
7091
7142
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
7092
7143
  const traceId = consumeRid;
7093
7144
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
7145
+ captureScope.liveCapture = runtime.captureMode === 'live';
7094
7146
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7095
7147
  const rawTraceEvents = [];
7096
7148
  const maskReq = {
@@ -7205,6 +7257,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
7205
7257
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
7206
7258
  const traceId = consumeRid;
7207
7259
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
7260
+ captureScope.liveCapture = runtime.captureMode === 'live';
7208
7261
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7209
7262
  const rawTraceEvents = [];
7210
7263
  const maskReq = {
@@ -7642,7 +7695,8 @@ function resolveAmqpContext(cfg, headers) {
7642
7695
  readAmqpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
7643
7696
  null;
7644
7697
  const requestRid = readAmqpHeader(headers, KAFKA_HEADER_REQUEST_ID);
7645
- 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' };
7646
7700
  }
7647
7701
  function withOptionalTraceContextSync(traceId, fn) {
7648
7702
  const tracerWithTrace = __TRACER__?.withTrace;
@@ -7817,6 +7871,7 @@ function patchAmqplibPublishMethod(channel, cfg, methodName) {
7817
7871
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
7818
7872
  [KAFKA_HEADER_SESSION_ID]: sid,
7819
7873
  [KAFKA_HEADER_ACTION_ID]: aid,
7874
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
7820
7875
  };
7821
7876
  const shouldPropagate = hasAmqpPropagationValues(propagationValues);
7822
7877
  const callbackIndex = args.length > 0 && typeof args[args.length - 1] === 'function'
@@ -7922,6 +7977,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
7922
7977
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
7923
7978
  const traceId = consumeRid;
7924
7979
  const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
7980
+ captureScope.liveCapture = runtime.captureMode === 'live';
7925
7981
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
7926
7982
  const rawTraceEvents = [];
7927
7983
  const maskReq = {
@@ -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[];