@reproapp/node-sdk 0.0.8 → 0.0.11
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/dist/index.d.ts +14 -0
- package/dist/index.js +117 -16
- package/package.json +2 -2
- package/src/index.ts +152 -18
- package/test/kafka-runtime-privacy-policy.test.js +6 -0
- package/test/sdk-background-priority.test.js +35 -0
- package/test/trace-batch-size.test.js +58 -0
package/dist/index.d.ts
CHANGED
|
@@ -254,6 +254,8 @@ export type ReproTracingInitOptions = TracerInitOpts & {
|
|
|
254
254
|
export declare function initReproTracing(opts?: ReproTracingInitOptions): TracerApi | null;
|
|
255
255
|
/** Optional helper if users want to check it. */
|
|
256
256
|
export declare function isReproTracingEnabled(): boolean;
|
|
257
|
+
declare function sanitizeTraceValueForPrivacy(value: any): any;
|
|
258
|
+
declare function sanitizeMaterializedTraceValue(value: any): any;
|
|
257
259
|
export declare function __materializePendingTraceEventsForWorker(payload: TraceMaterializationWorkerPayload): Promise<PendingTraceEventRecord[]>;
|
|
258
260
|
type NormalizedMaskRule = {
|
|
259
261
|
when?: ReproMaskWhen;
|
|
@@ -283,6 +285,8 @@ type InlinePrivacyMaterializationResult = {
|
|
|
283
285
|
};
|
|
284
286
|
};
|
|
285
287
|
declare function materializeInlinePrivacyValueAsync(target: RuntimePrivacySurface, value: any, cfg: ReproMiddlewareConfig, req: MaskRequestContext, trace: TraceEventForFilter | null, masking: NormalizedMaskingConfig | null, privacy: NormalizedRuntimePrivacyPolicy | null, db?: RuntimePrivacyDbContext | null): Promise<InlinePrivacyMaterializationResult>;
|
|
288
|
+
declare function estimateTraceBatchSerializedBytes(batch: TraceEventRecord[], requestRid: string, actionId: string | null | undefined, batchIndex: number): number;
|
|
289
|
+
declare function chunkTraceEventsForTransport(events: TraceEventRecord[], requestRid: string, actionId: string | null | undefined): TraceEventRecord[][];
|
|
286
290
|
export type ReproMiddlewareConfig = IngestClientConfig & {
|
|
287
291
|
/** Configure header capture/masking. Defaults to capturing with sensitive headers masked. */
|
|
288
292
|
captureHeaders?: boolean | HeaderCaptureOptions;
|
|
@@ -342,11 +346,21 @@ export declare function initRepro(cfg: ReproInitConfig): Promise<void>;
|
|
|
342
346
|
/** @internal Test hooks for runtime privacy policy wiring. */
|
|
343
347
|
export declare const __reproTestHooks: {
|
|
344
348
|
shareRuntimePrivacyStateForTest: typeof shareRuntimePrivacyState;
|
|
349
|
+
setFullValueCaptureEnabledForTest(enabled: boolean): void;
|
|
350
|
+
scheduleSdkBackgroundWorkForTest(work: () => Promise<void> | void, options?: {
|
|
351
|
+
priority?: 'high' | 'normal';
|
|
352
|
+
}): void;
|
|
353
|
+
drainSdkBackgroundQueueForTest(): Promise<void>;
|
|
354
|
+
resetSdkBackgroundQueuesForTest(): void;
|
|
345
355
|
getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null;
|
|
346
356
|
setRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig, policy: NormalizedRuntimePrivacyPolicy | null): void;
|
|
347
357
|
recordKafkaTraceEventAsyncForTest: typeof recordKafkaTraceEventAsync;
|
|
348
358
|
materializeInlinePrivacyValueAsyncForTest: typeof materializeInlinePrivacyValueAsync;
|
|
349
359
|
patchKafkaProducerInstanceForTest: typeof patchKafkaProducerInstance;
|
|
350
360
|
wrapKafkaEachMessageHandlerForTest: typeof wrapKafkaEachMessageHandler;
|
|
361
|
+
chunkTraceEventsForTransportForTest: typeof chunkTraceEventsForTransport;
|
|
362
|
+
estimateTraceBatchSerializedBytesForTest: typeof estimateTraceBatchSerializedBytes;
|
|
363
|
+
sanitizeTraceValueForPrivacyForTest: typeof sanitizeTraceValueForPrivacy;
|
|
364
|
+
sanitizeMaterializedTraceValueForTest: typeof sanitizeMaterializedTraceValue;
|
|
351
365
|
};
|
|
352
366
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -863,6 +863,7 @@ let activeClientRequestCount = 0;
|
|
|
863
863
|
let oldestActiveClientRequestAt = null;
|
|
864
864
|
let lastClientActivityAt = 0;
|
|
865
865
|
let sdkBackgroundQuietUntil = 0;
|
|
866
|
+
const sdkBackgroundHighQueue = [];
|
|
866
867
|
const sdkBackgroundQueue = [];
|
|
867
868
|
let sdkBackgroundTimer = null;
|
|
868
869
|
let sdkBackgroundDraining = false;
|
|
@@ -1155,8 +1156,13 @@ function endClientRequest() {
|
|
|
1155
1156
|
scheduleSdkBackgroundDrain();
|
|
1156
1157
|
}
|
|
1157
1158
|
}
|
|
1158
|
-
function scheduleSdkBackgroundWork(work) {
|
|
1159
|
-
|
|
1159
|
+
function scheduleSdkBackgroundWork(work, options = {}) {
|
|
1160
|
+
if (options.priority === 'high') {
|
|
1161
|
+
sdkBackgroundHighQueue.push(work);
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
sdkBackgroundQueue.push(work);
|
|
1165
|
+
}
|
|
1160
1166
|
scheduleSdkBackgroundDrain();
|
|
1161
1167
|
}
|
|
1162
1168
|
function scheduleSdkBackgroundDrain(delayMs = 0) {
|
|
@@ -1209,12 +1215,12 @@ async function drainSdkBackgroundQueue() {
|
|
|
1209
1215
|
}
|
|
1210
1216
|
sdkBackgroundDraining = true;
|
|
1211
1217
|
try {
|
|
1212
|
-
while (sdkBackgroundQueue.length > 0) {
|
|
1218
|
+
while (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
|
|
1213
1219
|
if (shouldDeferSdkBackgroundWork()) {
|
|
1214
1220
|
scheduleSdkBackgroundDrain();
|
|
1215
1221
|
return;
|
|
1216
1222
|
}
|
|
1217
|
-
const work = sdkBackgroundQueue.shift();
|
|
1223
|
+
const work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
|
|
1218
1224
|
if (!work)
|
|
1219
1225
|
continue;
|
|
1220
1226
|
try {
|
|
@@ -1228,7 +1234,7 @@ async function drainSdkBackgroundQueue() {
|
|
|
1228
1234
|
}
|
|
1229
1235
|
finally {
|
|
1230
1236
|
sdkBackgroundDraining = false;
|
|
1231
|
-
if (sdkBackgroundQueue.length > 0) {
|
|
1237
|
+
if (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
|
|
1232
1238
|
scheduleSdkBackgroundDrain();
|
|
1233
1239
|
}
|
|
1234
1240
|
else {
|
|
@@ -1657,6 +1663,12 @@ const TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = (() => {
|
|
|
1657
1663
|
})();
|
|
1658
1664
|
const TRACE_VALUE_SIZE_EXCEEDED = Symbol('trace-value-size-exceeded');
|
|
1659
1665
|
const TRACE_BATCH_SIZE = 100;
|
|
1666
|
+
const TRACE_BATCH_MAX_SERIALIZED_BYTES = (() => {
|
|
1667
|
+
const env = Number(process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES);
|
|
1668
|
+
if (Number.isFinite(env) && env > 0)
|
|
1669
|
+
return Math.trunc(env);
|
|
1670
|
+
return 512 * 1024;
|
|
1671
|
+
})();
|
|
1660
1672
|
const TRACE_FLUSH_DELAY_MS = 20;
|
|
1661
1673
|
// Choose how to order trace events in payloads.
|
|
1662
1674
|
// - "chronological" (default): preserve event arrival order (no reshuffle).
|
|
@@ -2026,14 +2038,26 @@ function sanitizeTraceValue(value, depth = 0, seen = new WeakMap(), options = {}
|
|
|
2026
2038
|
return circularReference(existingPath);
|
|
2027
2039
|
seen.set(value, valuePath);
|
|
2028
2040
|
if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2041
|
+
if (Array.isArray(value)) {
|
|
2042
|
+
return {
|
|
2043
|
+
__type: 'Array',
|
|
2044
|
+
length: value.length,
|
|
2045
|
+
__truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
|
|
2046
|
+
};
|
|
2032
2047
|
}
|
|
2033
2048
|
const ctor = value?.constructor?.name;
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2049
|
+
const keys = Object.keys(value);
|
|
2050
|
+
const summary = {
|
|
2051
|
+
__truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
|
|
2052
|
+
__keys: keys.slice(0, TRACE_VALUE_MAX_KEYS),
|
|
2053
|
+
};
|
|
2054
|
+
if (keys.length > TRACE_VALUE_MAX_KEYS) {
|
|
2055
|
+
summary.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
|
|
2056
|
+
}
|
|
2057
|
+
if (ctor && ctor !== 'Object') {
|
|
2058
|
+
summary.__class = ctor;
|
|
2059
|
+
}
|
|
2060
|
+
return summary;
|
|
2037
2061
|
}
|
|
2038
2062
|
if (Array.isArray(value)) {
|
|
2039
2063
|
const sourceItems = options.disableTruncation ? value : value.slice(0, TRACE_VALUE_MAX_ITEMS);
|
|
@@ -2099,6 +2123,9 @@ function sanitizeTraceArgs(values) {
|
|
|
2099
2123
|
function sanitizeTraceValueForPrivacy(value) {
|
|
2100
2124
|
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2101
2125
|
}
|
|
2126
|
+
function sanitizeInlinePrivacyValue(value) {
|
|
2127
|
+
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2128
|
+
}
|
|
2102
2129
|
function sanitizeMaterializedTraceValue(value) {
|
|
2103
2130
|
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2104
2131
|
}
|
|
@@ -3526,6 +3553,51 @@ function collectBatchTraceValueEntries(batch, batchIndex) {
|
|
|
3526
3553
|
});
|
|
3527
3554
|
return collected;
|
|
3528
3555
|
}
|
|
3556
|
+
function serializedByteLength(value) {
|
|
3557
|
+
try {
|
|
3558
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
3559
|
+
}
|
|
3560
|
+
catch {
|
|
3561
|
+
return Number.MAX_SAFE_INTEGER;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function estimateTraceBatchSerializedBytes(batch, requestRid, actionId, batchIndex) {
|
|
3565
|
+
const traceValues = collectBatchTraceValueEntries(batch, batchIndex);
|
|
3566
|
+
return serializedByteLength({
|
|
3567
|
+
actionId: actionId ?? null,
|
|
3568
|
+
trace: batch,
|
|
3569
|
+
traceValues: traceValues.length ? traceValues : undefined,
|
|
3570
|
+
traceBatch: {
|
|
3571
|
+
rid: requestRid,
|
|
3572
|
+
index: batchIndex,
|
|
3573
|
+
total: 0,
|
|
3574
|
+
},
|
|
3575
|
+
t: 0,
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
function chunkTraceEventsForTransport(events, requestRid, actionId) {
|
|
3579
|
+
if (!Array.isArray(events) || events.length === 0)
|
|
3580
|
+
return [];
|
|
3581
|
+
const countBatches = chunkArray(events, TRACE_BATCH_SIZE);
|
|
3582
|
+
const sizedBatches = [];
|
|
3583
|
+
let current = [];
|
|
3584
|
+
for (const event of countBatches.flat()) {
|
|
3585
|
+
const candidate = current.concat(event);
|
|
3586
|
+
const batchIndex = sizedBatches.length;
|
|
3587
|
+
const estimatedBytes = estimateTraceBatchSerializedBytes(candidate, requestRid, actionId, batchIndex);
|
|
3588
|
+
if (current.length > 0 &&
|
|
3589
|
+
estimatedBytes > TRACE_BATCH_MAX_SERIALIZED_BYTES) {
|
|
3590
|
+
sizedBatches.push(current);
|
|
3591
|
+
current = [event];
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
current = candidate;
|
|
3595
|
+
}
|
|
3596
|
+
if (current.length > 0) {
|
|
3597
|
+
sizedBatches.push(current);
|
|
3598
|
+
}
|
|
3599
|
+
return sizedBatches;
|
|
3600
|
+
}
|
|
3529
3601
|
function createCapturedValueEntry(params) {
|
|
3530
3602
|
if (!__FULL_VALUE_CAPTURE_ENABLED)
|
|
3531
3603
|
return undefined;
|
|
@@ -3809,7 +3881,7 @@ function reproMiddleware(cfg) {
|
|
|
3809
3881
|
}
|
|
3810
3882
|
catch { }
|
|
3811
3883
|
if (flushPayload) {
|
|
3812
|
-
const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload);
|
|
3884
|
+
const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload, { priority: 'high' });
|
|
3813
3885
|
if (sessionDrainWait) {
|
|
3814
3886
|
sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
|
|
3815
3887
|
}
|
|
@@ -4109,7 +4181,7 @@ function reproMiddleware(cfg) {
|
|
|
4109
4181
|
const orderedEvents = TRACE_ORDER_MODE === 'tree'
|
|
4110
4182
|
? reorderTraceEvents(baseEvents)
|
|
4111
4183
|
: sortTraceEventsChronologically(baseEvents);
|
|
4112
|
-
const traceBatches =
|
|
4184
|
+
const traceBatches = chunkTraceEventsForTransport(orderedEvents, rid, aid);
|
|
4113
4185
|
if (traceBatches.length) {
|
|
4114
4186
|
for (let i = 0; i < traceBatches.length; i++) {
|
|
4115
4187
|
const batch = traceBatches[i];
|
|
@@ -5125,7 +5197,7 @@ function cloneKafkaTelemetryValue(value) {
|
|
|
5125
5197
|
}
|
|
5126
5198
|
}
|
|
5127
5199
|
async function materializeKafkaRequestBody(cfg, maskReq, body) {
|
|
5128
|
-
return materializeInlinePrivacyValueAsync('request.body',
|
|
5200
|
+
return materializeInlinePrivacyValueAsync('request.body', sanitizeInlinePrivacyValue(body), cfg, maskReq, null, normalizeMaskingConfig(cfg.masking), getRuntimePrivacyState(cfg).policy ?? null);
|
|
5129
5201
|
}
|
|
5130
5202
|
function summarizeKafkaError(error) {
|
|
5131
5203
|
const message = typeof error?.message === 'string'
|
|
@@ -5312,7 +5384,7 @@ function buildKafkaTraceEntries(actionId, requestRid, events) {
|
|
|
5312
5384
|
const orderedEvents = TRACE_ORDER_MODE === 'tree'
|
|
5313
5385
|
? reorderTraceEvents(baseEvents)
|
|
5314
5386
|
: sortTraceEventsChronologically(baseEvents);
|
|
5315
|
-
const batches =
|
|
5387
|
+
const batches = chunkTraceEventsForTransport(orderedEvents, requestRid, actionId);
|
|
5316
5388
|
return batches.map((batch, index) => ({
|
|
5317
5389
|
actionId: actionId ?? null,
|
|
5318
5390
|
trace: batch,
|
|
@@ -5353,7 +5425,7 @@ function scheduleKafkaTraceFlush(cfg, kafkaCtx, actionId, requestRid, rawTraceEv
|
|
|
5353
5425
|
post(cfg, sid, { entries });
|
|
5354
5426
|
}
|
|
5355
5427
|
});
|
|
5356
|
-
});
|
|
5428
|
+
}, { priority: 'high' });
|
|
5357
5429
|
}
|
|
5358
5430
|
function patchKafkaProducerInstance(producer, cfg) {
|
|
5359
5431
|
if (!producer || producer.__repro_kafka_producer_patched)
|
|
@@ -6616,6 +6688,31 @@ exports.initRepro = initRepro;
|
|
|
6616
6688
|
/** @internal Test hooks for runtime privacy policy wiring. */
|
|
6617
6689
|
exports.__reproTestHooks = {
|
|
6618
6690
|
shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
|
|
6691
|
+
setFullValueCaptureEnabledForTest(enabled) {
|
|
6692
|
+
__FULL_VALUE_CAPTURE_ENABLED = enabled === true;
|
|
6693
|
+
},
|
|
6694
|
+
scheduleSdkBackgroundWorkForTest(work, options) {
|
|
6695
|
+
scheduleSdkBackgroundWork(work, options);
|
|
6696
|
+
},
|
|
6697
|
+
async drainSdkBackgroundQueueForTest() {
|
|
6698
|
+
await drainSdkBackgroundQueue();
|
|
6699
|
+
},
|
|
6700
|
+
resetSdkBackgroundQueuesForTest() {
|
|
6701
|
+
sdkBackgroundHighQueue.length = 0;
|
|
6702
|
+
sdkBackgroundQueue.length = 0;
|
|
6703
|
+
if (sdkBackgroundTimer) {
|
|
6704
|
+
try {
|
|
6705
|
+
clearTimeout(sdkBackgroundTimer);
|
|
6706
|
+
}
|
|
6707
|
+
catch { }
|
|
6708
|
+
sdkBackgroundTimer = null;
|
|
6709
|
+
}
|
|
6710
|
+
sdkBackgroundDraining = false;
|
|
6711
|
+
activeClientRequestCount = 0;
|
|
6712
|
+
oldestActiveClientRequestAt = null;
|
|
6713
|
+
lastClientActivityAt = 0;
|
|
6714
|
+
sdkBackgroundQuietUntil = 0;
|
|
6715
|
+
},
|
|
6619
6716
|
getRuntimePrivacyPolicyForTest(cfg) {
|
|
6620
6717
|
return getRuntimePrivacyState(cfg).policy;
|
|
6621
6718
|
},
|
|
@@ -6626,4 +6723,8 @@ exports.__reproTestHooks = {
|
|
|
6626
6723
|
materializeInlinePrivacyValueAsyncForTest: materializeInlinePrivacyValueAsync,
|
|
6627
6724
|
patchKafkaProducerInstanceForTest: patchKafkaProducerInstance,
|
|
6628
6725
|
wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
|
|
6726
|
+
chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
|
|
6727
|
+
estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
|
|
6728
|
+
sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
|
|
6729
|
+
sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
|
|
6629
6730
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
13
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
|
-
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/express-trace-http-args.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
|
|
15
|
+
"test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"express": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -1231,6 +1231,7 @@ let activeClientRequestCount = 0;
|
|
|
1231
1231
|
let oldestActiveClientRequestAt: number | null = null;
|
|
1232
1232
|
let lastClientActivityAt = 0;
|
|
1233
1233
|
let sdkBackgroundQuietUntil = 0;
|
|
1234
|
+
const sdkBackgroundHighQueue: Array<() => Promise<void> | void> = [];
|
|
1234
1235
|
const sdkBackgroundQueue: Array<() => Promise<void> | void> = [];
|
|
1235
1236
|
let sdkBackgroundTimer: NodeJS.Timeout | null = null;
|
|
1236
1237
|
let sdkBackgroundDraining = false;
|
|
@@ -1499,8 +1500,15 @@ function endClientRequest(): void {
|
|
|
1499
1500
|
}
|
|
1500
1501
|
}
|
|
1501
1502
|
|
|
1502
|
-
function scheduleSdkBackgroundWork(
|
|
1503
|
-
|
|
1503
|
+
function scheduleSdkBackgroundWork(
|
|
1504
|
+
work: () => Promise<void> | void,
|
|
1505
|
+
options: { priority?: 'high' | 'normal' } = {},
|
|
1506
|
+
): void {
|
|
1507
|
+
if (options.priority === 'high') {
|
|
1508
|
+
sdkBackgroundHighQueue.push(work);
|
|
1509
|
+
} else {
|
|
1510
|
+
sdkBackgroundQueue.push(work);
|
|
1511
|
+
}
|
|
1504
1512
|
scheduleSdkBackgroundDrain();
|
|
1505
1513
|
}
|
|
1506
1514
|
|
|
@@ -1549,12 +1557,12 @@ async function drainSdkBackgroundQueue(): Promise<void> {
|
|
|
1549
1557
|
|
|
1550
1558
|
sdkBackgroundDraining = true;
|
|
1551
1559
|
try {
|
|
1552
|
-
while (sdkBackgroundQueue.length > 0) {
|
|
1560
|
+
while (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
|
|
1553
1561
|
if (shouldDeferSdkBackgroundWork()) {
|
|
1554
1562
|
scheduleSdkBackgroundDrain();
|
|
1555
1563
|
return;
|
|
1556
1564
|
}
|
|
1557
|
-
const work = sdkBackgroundQueue.shift();
|
|
1565
|
+
const work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
|
|
1558
1566
|
if (!work) continue;
|
|
1559
1567
|
try {
|
|
1560
1568
|
await drainQueuedIngestBeforeBackgroundWork();
|
|
@@ -1565,7 +1573,7 @@ async function drainSdkBackgroundQueue(): Promise<void> {
|
|
|
1565
1573
|
}
|
|
1566
1574
|
} finally {
|
|
1567
1575
|
sdkBackgroundDraining = false;
|
|
1568
|
-
if (sdkBackgroundQueue.length > 0) {
|
|
1576
|
+
if (sdkBackgroundHighQueue.length > 0 || sdkBackgroundQueue.length > 0) {
|
|
1569
1577
|
scheduleSdkBackgroundDrain();
|
|
1570
1578
|
} else {
|
|
1571
1579
|
kickIngestQueueDrain();
|
|
@@ -2028,6 +2036,11 @@ const TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = (() => {
|
|
|
2028
2036
|
})();
|
|
2029
2037
|
const TRACE_VALUE_SIZE_EXCEEDED = Symbol('trace-value-size-exceeded');
|
|
2030
2038
|
const TRACE_BATCH_SIZE = 100;
|
|
2039
|
+
const TRACE_BATCH_MAX_SERIALIZED_BYTES = (() => {
|
|
2040
|
+
const env = Number(process.env.REPRO_TRACE_BATCH_MAX_SERIALIZED_BYTES);
|
|
2041
|
+
if (Number.isFinite(env) && env > 0) return Math.trunc(env);
|
|
2042
|
+
return 512 * 1024;
|
|
2043
|
+
})();
|
|
2031
2044
|
const TRACE_FLUSH_DELAY_MS = 20;
|
|
2032
2045
|
// Choose how to order trace events in payloads.
|
|
2033
2046
|
// - "chronological" (default): preserve event arrival order (no reshuffle).
|
|
@@ -2424,14 +2437,26 @@ function sanitizeTraceValue(
|
|
|
2424
2437
|
seen.set(value, valuePath);
|
|
2425
2438
|
|
|
2426
2439
|
if (!options.disableTruncation && depth >= TRACE_VALUE_MAX_DEPTH) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2440
|
+
if (Array.isArray(value)) {
|
|
2441
|
+
return {
|
|
2442
|
+
__type: 'Array',
|
|
2443
|
+
length: value.length,
|
|
2444
|
+
__truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
|
|
2445
|
+
};
|
|
2430
2446
|
}
|
|
2431
2447
|
const ctor = value?.constructor?.name;
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2448
|
+
const keys = Object.keys(value);
|
|
2449
|
+
const summary: Record<string, any> = {
|
|
2450
|
+
__truncated: `depth>${TRACE_VALUE_MAX_DEPTH}`,
|
|
2451
|
+
__keys: keys.slice(0, TRACE_VALUE_MAX_KEYS),
|
|
2452
|
+
};
|
|
2453
|
+
if (keys.length > TRACE_VALUE_MAX_KEYS) {
|
|
2454
|
+
summary.__truncatedKeys = keys.length - TRACE_VALUE_MAX_KEYS;
|
|
2455
|
+
}
|
|
2456
|
+
if (ctor && ctor !== 'Object') {
|
|
2457
|
+
summary.__class = ctor;
|
|
2458
|
+
}
|
|
2459
|
+
return summary;
|
|
2435
2460
|
}
|
|
2436
2461
|
|
|
2437
2462
|
if (Array.isArray(value)) {
|
|
@@ -2500,6 +2525,10 @@ function sanitizeTraceValueForPrivacy(value: any): any {
|
|
|
2500
2525
|
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2501
2526
|
}
|
|
2502
2527
|
|
|
2528
|
+
function sanitizeInlinePrivacyValue(value: any): any {
|
|
2529
|
+
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2503
2532
|
function sanitizeMaterializedTraceValue(value: any): any {
|
|
2504
2533
|
return sanitizeTraceValue(value, 0, new WeakMap(), { preserveLongStrings: true, disableTruncation: true });
|
|
2505
2534
|
}
|
|
@@ -4215,6 +4244,74 @@ function collectBatchTraceValueEntries(batch: TraceEventRecord[], batchIndex: nu
|
|
|
4215
4244
|
return collected;
|
|
4216
4245
|
}
|
|
4217
4246
|
|
|
4247
|
+
function serializedByteLength(value: any): number {
|
|
4248
|
+
try {
|
|
4249
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
4250
|
+
} catch {
|
|
4251
|
+
return Number.MAX_SAFE_INTEGER;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
function estimateTraceBatchSerializedBytes(
|
|
4256
|
+
batch: TraceEventRecord[],
|
|
4257
|
+
requestRid: string,
|
|
4258
|
+
actionId: string | null | undefined,
|
|
4259
|
+
batchIndex: number,
|
|
4260
|
+
): number {
|
|
4261
|
+
const traceValues = collectBatchTraceValueEntries(batch, batchIndex);
|
|
4262
|
+
return serializedByteLength({
|
|
4263
|
+
actionId: actionId ?? null,
|
|
4264
|
+
trace: batch,
|
|
4265
|
+
traceValues: traceValues.length ? traceValues : undefined,
|
|
4266
|
+
traceBatch: {
|
|
4267
|
+
rid: requestRid,
|
|
4268
|
+
index: batchIndex,
|
|
4269
|
+
total: 0,
|
|
4270
|
+
},
|
|
4271
|
+
t: 0,
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function chunkTraceEventsForTransport(
|
|
4276
|
+
events: TraceEventRecord[],
|
|
4277
|
+
requestRid: string,
|
|
4278
|
+
actionId: string | null | undefined,
|
|
4279
|
+
): TraceEventRecord[][] {
|
|
4280
|
+
if (!Array.isArray(events) || events.length === 0) return [];
|
|
4281
|
+
|
|
4282
|
+
const countBatches = chunkArray(events, TRACE_BATCH_SIZE);
|
|
4283
|
+
const sizedBatches: TraceEventRecord[][] = [];
|
|
4284
|
+
let current: TraceEventRecord[] = [];
|
|
4285
|
+
|
|
4286
|
+
for (const event of countBatches.flat()) {
|
|
4287
|
+
const candidate = current.concat(event);
|
|
4288
|
+
const batchIndex = sizedBatches.length;
|
|
4289
|
+
const estimatedBytes = estimateTraceBatchSerializedBytes(
|
|
4290
|
+
candidate,
|
|
4291
|
+
requestRid,
|
|
4292
|
+
actionId,
|
|
4293
|
+
batchIndex,
|
|
4294
|
+
);
|
|
4295
|
+
|
|
4296
|
+
if (
|
|
4297
|
+
current.length > 0 &&
|
|
4298
|
+
estimatedBytes > TRACE_BATCH_MAX_SERIALIZED_BYTES
|
|
4299
|
+
) {
|
|
4300
|
+
sizedBatches.push(current);
|
|
4301
|
+
current = [event];
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
current = candidate;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
if (current.length > 0) {
|
|
4309
|
+
sizedBatches.push(current);
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
return sizedBatches;
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4218
4315
|
function createCapturedValueEntry(
|
|
4219
4316
|
params: {
|
|
4220
4317
|
target:
|
|
@@ -4601,11 +4698,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4601
4698
|
return;
|
|
4602
4699
|
}
|
|
4603
4700
|
}
|
|
4604
|
-
|
|
4605
|
-
|
|
4701
|
+
flushed = true;
|
|
4702
|
+
clearTimers();
|
|
4606
4703
|
try { unsubscribe && unsubscribe(); } catch {}
|
|
4607
4704
|
if (flushPayload) {
|
|
4608
|
-
const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload
|
|
4705
|
+
const scheduleFlushPayload = () => scheduleSdkBackgroundWork(flushPayload!, { priority: 'high' });
|
|
4609
4706
|
if (sessionDrainWait) {
|
|
4610
4707
|
sessionDrainWait.then(scheduleFlushPayload).catch(scheduleFlushPayload);
|
|
4611
4708
|
} else {
|
|
@@ -4953,7 +5050,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4953
5050
|
const orderedEvents = TRACE_ORDER_MODE === 'tree'
|
|
4954
5051
|
? reorderTraceEvents(baseEvents)
|
|
4955
5052
|
: sortTraceEventsChronologically(baseEvents);
|
|
4956
|
-
const traceBatches =
|
|
5053
|
+
const traceBatches = chunkTraceEventsForTransport(
|
|
5054
|
+
orderedEvents,
|
|
5055
|
+
rid,
|
|
5056
|
+
aid,
|
|
5057
|
+
);
|
|
4957
5058
|
|
|
4958
5059
|
if (traceBatches.length) {
|
|
4959
5060
|
for (let i = 0; i < traceBatches.length; i++) {
|
|
@@ -6006,7 +6107,7 @@ async function materializeKafkaRequestBody(
|
|
|
6006
6107
|
): Promise<InlinePrivacyMaterializationResult> {
|
|
6007
6108
|
return materializeInlinePrivacyValueAsync(
|
|
6008
6109
|
'request.body',
|
|
6009
|
-
|
|
6110
|
+
sanitizeInlinePrivacyValue(body),
|
|
6010
6111
|
cfg,
|
|
6011
6112
|
maskReq,
|
|
6012
6113
|
null,
|
|
@@ -6253,7 +6354,11 @@ function buildKafkaTraceEntries(
|
|
|
6253
6354
|
TRACE_ORDER_MODE === 'tree'
|
|
6254
6355
|
? reorderTraceEvents(baseEvents)
|
|
6255
6356
|
: sortTraceEventsChronologically(baseEvents);
|
|
6256
|
-
const batches =
|
|
6357
|
+
const batches = chunkTraceEventsForTransport(
|
|
6358
|
+
orderedEvents,
|
|
6359
|
+
requestRid,
|
|
6360
|
+
actionId,
|
|
6361
|
+
);
|
|
6257
6362
|
return batches.map((batch, index) => ({
|
|
6258
6363
|
actionId: actionId ?? null,
|
|
6259
6364
|
trace: batch,
|
|
@@ -6300,7 +6405,7 @@ function scheduleKafkaTraceFlush(
|
|
|
6300
6405
|
post(cfg, sid, { entries });
|
|
6301
6406
|
}
|
|
6302
6407
|
});
|
|
6303
|
-
});
|
|
6408
|
+
}, { priority: 'high' });
|
|
6304
6409
|
}
|
|
6305
6410
|
|
|
6306
6411
|
function patchKafkaProducerInstance(producer: any, cfg: KafkaJsPatchConfig) {
|
|
@@ -7605,6 +7710,31 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
7605
7710
|
/** @internal Test hooks for runtime privacy policy wiring. */
|
|
7606
7711
|
export const __reproTestHooks = {
|
|
7607
7712
|
shareRuntimePrivacyStateForTest: shareRuntimePrivacyState,
|
|
7713
|
+
setFullValueCaptureEnabledForTest(enabled: boolean): void {
|
|
7714
|
+
__FULL_VALUE_CAPTURE_ENABLED = enabled === true;
|
|
7715
|
+
},
|
|
7716
|
+
scheduleSdkBackgroundWorkForTest(
|
|
7717
|
+
work: () => Promise<void> | void,
|
|
7718
|
+
options?: { priority?: 'high' | 'normal' },
|
|
7719
|
+
): void {
|
|
7720
|
+
scheduleSdkBackgroundWork(work, options);
|
|
7721
|
+
},
|
|
7722
|
+
async drainSdkBackgroundQueueForTest(): Promise<void> {
|
|
7723
|
+
await drainSdkBackgroundQueue();
|
|
7724
|
+
},
|
|
7725
|
+
resetSdkBackgroundQueuesForTest(): void {
|
|
7726
|
+
sdkBackgroundHighQueue.length = 0;
|
|
7727
|
+
sdkBackgroundQueue.length = 0;
|
|
7728
|
+
if (sdkBackgroundTimer) {
|
|
7729
|
+
try { clearTimeout(sdkBackgroundTimer); } catch {}
|
|
7730
|
+
sdkBackgroundTimer = null;
|
|
7731
|
+
}
|
|
7732
|
+
sdkBackgroundDraining = false;
|
|
7733
|
+
activeClientRequestCount = 0;
|
|
7734
|
+
oldestActiveClientRequestAt = null;
|
|
7735
|
+
lastClientActivityAt = 0;
|
|
7736
|
+
sdkBackgroundQuietUntil = 0;
|
|
7737
|
+
},
|
|
7608
7738
|
getRuntimePrivacyPolicyForTest(cfg: ReproMiddlewareConfig): NormalizedRuntimePrivacyPolicy | null {
|
|
7609
7739
|
return getRuntimePrivacyState(cfg).policy;
|
|
7610
7740
|
},
|
|
@@ -7618,4 +7748,8 @@ export const __reproTestHooks = {
|
|
|
7618
7748
|
materializeInlinePrivacyValueAsyncForTest: materializeInlinePrivacyValueAsync,
|
|
7619
7749
|
patchKafkaProducerInstanceForTest: patchKafkaProducerInstance,
|
|
7620
7750
|
wrapKafkaEachMessageHandlerForTest: wrapKafkaEachMessageHandler,
|
|
7751
|
+
chunkTraceEventsForTransportForTest: chunkTraceEventsForTransport,
|
|
7752
|
+
estimateTraceBatchSerializedBytesForTest: estimateTraceBatchSerializedBytes,
|
|
7753
|
+
sanitizeTraceValueForPrivacyForTest: sanitizeTraceValueForPrivacy,
|
|
7754
|
+
sanitizeMaterializedTraceValueForTest: sanitizeMaterializedTraceValue,
|
|
7621
7755
|
};
|
|
@@ -567,6 +567,7 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
|
|
|
567
567
|
};
|
|
568
568
|
|
|
569
569
|
const sink = [];
|
|
570
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(true);
|
|
570
571
|
await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
|
|
571
572
|
{
|
|
572
573
|
type: 'enter',
|
|
@@ -579,6 +580,7 @@ async function testLargeStringTraceValuesAreCapturedInlineWithoutTruncation() {
|
|
|
579
580
|
cfg,
|
|
580
581
|
maskReq,
|
|
581
582
|
);
|
|
583
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(false);
|
|
582
584
|
|
|
583
585
|
assert.strictEqual(sink.length, 1);
|
|
584
586
|
assert.strictEqual(sink[0].fn, 'parseGatewayPayload');
|
|
@@ -622,6 +624,7 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
|
|
|
622
624
|
};
|
|
623
625
|
|
|
624
626
|
const sink = [];
|
|
627
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(true);
|
|
625
628
|
await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
|
|
626
629
|
{
|
|
627
630
|
type: 'enter',
|
|
@@ -634,6 +637,7 @@ async function testLargeStructuredTraceValuesAreCapturedInlineWithoutTruncation(
|
|
|
634
637
|
cfg,
|
|
635
638
|
maskReq,
|
|
636
639
|
);
|
|
640
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(false);
|
|
637
641
|
|
|
638
642
|
assert.strictEqual(sink.length, 1);
|
|
639
643
|
assert.strictEqual(sink[0].fn, 'recordConsumedEvent');
|
|
@@ -677,6 +681,7 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
|
|
|
677
681
|
};
|
|
678
682
|
|
|
679
683
|
const sink = [];
|
|
684
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(true);
|
|
680
685
|
await __reproTestHooks.recordKafkaTraceEventAsyncForTest(
|
|
681
686
|
{
|
|
682
687
|
type: 'exit',
|
|
@@ -689,6 +694,7 @@ async function testLargeTraceReturnValueIsCapturedInlineWithoutTruncation() {
|
|
|
689
694
|
cfg,
|
|
690
695
|
maskReq,
|
|
691
696
|
);
|
|
697
|
+
__reproTestHooks.setFullValueCaptureEnabledForTest(false);
|
|
692
698
|
|
|
693
699
|
assert.strictEqual(sink.length, 1);
|
|
694
700
|
assert.strictEqual(sink[0].fn, 'toDrillResponse');
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
|
|
3
|
+
const sdk = require('../dist/index.js');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const hooks = sdk.__reproTestHooks;
|
|
7
|
+
hooks.resetSdkBackgroundQueuesForTest();
|
|
8
|
+
|
|
9
|
+
const order = [];
|
|
10
|
+
|
|
11
|
+
hooks.scheduleSdkBackgroundWorkForTest(() => {
|
|
12
|
+
order.push('normal-1');
|
|
13
|
+
});
|
|
14
|
+
hooks.scheduleSdkBackgroundWorkForTest(() => {
|
|
15
|
+
order.push('normal-2');
|
|
16
|
+
});
|
|
17
|
+
hooks.scheduleSdkBackgroundWorkForTest(() => {
|
|
18
|
+
order.push('high-1');
|
|
19
|
+
}, { priority: 'high' });
|
|
20
|
+
hooks.scheduleSdkBackgroundWorkForTest(() => {
|
|
21
|
+
order.push('high-2');
|
|
22
|
+
}, { priority: 'high' });
|
|
23
|
+
|
|
24
|
+
await hooks.drainSdkBackgroundQueueForTest();
|
|
25
|
+
|
|
26
|
+
assert.deepStrictEqual(order, ['high-1', 'high-2', 'normal-1', 'normal-2']);
|
|
27
|
+
|
|
28
|
+
hooks.resetSdkBackgroundQueuesForTest();
|
|
29
|
+
console.log('sdk background priority OK');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error(error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { __reproTestHooks } = require('../dist');
|
|
3
|
+
|
|
4
|
+
function makeEvent(index, payload) {
|
|
5
|
+
return {
|
|
6
|
+
t: index,
|
|
7
|
+
type: 'enter',
|
|
8
|
+
fn: `fn${index}`,
|
|
9
|
+
file: '/app/src/controllers/subjects/index.ts',
|
|
10
|
+
line: index + 1,
|
|
11
|
+
depth: 1,
|
|
12
|
+
spanId: index + 1,
|
|
13
|
+
parentSpanId: null,
|
|
14
|
+
args: [payload],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function main() {
|
|
19
|
+
const payload = {
|
|
20
|
+
__kind: 'http-request',
|
|
21
|
+
method: 'POST',
|
|
22
|
+
url: '/api/v1/subject_visits/697001aaac70cc3d60f21273/subjects/createNewSubject?tenantId=tgtherapeutics',
|
|
23
|
+
headers: {
|
|
24
|
+
x_http_user: 'x'.repeat(12000),
|
|
25
|
+
authorization: '[dropped]',
|
|
26
|
+
},
|
|
27
|
+
body: {
|
|
28
|
+
value: 'y'.repeat(12000),
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const events = Array.from({ length: 120 }, (_, index) => makeEvent(index, payload));
|
|
33
|
+
const batches = __reproTestHooks.chunkTraceEventsForTransportForTest(
|
|
34
|
+
events,
|
|
35
|
+
'RID_test_trace_batch_size',
|
|
36
|
+
'A_test_trace_batch_size',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
assert(batches.length > 1, `expected more than one batch, got ${batches.length}`);
|
|
40
|
+
|
|
41
|
+
batches.forEach((batch, index) => {
|
|
42
|
+
const size = __reproTestHooks.estimateTraceBatchSerializedBytesForTest(
|
|
43
|
+
batch,
|
|
44
|
+
'RID_test_trace_batch_size',
|
|
45
|
+
'A_test_trace_batch_size',
|
|
46
|
+
index,
|
|
47
|
+
);
|
|
48
|
+
assert(
|
|
49
|
+
size <= 512 * 1024,
|
|
50
|
+
`batch ${index} too large: ${size}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.log('trace batch size chunking OK');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main();
|