@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.
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.limitLiveArguments = exports.collectCaptureOmissions = exports.limitLiveCapture = exports.LIVE_CAPTURE_LIMITS = void 0;
4
+ const http_1 = require("http");
5
+ const semantic_serialization_1 = require("./semantic-serialization");
6
+ exports.LIVE_CAPTURE_LIMITS = Object.freeze({ maxDepth: 10, maxKeys: 100, maxEntries: 100 });
7
+ const marker = (omission) => ({ __kind: 'repro-omitted', complete: false, ...omission });
8
+ const childPath = (path, key) => `${path}[${JSON.stringify(key)}]`;
9
+ function parseStructuredString(value) {
10
+ if (typeof value !== 'string' || !/^[\s]*[\[{]/.test(value))
11
+ return undefined;
12
+ try {
13
+ const parsed = JSON.parse(value);
14
+ return parsed && typeof parsed === 'object' ? parsed : undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /** Snapshot only data descriptors: capture must never execute application getters. */
21
+ function limitLiveCapture(value, rootPath = '$') {
22
+ const omissions = [];
23
+ const seen = new WeakMap();
24
+ const omit = (detail) => { omissions.push(detail); return marker(detail); };
25
+ const walk = (input, depth, path) => {
26
+ if (input === null || input === undefined)
27
+ return input;
28
+ if (typeof input === 'string') {
29
+ const parsed = parseStructuredString(input);
30
+ if (parsed === undefined)
31
+ return input;
32
+ const before = omissions.length;
33
+ const bounded = walk(parsed, depth, path);
34
+ // Preserve the original string exactly unless its structured data exceeded a limit.
35
+ return omissions.length === before ? input : JSON.stringify(bounded);
36
+ }
37
+ if (typeof input === 'bigint' || typeof input === 'symbol')
38
+ return String(input);
39
+ if (typeof input === 'function')
40
+ return '[Function]';
41
+ if (typeof input !== 'object')
42
+ return input;
43
+ const previous = seen.get(input);
44
+ if (previous && previous.depth <= depth)
45
+ return { __reproCircularRef: previous.path };
46
+ seen.set(input, { path, depth });
47
+ const descriptor = (key) => Object.getOwnPropertyDescriptor(input, key);
48
+ const data = (key) => descriptor(key)?.value;
49
+ if (data('__kind') === 'repro-omitted') {
50
+ const detail = { reason: data('reason'), path: data('path'), limit: data('limit'), originalCount: data('originalCount'), capturedCount: data('capturedCount') };
51
+ omissions.push(detail);
52
+ return marker(detail);
53
+ }
54
+ if (input instanceof Date)
55
+ return Number.isNaN(Date.prototype.getTime.call(input)) ? 'Invalid Date' : Date.prototype.toISOString.call(input);
56
+ if (input instanceof RegExp)
57
+ return RegExp.prototype.toString.call(input);
58
+ if (input instanceof http_1.IncomingMessage) {
59
+ // The business body is its own root, not depth one in a socket graph.
60
+ return { __kind: 'http-request', method: data('method'), url: data('originalUrl') ?? data('url'),
61
+ headers: walk(data('headers') ?? Object.getOwnPropertyDescriptor(http_1.IncomingMessage.prototype, 'headers')?.get?.call(input), 0, childPath(path, 'headers')),
62
+ query: walk(data('query'), 0, childPath(path, 'query')),
63
+ body: walk(data('body'), 0, childPath(path, 'body')),
64
+ params: walk(data('params'), 0, childPath(path, 'params')) };
65
+ }
66
+ if (input instanceof http_1.ServerResponse)
67
+ return { __kind: 'http-response', statusCode: data('statusCode') };
68
+ const projected = (0, semantic_serialization_1.projectSemanticInfrastructureValue)(input);
69
+ if (projected)
70
+ return projected.value;
71
+ // Mongoose stores business fields in _doc; never recurse through $__ state.
72
+ const documentData = (0, semantic_serialization_1.mongooseDocumentData)(input);
73
+ if (documentData)
74
+ return walk(documentData, depth, path);
75
+ if (depth > exports.LIVE_CAPTURE_LIMITS.maxDepth)
76
+ return omit({ reason: 'max-depth', path, limit: exports.LIVE_CAPTURE_LIMITS.maxDepth, capturedCount: 0 });
77
+ if (input instanceof Error) {
78
+ const details = Object.create(null);
79
+ for (const key of ['message', 'stack', 'cause', 'errors', ...Object.keys(input)]) {
80
+ const item = descriptor(key);
81
+ if (item)
82
+ Object.defineProperty(details, key, { ...item, enumerable: true, configurable: true });
83
+ }
84
+ return walk(details, depth, path);
85
+ }
86
+ if (input instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && input instanceof SharedArrayBuffer) || input instanceof DataView) {
87
+ let bytes;
88
+ if (input instanceof DataView) {
89
+ const read = (key) => Object.getOwnPropertyDescriptor(DataView.prototype, key).get.call(input);
90
+ bytes = new Uint8Array(read('buffer'), read('byteOffset'), read('byteLength'));
91
+ }
92
+ else
93
+ bytes = new Uint8Array(input);
94
+ return { __type: input instanceof DataView ? 'DataView' : 'ArrayBuffer', values: walk(bytes, depth, path).values };
95
+ }
96
+ if (input instanceof Map) {
97
+ const entries = [];
98
+ const size = Object.getOwnPropertyDescriptor(Map.prototype, 'size').get.call(input);
99
+ for (const [key, item] of Map.prototype.entries.call(input)) {
100
+ if (entries.length >= exports.LIVE_CAPTURE_LIMITS.maxEntries)
101
+ break;
102
+ const index = entries.length;
103
+ entries.push([walk(key, depth + 1, childPath(path, `key:${index}`)), walk(item, depth + 1, childPath(path, index))]);
104
+ }
105
+ if (size > entries.length)
106
+ entries.push(omit({ reason: 'max-entries', path, limit: 100, originalCount: size, capturedCount: entries.length }));
107
+ return { __type: 'Map', entries };
108
+ }
109
+ if (input instanceof Set) {
110
+ const values = [];
111
+ const size = Object.getOwnPropertyDescriptor(Set.prototype, 'size').get.call(input);
112
+ for (const item of Set.prototype.values.call(input)) {
113
+ if (values.length >= 100)
114
+ break;
115
+ values.push(walk(item, depth + 1, childPath(path, values.length)));
116
+ }
117
+ if (size > values.length)
118
+ values.push(omit({ reason: 'max-entries', path, limit: 100, originalCount: size, capturedCount: values.length }));
119
+ return { __type: 'Set', values };
120
+ }
121
+ if (Array.isArray(input) || (ArrayBuffer.isView(input) && !(input instanceof DataView))) {
122
+ const length = input.length;
123
+ const previousMarker = Array.isArray(input) && length === 101 ? descriptor('100')?.value : null;
124
+ const prior = previousMarker && Object.getOwnPropertyDescriptors(previousMarker);
125
+ const hasPriorLimit = prior?.__kind?.value === 'repro-omitted' && prior?.reason?.value === 'max-entries';
126
+ const values = [];
127
+ for (let i = 0; i < Math.min(length, 100); i++) {
128
+ const itemDescriptor = descriptor(String(i));
129
+ values.push(itemDescriptor && !('value' in itemDescriptor)
130
+ ? omit({ reason: 'accessor', path: childPath(path, i), capturedCount: 0 })
131
+ : walk(itemDescriptor?.value, depth + 1, childPath(path, i)));
132
+ }
133
+ if (length > 100)
134
+ values.push(hasPriorLimit
135
+ ? walk(previousMarker, depth + 1, path)
136
+ : omit({ reason: 'max-entries', path, limit: 100, originalCount: length, capturedCount: 100 }));
137
+ return Array.isArray(input) ? values : { __type: 'TypedArray', values };
138
+ }
139
+ const keys = Object.keys(input);
140
+ const previousMetadataKey = keys.find(key => {
141
+ if (!/^__reproCapture\$*$/.test(key))
142
+ return false;
143
+ const candidate = descriptor(key)?.value;
144
+ return candidate && Object.getOwnPropertyDescriptor(candidate, '__kind')?.value === 'repro-omitted'
145
+ && Object.getOwnPropertyDescriptor(candidate, 'reason')?.value === 'max-keys';
146
+ });
147
+ const businessKeys = keys.filter(key => key !== previousMetadataKey);
148
+ const out = Object.create(null);
149
+ for (const key of businessKeys.slice(0, 100)) {
150
+ const itemDescriptor = descriptor(key);
151
+ out[key] = 'value' in itemDescriptor
152
+ ? walk(itemDescriptor.value, depth + 1, childPath(path, key))
153
+ : omit({ reason: 'accessor', path: childPath(path, key), capturedCount: 0 });
154
+ }
155
+ if (businessKeys.length > 100 || previousMetadataKey) {
156
+ const detail = previousMetadataKey
157
+ ? walk(descriptor(previousMetadataKey)?.value, depth + 1, path)
158
+ : omit({ reason: 'max-keys', path, limit: 100, originalCount: businessKeys.length, capturedCount: 100 });
159
+ let metadataKey = '__reproCapture';
160
+ while (Object.prototype.hasOwnProperty.call(out, metadataKey))
161
+ metadataKey += '$';
162
+ out[metadataKey] = detail;
163
+ }
164
+ return out;
165
+ };
166
+ try {
167
+ return { value: walk(value, 0, rootPath), omissions };
168
+ }
169
+ catch {
170
+ return { value: omit({ reason: 'uninspectable', path: rootPath, capturedCount: 0 }), omissions };
171
+ }
172
+ }
173
+ exports.limitLiveCapture = limitLiveCapture;
174
+ function collectCaptureOmissions(value) {
175
+ const result = [];
176
+ const seen = new WeakSet();
177
+ const pending = [value];
178
+ while (pending.length) {
179
+ const item = pending.pop();
180
+ const node = typeof item === 'string' ? parseStructuredString(item) : item;
181
+ if (!node || typeof node !== 'object' || seen.has(node))
182
+ continue;
183
+ seen.add(node);
184
+ const descriptors = Object.getOwnPropertyDescriptors(node);
185
+ if (descriptors.__kind?.value === 'repro-omitted') {
186
+ result.push({ reason: descriptors.reason?.value, path: descriptors.path?.value, limit: descriptors.limit?.value,
187
+ originalCount: descriptors.originalCount?.value, capturedCount: descriptors.capturedCount?.value });
188
+ continue;
189
+ }
190
+ for (const item of Object.values(descriptors))
191
+ if ('value' in item)
192
+ pending.push(item.value);
193
+ }
194
+ return result;
195
+ }
196
+ exports.collectCaptureOmissions = collectCaptureOmissions;
197
+ function limitLiveArguments(args) {
198
+ const values = args.slice(0, 100).map((arg, index) => limitLiveCapture(arg, `$[${index}]`).value);
199
+ if (args.length > 100) {
200
+ const prior = Object.getOwnPropertyDescriptor(args, '100')?.value;
201
+ values.push(args.length === 101 && prior && Object.getOwnPropertyDescriptor(prior, '__kind')?.value === 'repro-omitted'
202
+ ? limitLiveCapture(prior).value
203
+ : marker({ reason: 'max-entries', path: '$', limit: 100, originalCount: args.length, capturedCount: 100 }));
204
+ }
205
+ return values;
206
+ }
207
+ exports.limitLiveArguments = limitLiveArguments;
@@ -2,6 +2,7 @@ export type SemanticProjection = {
2
2
  matched: true;
3
3
  value: Record<string, unknown>;
4
4
  };
5
+ export declare function mongooseDocumentData(value: object): object | null;
5
6
  /**
6
7
  * Projects known runtime machinery into stable, non-recursive descriptions.
7
8
  * Unknown application values deliberately return null and remain fully captured.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.projectSemanticInfrastructureValue = void 0;
3
+ exports.projectSemanticInfrastructureValue = exports.mongooseDocumentData = void 0;
4
4
  function constructorName(value) {
5
5
  if (!value || typeof value !== 'object')
6
6
  return '';
@@ -49,6 +49,13 @@ function method(value, key) {
49
49
  return false;
50
50
  }
51
51
  }
52
+ function mongooseDocumentData(value) {
53
+ const doc = Object.getOwnPropertyDescriptor(value, '_doc')?.value;
54
+ if (!doc || typeof doc !== 'object' || !Object.getOwnPropertyDescriptor(value, '$__')?.value)
55
+ return null;
56
+ return method(value, '$__init') && method(value, '$toObject') ? doc : null;
57
+ }
58
+ exports.mongooseDocumentData = mongooseDocumentData;
52
59
  function primitiveStateValue(value) {
53
60
  if (value === null)
54
61
  return null;
@@ -82,6 +89,8 @@ function projectConfigurationService(value, className) {
82
89
  function projectLogger(value, className) {
83
90
  if (!/(Logger|LoggerService)$/i.test(className))
84
91
  return null;
92
+ if (!method(value, 'log') && !method(value, 'info'))
93
+ return null;
85
94
  const descriptors = ownDataDescriptors(value);
86
95
  const context = primitiveStateValue(descriptors.context?.value);
87
96
  return semanticValue('logger', className, context !== undefined ? { context } : {});
@@ -90,6 +99,15 @@ function projectNestInfrastructure(value, className) {
90
99
  if (!/(ModuleRef|ModulesContainer|InstanceWrapper|NestContainer|Injector|DiscoveryService|ExecutionContextHost|ArgumentsHost|HttpAdapterHost)$/i.test(className)) {
91
100
  return null;
92
101
  }
102
+ const signatures = {
103
+ ModuleRef: ['get', 'resolve'], ModulesContainer: ['get', 'set'],
104
+ InstanceWrapper: ['getInstanceByContextId'], NestContainer: ['getModules'],
105
+ Injector: ['loadInstance'], DiscoveryService: ['getProviders'],
106
+ ExecutionContextHost: ['switchToHttp', 'getArgs'], ArgumentsHost: ['switchToHttp', 'getArgs'],
107
+ HttpAdapterHost: ['listen$'],
108
+ };
109
+ if (!(signatures[className] ?? []).some(key => method(value, key)))
110
+ return null;
93
111
  return semanticValue('nestjs-infrastructure', className);
94
112
  }
95
113
  function projectRedis(value, className) {
@@ -149,6 +167,19 @@ function projectTypeOrm(value, className) {
149
167
  }
150
168
  return null;
151
169
  }
170
+ function projectMongoInfrastructure(value, className) {
171
+ const signatures = {
172
+ MongoClient: ['connect', 'db', 'close'],
173
+ ClientSession: ['startTransaction', 'endSession'],
174
+ ConnectionPool: ['checkOut', 'checkIn'],
175
+ NativeConnection: ['openUri', 'model', 'close'],
176
+ Schema: ['add', 'path', 'plugin'],
177
+ };
178
+ const required = signatures[className];
179
+ return required?.every(key => method(value, key))
180
+ ? semanticValue('mongodb-infrastructure', className)
181
+ : null;
182
+ }
152
183
  /**
153
184
  * Projects known runtime machinery into stable, non-recursive descriptions.
154
185
  * Unknown application values deliberately return null and remain fully captured.
@@ -165,6 +196,7 @@ function projectSemanticInfrastructureValue(value) {
165
196
  projectRedis(value, className) ||
166
197
  projectKafka(value, className) ||
167
198
  projectAmqp(value, className) ||
199
+ projectMongoInfrastructure(value, className) ||
168
200
  projectPostgres(value, className) ||
169
201
  projectTypeOrm(value, className));
170
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,6 +9,8 @@
9
9
  "node": ">=18"
10
10
  },
11
11
  "scripts": {
12
+ "test:live-capture": "npm run build && node test/live-capture-limits.test.js && node test/ingest-throttle-retention.test.js && node test/live-capture-limits.testcontainers.test.js",
13
+ "test:live-brokers": "npm run build && node test/live-broker-limits.testcontainers.test.js",
12
14
  "build": "tsc -p tsconfig.json",
13
15
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
14
16
  "prepublishOnly": "npm run build",
@@ -21,8 +23,11 @@
21
23
  },
22
24
  "devDependencies": {
23
25
  "@nestjs/typeorm": "^11.0.0",
26
+ "@testcontainers/kafka": "^11.8.1",
24
27
  "@types/express": "^5.0.3",
25
28
  "@types/pg": "^8.20.0",
29
+ "amqplib": "^0.10.9",
30
+ "kafkajs": "^2.2.4",
26
31
  "mongodb": "^6.21.0",
27
32
  "pg": "^8.22.0",
28
33
  "postgres": "^3.4.9",
package/src/index.ts CHANGED
@@ -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;
@@ -1246,6 +1251,7 @@ function createRequestCaptureScope(
1246
1251
  return {
1247
1252
  requestRid,
1248
1253
  transport,
1254
+ liveCapture: cfg?.captureMode === 'live' || (!!cfg && cfg.captureMode !== 'staging' && !!normalizeLiveModeConfig(cfg)),
1249
1255
  // Every scope represents one captured transport request. This is an
1250
1256
  // observed expectation, not a count of successful enqueue operations.
1251
1257
  backendRequestCount: 1,
@@ -2599,6 +2605,7 @@ async function resolveBackendLiveContext(
2599
2605
  liveMode,
2600
2606
  '/v1/live-sessions/claim',
2601
2607
  {
2608
+ liveCapture: liveMode.liveCapture,
2602
2609
  clientTime: requestStart,
2603
2610
  captureMode: 'backend-only',
2604
2611
  actorId,
@@ -2712,6 +2719,7 @@ function stripReproQueryFromUrl(rawUrl: string): string {
2712
2719
  parsed.searchParams.delete(REPRO_QUERY_SESSION_PARAM);
2713
2720
  parsed.searchParams.delete(REPRO_QUERY_ACTION_PARAM);
2714
2721
  parsed.searchParams.delete(REPRO_QUERY_START_PARAM);
2722
+ parsed.searchParams.delete('__repro_mode');
2715
2723
  return `${parsed.pathname}${parsed.search}${parsed.hash}`;
2716
2724
  } catch {
2717
2725
  return rawUrl;
@@ -2725,6 +2733,7 @@ function stripReproQueryParams(req: Request): void {
2725
2733
  delete query[REPRO_QUERY_SESSION_PARAM];
2726
2734
  delete query[REPRO_QUERY_ACTION_PARAM];
2727
2735
  delete query[REPRO_QUERY_START_PARAM];
2736
+ delete query.__repro_mode;
2728
2737
  }
2729
2738
  } catch {}
2730
2739
  try {
@@ -3439,6 +3448,11 @@ function snapshotTraceValueAtObservation(
3439
3448
  target = 'trace.value',
3440
3449
  ): any {
3441
3450
  try {
3451
+ if (captureScope?.liveCapture) {
3452
+ value = target === 'trace.args' && Array.isArray(value)
3453
+ ? limitLiveArguments(value)
3454
+ : limitLiveCapture(value, valuePath).value;
3455
+ }
3442
3456
  const snapshot = sanitizeTraceValue(
3443
3457
  value,
3444
3458
  0,
@@ -3714,6 +3728,7 @@ async function materializePendingTraceEventsAsync(
3714
3728
  appSecret: params.cfg.appSecret,
3715
3729
  appName: params.cfg.appName,
3716
3730
  privacy: params.cfg.privacy,
3731
+ captureMode: params.captureScope?.liveCapture ? 'live' : params.cfg.captureMode,
3717
3732
  },
3718
3733
  maskReq: params.maskReq,
3719
3734
  masking: params.masking,
@@ -4223,6 +4238,18 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg: ReproMiddlewareConfi
4223
4238
  );
4224
4239
  }
4225
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
+
4226
4253
  function applyPrivacyThenMask(
4227
4254
  target: RuntimePrivacySurface,
4228
4255
  value: any,
@@ -4233,6 +4260,7 @@ function applyPrivacyThenMask(
4233
4260
  privacy: NormalizedRuntimePrivacyPolicy | null,
4234
4261
  db?: RuntimePrivacyDbContext | null,
4235
4262
  ) {
4263
+ value = constrainLiveCapture(value, cfg, target);
4236
4264
  if (shouldCapturePrivacyRaw(cfg)) {
4237
4265
  return value;
4238
4266
  }
@@ -4263,6 +4291,7 @@ async function applyPrivacyThenMaskAsync(
4263
4291
  privacy: NormalizedRuntimePrivacyPolicy | null,
4264
4292
  db?: RuntimePrivacyDbContext | null,
4265
4293
  ) {
4294
+ value = constrainLiveCapture(value, cfg, target);
4266
4295
  if (shouldCapturePrivacyRaw(cfg)) {
4267
4296
  return value;
4268
4297
  }
@@ -4342,6 +4371,7 @@ function materializeInlinePrivacyValue(
4342
4371
  db?: RuntimePrivacyDbContext | null,
4343
4372
  captureScope?: RequestCaptureScopeState | null,
4344
4373
  ): InlinePrivacyMaterializationResult {
4374
+ value = constrainLiveCapture(value, cfg, target, captureScope);
4345
4375
  if (value === undefined) {
4346
4376
  return { value };
4347
4377
  }
@@ -4381,6 +4411,7 @@ async function materializeInlinePrivacyValueAsync(
4381
4411
  db?: RuntimePrivacyDbContext | null,
4382
4412
  captureScope?: RequestCaptureScopeState | null,
4383
4413
  ): Promise<InlinePrivacyMaterializationResult> {
4414
+ value = constrainLiveCapture(value, cfg, target, captureScope);
4384
4415
  if (value === undefined) {
4385
4416
  return { value };
4386
4417
  }
@@ -5549,6 +5580,9 @@ function createCapturedValueEntry(
5549
5580
  ): { captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined {
5550
5581
  if (!__FULL_VALUE_CAPTURE_ENABLED) return undefined;
5551
5582
 
5583
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
5584
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? collectCaptureOmissions(limitedRaw) : [];
5585
+
5552
5586
  const preferredLimits: FullValueCloneLimits = {
5553
5587
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
5554
5588
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -5558,7 +5592,7 @@ function createCapturedValueEntry(
5558
5592
  const buildMaskedCandidate = (limits: FullValueCloneLimits) => {
5559
5593
  const projected = params.target === 'request.headers'
5560
5594
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
5561
- : cloneFullValueCandidate(params.rawValue, params.capture, limits);
5595
+ : cloneFullValueCandidate(limitedRaw, params.capture, limits);
5562
5596
  if (projected === undefined) return undefined;
5563
5597
  return applyPrivacyThenMask(
5564
5598
  params.target,
@@ -5579,7 +5613,7 @@ function createCapturedValueEntry(
5579
5613
  }
5580
5614
 
5581
5615
  const candidateSerialized = stableSerializeTraceValue(candidate);
5582
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
5616
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
5583
5617
  return undefined;
5584
5618
  }
5585
5619
 
@@ -5605,6 +5639,7 @@ function createCapturedValueEntry(
5605
5639
  status: 'stored',
5606
5640
  valueId,
5607
5641
  projectedKind: entry.projectedKind,
5642
+ ...(omissions.length ? { complete: false, omissions } : {}),
5608
5643
  },
5609
5644
  entry,
5610
5645
  };
@@ -5635,6 +5670,9 @@ async function createCapturedValueEntryAsync(
5635
5670
  ): Promise<{ captureRef: TraceValueCaptureRef; entry?: TraceValueBatchEntry } | undefined> {
5636
5671
  if (!__FULL_VALUE_CAPTURE_ENABLED) return undefined;
5637
5672
 
5673
+ const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
5674
+ const omissions = isLiveCapture(params.capture.runtimeConfig) ? collectCaptureOmissions(limitedRaw) : [];
5675
+
5638
5676
  const preferredLimits: FullValueCloneLimits = {
5639
5677
  maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
5640
5678
  maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
@@ -5644,7 +5682,7 @@ async function createCapturedValueEntryAsync(
5644
5682
  const buildMaskedCandidate = async (limits: FullValueCloneLimits) => {
5645
5683
  const projected = params.target === 'request.headers'
5646
5684
  ? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
5647
- : await cloneFullValueCandidateAsync(params.rawValue, params.capture, limits);
5685
+ : await cloneFullValueCandidateAsync(limitedRaw, params.capture, limits);
5648
5686
  if (projected === undefined) return undefined;
5649
5687
  return applyPrivacyThenMaskAsync(
5650
5688
  params.target,
@@ -5665,7 +5703,7 @@ async function createCapturedValueEntryAsync(
5665
5703
  }
5666
5704
 
5667
5705
  const candidateSerialized = stableSerializeTraceValue(candidate);
5668
- if (!candidateSerialized || candidateSerialized === previewSerialized) {
5706
+ if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
5669
5707
  return undefined;
5670
5708
  }
5671
5709
 
@@ -5691,6 +5729,7 @@ async function createCapturedValueEntryAsync(
5691
5729
  status: 'stored',
5692
5730
  valueId,
5693
5731
  projectedKind: entry.projectedKind,
5732
+ ...(omissions.length ? { complete: false, omissions } : {}),
5694
5733
  },
5695
5734
  entry,
5696
5735
  };
@@ -5892,6 +5931,8 @@ async function maybeCaptureDbValueFromCandidatesAsync(
5892
5931
  // reproMiddleware — unchanged behavior + passive per-request trace
5893
5932
  // ===================================================================
5894
5933
  export type ReproMiddlewareConfig = IngestClientConfig & {
5934
+ /** Propagated recording mode. A tagged staging request never inherits live limits. */
5935
+ captureMode?: 'live' | 'staging';
5895
5936
  /** API base used for backend-owned live session documents. Defaults to REPRO_API_BASE_URL/REPRO_API_BASE/privacy.apiBase. */
5896
5937
  apiBase?: string;
5897
5938
  /** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
@@ -5923,6 +5964,11 @@ export type ReproLiveActorDecision = ReproLiveActorArgs & {
5923
5964
  };
5924
5965
 
5925
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
+ };
5926
5972
  enabled?: boolean;
5927
5973
  backendOnly?: boolean;
5928
5974
  /**
@@ -6112,6 +6158,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6112
6158
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
6113
6159
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
6114
6160
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
6161
+ const queryCaptureMode = readRequestQueryParam(req, '__repro_mode');
6115
6162
  if (querySid || queryAid || queryTs !== null) {
6116
6163
  stripReproQueryParams(req);
6117
6164
  }
@@ -6181,6 +6228,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6181
6228
  }],
6182
6229
  }, { priority: 'high' });
6183
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;
6184
6236
  const resolvePrivacy = (): NormalizedRuntimePrivacyPolicy | null =>
6185
6237
  getRuntimePrivacyState(captureCfg).policy ?? null;
6186
6238
 
@@ -7745,6 +7797,7 @@ function patchNodeFetchForPropagation() {
7745
7797
  if (runtime.aid && !hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID)) {
7746
7798
  setHttpHeader(headers, KAFKA_HEADER_ACTION_ID, runtime.aid);
7747
7799
  }
7800
+ if (runtime.sid) setHttpHeader(headers, 'x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
7748
7801
  if (runtime.traceId && !hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID)) {
7749
7802
  setHttpHeader(headers, KAFKA_HEADER_TRACE_ID, runtime.traceId);
7750
7803
  }
@@ -7799,6 +7852,7 @@ function patchNodeHttpModuleRequest(mod: any) {
7799
7852
  maybeSet(REQUEST_START_HEADER, String(alignedNow()));
7800
7853
  maybeSet(KAFKA_HEADER_SESSION_ID, runtime.sid);
7801
7854
  maybeSet(KAFKA_HEADER_ACTION_ID, runtime.aid);
7855
+ if (runtime.sid) maybeSet('x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
7802
7856
  maybeSet(KAFKA_HEADER_TRACE_ID, runtime.traceId);
7803
7857
  maybeSet(KAFKA_HEADER_PARENT_SPAN_ID, runtime.parentSpanId);
7804
7858
  maybeSet(KAFKA_HEADER_REQUEST_ID, runtime.requestRid);
@@ -7826,6 +7880,7 @@ type KafkaHeaderValue = string | Buffer | null | undefined;
7826
7880
  type KafkaHeaderMap = Record<string, KafkaHeaderValue>;
7827
7881
  type KafkaContextResolver = () => { sid?: string; aid?: string } | undefined;
7828
7882
  type KafkaPropagationContext = {
7883
+ captureMode?: 'live' | 'staging';
7829
7884
  sid: string | null;
7830
7885
  aid: string | null;
7831
7886
  traceId: string | null;
@@ -7990,6 +8045,11 @@ async function materializeKafkaRequestBody(
7990
8045
  body: Record<string, any>,
7991
8046
  captureScope?: RequestCaptureScopeState | null,
7992
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
+ }
7993
8053
  return materializeInlinePrivacyValueAsync(
7994
8054
  'request.body',
7995
8055
  sanitizeInlinePrivacyValue(body),
@@ -8053,7 +8113,8 @@ function resolveKafkaContext(cfg: KafkaJsPatchConfig, headers: any): KafkaPropag
8053
8113
  null;
8054
8114
  const requestRid = readKafkaHeader(headers, KAFKA_HEADER_REQUEST_ID);
8055
8115
 
8056
- 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' };
8057
8118
  }
8058
8119
 
8059
8120
  function withOptionalTraceContext<T>(traceId: string | null, fn: () => Promise<T>): Promise<T> {
@@ -8420,6 +8481,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8420
8481
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
8421
8482
  [KAFKA_HEADER_SESSION_ID]: sid,
8422
8483
  [KAFKA_HEADER_ACTION_ID]: aid,
8484
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
8423
8485
  }),
8424
8486
  };
8425
8487
  });
@@ -8552,6 +8614,7 @@ function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
8552
8614
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
8553
8615
  [KAFKA_HEADER_SESSION_ID]: sid,
8554
8616
  [KAFKA_HEADER_ACTION_ID]: aid,
8617
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
8555
8618
  }),
8556
8619
  };
8557
8620
  }),
@@ -8668,6 +8731,7 @@ function wrapKafkaEachMessageHandler(eachMessage: any, cfg: KafkaJsPatchConfig,
8668
8731
  const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
8669
8732
  const traceId = consumeRid;
8670
8733
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8734
+ captureScope.liveCapture = runtime.captureMode === 'live';
8671
8735
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8672
8736
  const rawTraceEvents: any[] = [];
8673
8737
  const maskReq: MaskRequestContext = {
@@ -8778,6 +8842,7 @@ function wrapKafkaEachBatchHandler(eachBatch: any, cfg: KafkaJsPatchConfig, cons
8778
8842
  const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
8779
8843
  const traceId = consumeRid;
8780
8844
  const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
8845
+ captureScope.liveCapture = runtime.captureMode === 'live';
8781
8846
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
8782
8847
  const rawTraceEvents: any[] = [];
8783
8848
  const maskReq: MaskRequestContext = {
@@ -8972,6 +9037,7 @@ export function patchKafkaJs(cfg: KafkaJsPatchConfig) {
8972
9037
  type AmqpHeaderMap = Record<string, any>;
8973
9038
  type AmqpContextResolver = () => { sid?: string; aid?: string } | undefined;
8974
9039
  type AmqpPropagationContext = {
9040
+ captureMode?: 'live' | 'staging';
8975
9041
  sid: string | null;
8976
9042
  aid: string | null;
8977
9043
  traceId: string | null;
@@ -9240,7 +9306,8 @@ function resolveAmqpContext(cfg: AmqplibPatchConfig, headers: any): AmqpPropagat
9240
9306
  null;
9241
9307
  const requestRid = readAmqpHeader(headers, KAFKA_HEADER_REQUEST_ID);
9242
9308
 
9243
- 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' };
9244
9311
  }
9245
9312
 
9246
9313
  function withOptionalTraceContextSync<T>(traceId: string | null, fn: () => T): T {
@@ -9478,6 +9545,7 @@ function patchAmqplibPublishMethod(
9478
9545
  [KAFKA_HEADER_REQUEST_ID]: publishRid,
9479
9546
  [KAFKA_HEADER_SESSION_ID]: sid,
9480
9547
  [KAFKA_HEADER_ACTION_ID]: aid,
9548
+ 'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
9481
9549
  };
9482
9550
  const shouldPropagate = hasAmqpPropagationValues(propagationValues);
9483
9551
  const callbackIndex = args.length > 0 && typeof args[args.length - 1] === 'function'
@@ -9586,6 +9654,7 @@ function wrapAmqplibConsumeHandler(onMessage: any, cfg: AmqplibPatchConfig, queu
9586
9654
  const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
9587
9655
  const traceId = consumeRid;
9588
9656
  const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
9657
+ captureScope.liveCapture = runtime.captureMode === 'live';
9589
9658
  emitRequestCaptureStart(cfg, sid, aid, captureScope);
9590
9659
  const rawTraceEvents: any[] = [];
9591
9660
  const maskReq: MaskRequestContext = {