@reproapp/node-sdk 0.0.49 → 0.0.51
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 +44 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +83 -31
- package/dist/ingest/client.d.ts +2 -2
- package/dist/ingest/client.js +33 -7
- package/dist/live-capture-limits.d.ts +19 -0
- package/dist/live-capture-limits.js +207 -0
- package/dist/semantic-serialization.d.ts +1 -0
- package/dist/semantic-serialization.js +33 -1
- package/package.json +7 -2
- package/src/index.ts +103 -31
- package/src/ingest/client.ts +36 -10
- package/src/live-capture-limits.ts +177 -0
- package/src/semantic-serialization.ts +30 -0
- package/test/background-continuous-traffic.testcontainers.test.js +100 -0
- package/test/fixtures/live-capture-limits-server.js +21 -0
- package/test/ingest-throttle-retention.test.js +41 -0
- package/test/live-broker-limits.testcontainers.test.js +86 -0
- package/test/live-capture-limits.test.js +99 -0
- package/test/live-capture-limits.testcontainers.test.js +78 -0
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
|
-
|
|
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,
|
|
@@ -1119,22 +1121,20 @@ function hasActiveClientRequests() {
|
|
|
1119
1121
|
function noteClientActivity(now = Date.now()) {
|
|
1120
1122
|
lastClientActivityAt = now;
|
|
1121
1123
|
}
|
|
1122
|
-
function getSdkBackgroundDelayMs(now = Date.now()) {
|
|
1124
|
+
function getSdkBackgroundDelayMs(now = Date.now(), ingestEnqueuedAt) {
|
|
1125
|
+
// Bound deferral by work age, not just by a continuously active request.
|
|
1126
|
+
// Short non-overlapping requests reset oldestActiveClientRequestAt forever.
|
|
1127
|
+
const oldestWorkAt = Math.min(sdkBackgroundHighQueue[0]?.enqueuedAt ?? Infinity, sdkBackgroundQueue[0]?.enqueuedAt ?? Infinity, ingestEnqueuedAt ?? Infinity, oldestActiveClientRequestAt ?? Infinity);
|
|
1128
|
+
const remainingDeferMs = Number.isFinite(oldestWorkAt) && Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS)
|
|
1129
|
+
? Math.max(0, oldestWorkAt + SDK_BACKGROUND_MAX_DEFER_MS - now)
|
|
1130
|
+
: Infinity;
|
|
1123
1131
|
if (hasActiveClientRequests()) {
|
|
1124
|
-
|
|
1125
|
-
Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS) &&
|
|
1126
|
-
SDK_BACKGROUND_MAX_DEFER_MS >= 0) {
|
|
1127
|
-
const activeForMs = now - oldestActiveClientRequestAt;
|
|
1128
|
-
if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS)
|
|
1129
|
-
return 0;
|
|
1130
|
-
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, Math.max(0, SDK_BACKGROUND_MAX_DEFER_MS - activeForMs));
|
|
1131
|
-
}
|
|
1132
|
-
return SDK_BACKGROUND_IDLE_DELAY_MS;
|
|
1132
|
+
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, remainingDeferMs);
|
|
1133
1133
|
}
|
|
1134
1134
|
const sinceLastActivity = now - lastClientActivityAt;
|
|
1135
1135
|
const idleDelayRemaining = Math.max(0, SDK_BACKGROUND_IDLE_DELAY_MS - sinceLastActivity);
|
|
1136
1136
|
const quietWindowRemaining = Math.max(0, sdkBackgroundQuietUntil - now);
|
|
1137
|
-
return Math.max(idleDelayRemaining, quietWindowRemaining);
|
|
1137
|
+
return Math.min(Math.max(idleDelayRemaining, quietWindowRemaining), remainingDeferMs);
|
|
1138
1138
|
}
|
|
1139
1139
|
function shouldDeferSdkBackgroundWork(now = Date.now()) {
|
|
1140
1140
|
return getSdkBackgroundDelayMs(now) > 0;
|
|
@@ -1370,13 +1370,8 @@ function beginClientRequest() {
|
|
|
1370
1370
|
oldestActiveClientRequestAt = now;
|
|
1371
1371
|
}
|
|
1372
1372
|
activeClientRequestCount += 1;
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
clearTimeout(sdkBackgroundTimer);
|
|
1376
|
-
}
|
|
1377
|
-
catch { }
|
|
1378
|
-
sdkBackgroundTimer = null;
|
|
1379
|
-
}
|
|
1373
|
+
// The scheduled drain rechecks activity; cancelling it here loses the
|
|
1374
|
+
// queued work's deadline under sustained traffic.
|
|
1380
1375
|
}
|
|
1381
1376
|
function endClientRequest() {
|
|
1382
1377
|
activeClientRequestCount = Math.max(0, activeClientRequestCount - 1);
|
|
@@ -1395,15 +1390,18 @@ function endClientRequest() {
|
|
|
1395
1390
|
}
|
|
1396
1391
|
}
|
|
1397
1392
|
function scheduleSdkBackgroundWork(work, options = {}) {
|
|
1393
|
+
const item = { work, enqueuedAt: Date.now() };
|
|
1398
1394
|
if (options.priority === 'high') {
|
|
1399
|
-
sdkBackgroundHighQueue.push(
|
|
1395
|
+
sdkBackgroundHighQueue.push(item);
|
|
1400
1396
|
}
|
|
1401
1397
|
else {
|
|
1402
|
-
sdkBackgroundQueue.push(
|
|
1398
|
+
sdkBackgroundQueue.push(item);
|
|
1403
1399
|
}
|
|
1404
1400
|
scheduleSdkBackgroundDrain();
|
|
1405
1401
|
}
|
|
1406
1402
|
function scheduleSdkBackgroundDrain(delayMs = 0) {
|
|
1403
|
+
if (!sdkBackgroundHighQueue.length && !sdkBackgroundQueue.length)
|
|
1404
|
+
return;
|
|
1407
1405
|
if (sdkBackgroundTimer)
|
|
1408
1406
|
return;
|
|
1409
1407
|
const nextDelay = Math.max(delayMs, getSdkBackgroundDelayMs());
|
|
@@ -1439,12 +1437,12 @@ async function drainSdkBackgroundQueue() {
|
|
|
1439
1437
|
scheduleSdkBackgroundDrain();
|
|
1440
1438
|
return;
|
|
1441
1439
|
}
|
|
1442
|
-
const
|
|
1443
|
-
if (!
|
|
1440
|
+
const item = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
|
|
1441
|
+
if (!item)
|
|
1444
1442
|
continue;
|
|
1445
1443
|
try {
|
|
1446
1444
|
await drainQueuedIngestBeforeBackgroundWork();
|
|
1447
|
-
await runSdkBackgroundWork(work);
|
|
1445
|
+
await runSdkBackgroundWork(item.work);
|
|
1448
1446
|
}
|
|
1449
1447
|
catch {
|
|
1450
1448
|
// never break user code
|
|
@@ -1462,8 +1460,8 @@ async function drainSdkBackgroundQueue() {
|
|
|
1462
1460
|
}
|
|
1463
1461
|
}
|
|
1464
1462
|
(0, client_1.configureIngestQueueDrain)({
|
|
1465
|
-
shouldDefer:
|
|
1466
|
-
deferMs: SDK_BACKGROUND_IDLE_DELAY_MS,
|
|
1463
|
+
shouldDefer: (enqueuedAt) => getSdkBackgroundDelayMs(Date.now(), enqueuedAt) > 0,
|
|
1464
|
+
deferMs: Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, SDK_BACKGROUND_MAX_DEFER_MS),
|
|
1467
1465
|
});
|
|
1468
1466
|
// Track in-flight requests per session so we can delay flushing until a session is drained.
|
|
1469
1467
|
const sessionInFlight = new Map();
|
|
@@ -2054,6 +2052,7 @@ async function resolveBackendLiveContext(cfg, req, requestStart) {
|
|
|
2054
2052
|
}
|
|
2055
2053
|
const { actorId, actorType, actorLabels } = identity;
|
|
2056
2054
|
const claim = await postLiveApiJson(cfg, liveMode, '/v1/live-sessions/claim', {
|
|
2055
|
+
liveCapture: liveMode.liveCapture,
|
|
2057
2056
|
clientTime: requestStart,
|
|
2058
2057
|
captureMode: 'backend-only',
|
|
2059
2058
|
actorId,
|
|
@@ -2156,6 +2155,7 @@ function stripReproQueryFromUrl(rawUrl) {
|
|
|
2156
2155
|
parsed.searchParams.delete(REPRO_QUERY_SESSION_PARAM);
|
|
2157
2156
|
parsed.searchParams.delete(REPRO_QUERY_ACTION_PARAM);
|
|
2158
2157
|
parsed.searchParams.delete(REPRO_QUERY_START_PARAM);
|
|
2158
|
+
parsed.searchParams.delete('__repro_mode');
|
|
2159
2159
|
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
2160
2160
|
}
|
|
2161
2161
|
catch {
|
|
@@ -2169,6 +2169,7 @@ function stripReproQueryParams(req) {
|
|
|
2169
2169
|
delete query[REPRO_QUERY_SESSION_PARAM];
|
|
2170
2170
|
delete query[REPRO_QUERY_ACTION_PARAM];
|
|
2171
2171
|
delete query[REPRO_QUERY_START_PARAM];
|
|
2172
|
+
delete query.__repro_mode;
|
|
2172
2173
|
}
|
|
2173
2174
|
}
|
|
2174
2175
|
catch { }
|
|
@@ -2834,6 +2835,11 @@ function internTraceObservationSnapshot(snapshot, captureScope) {
|
|
|
2834
2835
|
}
|
|
2835
2836
|
function snapshotTraceValueAtObservation(value, captureScope, valuePath = '$', target = 'trace.value') {
|
|
2836
2837
|
try {
|
|
2838
|
+
if (captureScope?.liveCapture) {
|
|
2839
|
+
value = target === 'trace.args' && Array.isArray(value)
|
|
2840
|
+
? (0, live_capture_limits_1.limitLiveArguments)(value)
|
|
2841
|
+
: (0, live_capture_limits_1.limitLiveCapture)(value, valuePath).value;
|
|
2842
|
+
}
|
|
2837
2843
|
const snapshot = sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true }, valuePath);
|
|
2838
2844
|
return internTraceObservationSnapshot(snapshot, captureScope);
|
|
2839
2845
|
}
|
|
@@ -3018,6 +3024,7 @@ async function materializePendingTraceEventsAsync(events, params) {
|
|
|
3018
3024
|
appSecret: params.cfg.appSecret,
|
|
3019
3025
|
appName: params.cfg.appName,
|
|
3020
3026
|
privacy: params.cfg.privacy,
|
|
3027
|
+
captureMode: params.captureScope?.liveCapture ? 'live' : params.cfg.captureMode,
|
|
3021
3028
|
},
|
|
3022
3029
|
maskReq: params.maskReq,
|
|
3023
3030
|
masking: params.masking,
|
|
@@ -3439,7 +3446,20 @@ async function initializeRuntimePrivacyPolicyAtStartup(cfg) {
|
|
|
3439
3446
|
}
|
|
3440
3447
|
throw new Error(`Runtime privacy policy failed to load during startup from ${apiBase} for env ${state.environment} (${lastFailure}).`);
|
|
3441
3448
|
}
|
|
3449
|
+
function isLiveCapture(cfg, scope) {
|
|
3450
|
+
if (scope)
|
|
3451
|
+
return scope.liveCapture === true;
|
|
3452
|
+
return cfg.captureMode === 'live' || (cfg.captureMode !== 'staging' && getCtx().captureScope?.liveCapture === true);
|
|
3453
|
+
}
|
|
3454
|
+
function constrainLiveCapture(value, cfg, target, scope) {
|
|
3455
|
+
if (!isLiveCapture(cfg, scope))
|
|
3456
|
+
return value;
|
|
3457
|
+
return target === 'trace.args' && Array.isArray(value)
|
|
3458
|
+
? (0, live_capture_limits_1.limitLiveArguments)(value)
|
|
3459
|
+
: (0, live_capture_limits_1.limitLiveCapture)(value).value;
|
|
3460
|
+
}
|
|
3442
3461
|
function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy, db) {
|
|
3462
|
+
value = constrainLiveCapture(value, cfg, target);
|
|
3443
3463
|
if (shouldCapturePrivacyRaw(cfg)) {
|
|
3444
3464
|
return value;
|
|
3445
3465
|
}
|
|
@@ -3460,6 +3480,7 @@ function applyPrivacyThenMask(target, value, cfg, req, trace, masking, privacy,
|
|
|
3460
3480
|
return applyMasking(target, transformed, req, trace, masking);
|
|
3461
3481
|
}
|
|
3462
3482
|
async function applyPrivacyThenMaskAsync(target, value, cfg, req, trace, masking, privacy, db) {
|
|
3483
|
+
value = constrainLiveCapture(value, cfg, target);
|
|
3463
3484
|
if (shouldCapturePrivacyRaw(cfg)) {
|
|
3464
3485
|
return value;
|
|
3465
3486
|
}
|
|
@@ -3506,6 +3527,7 @@ function limitRawInlinePrivacyValue(target, value) {
|
|
|
3506
3527
|
return limited.skipped ? limited : null;
|
|
3507
3528
|
}
|
|
3508
3529
|
function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
|
|
3530
|
+
value = constrainLiveCapture(value, cfg, target, captureScope);
|
|
3509
3531
|
if (value === undefined) {
|
|
3510
3532
|
return { value };
|
|
3511
3533
|
}
|
|
@@ -3529,6 +3551,7 @@ function materializeInlinePrivacyValue(target, value, cfg, req, trace, masking,
|
|
|
3529
3551
|
}
|
|
3530
3552
|
}
|
|
3531
3553
|
async function materializeInlinePrivacyValueAsync(target, value, cfg, req, trace, masking, privacy, db, captureScope) {
|
|
3554
|
+
value = constrainLiveCapture(value, cfg, target, captureScope);
|
|
3532
3555
|
if (value === undefined) {
|
|
3533
3556
|
return { value };
|
|
3534
3557
|
}
|
|
@@ -4505,6 +4528,8 @@ function boundTraceEventsForTransport(events) {
|
|
|
4505
4528
|
function createCapturedValueEntry(params) {
|
|
4506
4529
|
if (!__FULL_VALUE_CAPTURE_ENABLED)
|
|
4507
4530
|
return undefined;
|
|
4531
|
+
const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
|
|
4532
|
+
const omissions = isLiveCapture(params.capture.runtimeConfig) ? (0, live_capture_limits_1.collectCaptureOmissions)(limitedRaw) : [];
|
|
4508
4533
|
const preferredLimits = {
|
|
4509
4534
|
maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
|
|
4510
4535
|
maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
|
|
@@ -4514,7 +4539,7 @@ function createCapturedValueEntry(params) {
|
|
|
4514
4539
|
const buildMaskedCandidate = (limits) => {
|
|
4515
4540
|
const projected = params.target === 'request.headers'
|
|
4516
4541
|
? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
|
|
4517
|
-
: cloneFullValueCandidate(
|
|
4542
|
+
: cloneFullValueCandidate(limitedRaw, params.capture, limits);
|
|
4518
4543
|
if (projected === undefined)
|
|
4519
4544
|
return undefined;
|
|
4520
4545
|
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 +4550,7 @@ function createCapturedValueEntry(params) {
|
|
|
4525
4550
|
return { captureRef: { status: 'skipped-non-meaningful' } };
|
|
4526
4551
|
}
|
|
4527
4552
|
const candidateSerialized = stableSerializeTraceValue(candidate);
|
|
4528
|
-
if (!candidateSerialized || candidateSerialized === previewSerialized) {
|
|
4553
|
+
if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
|
|
4529
4554
|
return undefined;
|
|
4530
4555
|
}
|
|
4531
4556
|
const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
|
|
@@ -4545,6 +4570,7 @@ function createCapturedValueEntry(params) {
|
|
|
4545
4570
|
status: 'stored',
|
|
4546
4571
|
valueId,
|
|
4547
4572
|
projectedKind: entry.projectedKind,
|
|
4573
|
+
...(omissions.length ? { complete: false, omissions } : {}),
|
|
4548
4574
|
},
|
|
4549
4575
|
entry,
|
|
4550
4576
|
};
|
|
@@ -4552,6 +4578,8 @@ function createCapturedValueEntry(params) {
|
|
|
4552
4578
|
async function createCapturedValueEntryAsync(params) {
|
|
4553
4579
|
if (!__FULL_VALUE_CAPTURE_ENABLED)
|
|
4554
4580
|
return undefined;
|
|
4581
|
+
const limitedRaw = constrainLiveCapture(params.rawValue, params.capture.runtimeConfig, params.target);
|
|
4582
|
+
const omissions = isLiveCapture(params.capture.runtimeConfig) ? (0, live_capture_limits_1.collectCaptureOmissions)(limitedRaw) : [];
|
|
4555
4583
|
const preferredLimits = {
|
|
4556
4584
|
maxDepth: TRACE_FULL_VALUE_MAX_DEPTH,
|
|
4557
4585
|
maxKeys: TRACE_FULL_VALUE_MAX_KEYS,
|
|
@@ -4561,7 +4589,7 @@ async function createCapturedValueEntryAsync(params) {
|
|
|
4561
4589
|
const buildMaskedCandidate = async (limits) => {
|
|
4562
4590
|
const projected = params.target === 'request.headers'
|
|
4563
4591
|
? sanitizeHeaders(params.rawValue, params.capture.captureHeaders)
|
|
4564
|
-
: await cloneFullValueCandidateAsync(
|
|
4592
|
+
: await cloneFullValueCandidateAsync(limitedRaw, params.capture, limits);
|
|
4565
4593
|
if (projected === undefined)
|
|
4566
4594
|
return undefined;
|
|
4567
4595
|
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 +4600,7 @@ async function createCapturedValueEntryAsync(params) {
|
|
|
4572
4600
|
return { captureRef: { status: 'skipped-non-meaningful' } };
|
|
4573
4601
|
}
|
|
4574
4602
|
const candidateSerialized = stableSerializeTraceValue(candidate);
|
|
4575
|
-
if (!candidateSerialized || candidateSerialized === previewSerialized) {
|
|
4603
|
+
if (!candidateSerialized || (candidateSerialized === previewSerialized && !omissions.length)) {
|
|
4576
4604
|
return undefined;
|
|
4577
4605
|
}
|
|
4578
4606
|
const valueId = contentAddressedValueId(params.target, candidateSerialized, params.capture);
|
|
@@ -4592,6 +4620,7 @@ async function createCapturedValueEntryAsync(params) {
|
|
|
4592
4620
|
status: 'stored',
|
|
4593
4621
|
valueId,
|
|
4594
4622
|
projectedKind: entry.projectedKind,
|
|
4623
|
+
...(omissions.length ? { complete: false, omissions } : {}),
|
|
4595
4624
|
},
|
|
4596
4625
|
entry,
|
|
4597
4626
|
};
|
|
@@ -4794,6 +4823,7 @@ function reproMiddleware(cfg) {
|
|
|
4794
4823
|
const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
|
|
4795
4824
|
const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
|
|
4796
4825
|
const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
|
|
4826
|
+
const queryCaptureMode = readRequestQueryParam(req, '__repro_mode');
|
|
4797
4827
|
if (querySid || queryAid || queryTs !== null) {
|
|
4798
4828
|
stripReproQueryParams(req);
|
|
4799
4829
|
}
|
|
@@ -4863,6 +4893,11 @@ function reproMiddleware(cfg) {
|
|
|
4863
4893
|
}],
|
|
4864
4894
|
}, { priority: 'high' });
|
|
4865
4895
|
}
|
|
4896
|
+
const captureMode = backendLiveContext || req.headers['x-repro-capture-mode'] === 'live' || queryCaptureMode === 'live'
|
|
4897
|
+
? 'live' : 'staging';
|
|
4898
|
+
const modeConfig = { ...captureCfg, captureMode };
|
|
4899
|
+
shareRuntimePrivacyState(captureCfg, modeConfig);
|
|
4900
|
+
captureCfg = modeConfig;
|
|
4866
4901
|
const resolvePrivacy = () => getRuntimePrivacyState(captureCfg).policy ?? null;
|
|
4867
4902
|
const clockSkewMs = headerTs !== null ? headerTs - requestStartRaw : 0;
|
|
4868
4903
|
const rid = nextRequestId(requestEpochMs);
|
|
@@ -6287,6 +6322,8 @@ function patchNodeFetchForPropagation() {
|
|
|
6287
6322
|
if (runtime.aid && !hasHttpHeader(headers, KAFKA_HEADER_ACTION_ID)) {
|
|
6288
6323
|
setHttpHeader(headers, KAFKA_HEADER_ACTION_ID, runtime.aid);
|
|
6289
6324
|
}
|
|
6325
|
+
if (runtime.sid)
|
|
6326
|
+
setHttpHeader(headers, 'x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
|
|
6290
6327
|
if (runtime.traceId && !hasHttpHeader(headers, KAFKA_HEADER_TRACE_ID)) {
|
|
6291
6328
|
setHttpHeader(headers, KAFKA_HEADER_TRACE_ID, runtime.traceId);
|
|
6292
6329
|
}
|
|
@@ -6337,6 +6374,8 @@ function patchNodeHttpModuleRequest(mod) {
|
|
|
6337
6374
|
maybeSet(REQUEST_START_HEADER, String(alignedNow()));
|
|
6338
6375
|
maybeSet(KAFKA_HEADER_SESSION_ID, runtime.sid);
|
|
6339
6376
|
maybeSet(KAFKA_HEADER_ACTION_ID, runtime.aid);
|
|
6377
|
+
if (runtime.sid)
|
|
6378
|
+
maybeSet('x-repro-capture-mode', getCtx().captureScope?.liveCapture ? 'live' : 'staging');
|
|
6340
6379
|
maybeSet(KAFKA_HEADER_TRACE_ID, runtime.traceId);
|
|
6341
6380
|
maybeSet(KAFKA_HEADER_PARENT_SPAN_ID, runtime.parentSpanId);
|
|
6342
6381
|
maybeSet(KAFKA_HEADER_REQUEST_ID, runtime.requestRid);
|
|
@@ -6513,6 +6552,11 @@ function cloneKafkaTelemetryValue(value) {
|
|
|
6513
6552
|
}
|
|
6514
6553
|
}
|
|
6515
6554
|
async function materializeKafkaRequestBody(cfg, maskReq, body, captureScope) {
|
|
6555
|
+
// Broker bodies have no separate full-value reference. Returning the trace
|
|
6556
|
+
// inline preview here would permanently replace message arrays with 20 items.
|
|
6557
|
+
if (shouldCapturePrivacyRaw(cfg)) {
|
|
6558
|
+
return { value: constrainLiveCapture(sanitizeInlinePrivacyValue(body), cfg, 'request.body', captureScope) };
|
|
6559
|
+
}
|
|
6516
6560
|
return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null, undefined, captureScope);
|
|
6517
6561
|
}
|
|
6518
6562
|
function summarizeKafkaError(error) {
|
|
@@ -6557,7 +6601,8 @@ function resolveKafkaContext(cfg, headers) {
|
|
|
6557
6601
|
readKafkaHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
|
|
6558
6602
|
null;
|
|
6559
6603
|
const requestRid = readKafkaHeader(headers, KAFKA_HEADER_REQUEST_ID);
|
|
6560
|
-
return { sid, aid, traceId, parentSpanId, requestRid
|
|
6604
|
+
return { sid, aid, traceId, parentSpanId, requestRid,
|
|
6605
|
+
captureMode: readKafkaHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
|
|
6561
6606
|
}
|
|
6562
6607
|
function withOptionalTraceContext(traceId, fn) {
|
|
6563
6608
|
const tracerWithTrace = __TRACER__?.withTrace;
|
|
@@ -6857,6 +6902,7 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
6857
6902
|
[KAFKA_HEADER_REQUEST_ID]: publishRid,
|
|
6858
6903
|
[KAFKA_HEADER_SESSION_ID]: sid,
|
|
6859
6904
|
[KAFKA_HEADER_ACTION_ID]: aid,
|
|
6905
|
+
'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
|
|
6860
6906
|
}),
|
|
6861
6907
|
};
|
|
6862
6908
|
});
|
|
@@ -6979,6 +7025,7 @@ function patchKafkaProducerInstance(producer, cfg) {
|
|
|
6979
7025
|
[KAFKA_HEADER_REQUEST_ID]: publishRid,
|
|
6980
7026
|
[KAFKA_HEADER_SESSION_ID]: sid,
|
|
6981
7027
|
[KAFKA_HEADER_ACTION_ID]: aid,
|
|
7028
|
+
'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
|
|
6982
7029
|
}),
|
|
6983
7030
|
};
|
|
6984
7031
|
}),
|
|
@@ -7091,6 +7138,7 @@ function wrapKafkaEachMessageHandler(eachMessage, cfg, consumer) {
|
|
|
7091
7138
|
const consumeRid = nextRequestId(`${topic}-${message?.offset ?? alignedNow()}`);
|
|
7092
7139
|
const traceId = consumeRid;
|
|
7093
7140
|
const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
|
|
7141
|
+
captureScope.liveCapture = runtime.captureMode === 'live';
|
|
7094
7142
|
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7095
7143
|
const rawTraceEvents = [];
|
|
7096
7144
|
const maskReq = {
|
|
@@ -7205,6 +7253,7 @@ function wrapKafkaEachBatchHandler(eachBatch, cfg, consumer) {
|
|
|
7205
7253
|
const consumeRid = nextRequestId(`${topic}-batch-${alignedNow()}`);
|
|
7206
7254
|
const traceId = consumeRid;
|
|
7207
7255
|
const captureScope = createRequestCaptureScope(consumeRid, 'kafka', cfg);
|
|
7256
|
+
captureScope.liveCapture = runtime.captureMode === 'live';
|
|
7208
7257
|
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7209
7258
|
const rawTraceEvents = [];
|
|
7210
7259
|
const maskReq = {
|
|
@@ -7642,7 +7691,8 @@ function resolveAmqpContext(cfg, headers) {
|
|
|
7642
7691
|
readAmqpHeader(headers, KAFKA_HEADER_PARENT_SPAN_ID) ??
|
|
7643
7692
|
null;
|
|
7644
7693
|
const requestRid = readAmqpHeader(headers, KAFKA_HEADER_REQUEST_ID);
|
|
7645
|
-
return { sid, aid, traceId, parentSpanId, requestRid
|
|
7694
|
+
return { sid, aid, traceId, parentSpanId, requestRid,
|
|
7695
|
+
captureMode: readAmqpHeader(headers, 'x-repro-capture-mode') === 'live' || ctx.captureScope?.liveCapture ? 'live' : 'staging' };
|
|
7646
7696
|
}
|
|
7647
7697
|
function withOptionalTraceContextSync(traceId, fn) {
|
|
7648
7698
|
const tracerWithTrace = __TRACER__?.withTrace;
|
|
@@ -7817,6 +7867,7 @@ function patchAmqplibPublishMethod(channel, cfg, methodName) {
|
|
|
7817
7867
|
[KAFKA_HEADER_REQUEST_ID]: publishRid,
|
|
7818
7868
|
[KAFKA_HEADER_SESSION_ID]: sid,
|
|
7819
7869
|
[KAFKA_HEADER_ACTION_ID]: aid,
|
|
7870
|
+
'x-repro-capture-mode': ctx.captureScope?.liveCapture ? 'live' : 'staging',
|
|
7820
7871
|
};
|
|
7821
7872
|
const shouldPropagate = hasAmqpPropagationValues(propagationValues);
|
|
7822
7873
|
const callbackIndex = args.length > 0 && typeof args[args.length - 1] === 'function'
|
|
@@ -7922,6 +7973,7 @@ function wrapAmqplibConsumeHandler(onMessage, cfg, queue) {
|
|
|
7922
7973
|
const consumeRid = nextRequestId(`${queueName}-${deliveryTag}`);
|
|
7923
7974
|
const traceId = consumeRid;
|
|
7924
7975
|
const captureScope = createRequestCaptureScope(consumeRid, 'amqp', cfg);
|
|
7976
|
+
captureScope.liveCapture = runtime.captureMode === 'live';
|
|
7925
7977
|
emitRequestCaptureStart(cfg, sid, aid, captureScope);
|
|
7926
7978
|
const rawTraceEvents = [];
|
|
7927
7979
|
const maskReq = {
|
package/dist/ingest/client.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type EnqueueIngestOptions = {
|
|
|
3
3
|
priority?: 'normal' | 'high' | 'critical';
|
|
4
4
|
};
|
|
5
5
|
export declare const configureIngestQueueDrain: (options: {
|
|
6
|
-
shouldDefer?: (() => boolean) | null;
|
|
7
|
-
deferMs?: number;
|
|
6
|
+
shouldDefer?: ((enqueuedAt?: number) => boolean) | null | undefined;
|
|
7
|
+
deferMs?: number | undefined;
|
|
8
8
|
}) => void;
|
|
9
9
|
export declare const kickIngestQueueDrain: () => void;
|
|
10
10
|
export declare const postIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[]) => Promise<void>;
|
package/dist/ingest/client.js
CHANGED
|
@@ -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) {
|
|
@@ -59,7 +61,7 @@ const kickIngestQueueDrain = () => {
|
|
|
59
61
|
scheduleCriticalDrain();
|
|
60
62
|
}
|
|
61
63
|
if (ingestQueue.length > 0 && !drainTimer) {
|
|
62
|
-
scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
|
|
64
|
+
scheduleDrain(shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt) ? deferDrainMs : 0);
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
67
|
exports.kickIngestQueueDrain = kickIngestQueueDrain;
|
|
@@ -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 {
|
|
@@ -216,12 +233,12 @@ const scheduleDrain = (delayMs = 0) => {
|
|
|
216
233
|
if (drainTimer) {
|
|
217
234
|
return;
|
|
218
235
|
}
|
|
219
|
-
const nextDelay = shouldDeferDrain?.()
|
|
236
|
+
const nextDelay = shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)
|
|
220
237
|
? Math.max(delayMs, deferDrainMs)
|
|
221
238
|
: delayMs;
|
|
222
239
|
drainTimer = setTimeout(() => {
|
|
223
240
|
drainTimer = null;
|
|
224
|
-
if (shouldDeferDrain?.()) {
|
|
241
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
225
242
|
scheduleDrain(deferDrainMs);
|
|
226
243
|
return;
|
|
227
244
|
}
|
|
@@ -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;
|
|
@@ -281,14 +302,14 @@ const drainIngestQueue = async () => {
|
|
|
281
302
|
if (draining) {
|
|
282
303
|
return;
|
|
283
304
|
}
|
|
284
|
-
if (shouldDeferDrain?.()) {
|
|
305
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
285
306
|
scheduleDrain(deferDrainMs);
|
|
286
307
|
return;
|
|
287
308
|
}
|
|
288
309
|
draining = true;
|
|
289
310
|
try {
|
|
290
311
|
while (ingestQueue.length > 0) {
|
|
291
|
-
if (shouldDeferDrain?.()) {
|
|
312
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
292
313
|
scheduleDrain(deferDrainMs);
|
|
293
314
|
return;
|
|
294
315
|
}
|
|
@@ -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;
|
|
@@ -342,6 +367,7 @@ const enqueueIngestEntries = (config, sessionId, entries, options = {}) => {
|
|
|
342
367
|
sessionId,
|
|
343
368
|
entries: entries.slice(),
|
|
344
369
|
attempts: 0,
|
|
370
|
+
enqueuedAt: Date.now(),
|
|
345
371
|
priority,
|
|
346
372
|
};
|
|
347
373
|
if (priority === 'critical') {
|
|
@@ -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[];
|