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