@reproapp/node-sdk 0.0.20 → 0.0.22
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.js +94 -52
- package/package.json +2 -2
- package/src/index.ts +124 -79
- package/test/circular-capture.test.js +1 -0
- package/test/client-performance-regression.test.js +189 -0
- package/test/disable-subtree.test.js +1 -0
- package/test/express-trace-http-args.test.js +2 -0
- package/test/kafka-runtime-privacy-policy.test.js +10 -7
- package/test/request-capture-background.test.js +131 -0
- package/test/request-flush-timing.test.js +1 -0
package/dist/index.js
CHANGED
|
@@ -843,7 +843,7 @@ const SDK_BACKGROUND_IDLE_DELAY_MS = (() => {
|
|
|
843
843
|
const env = Number(process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS);
|
|
844
844
|
if (Number.isFinite(env) && env >= 0)
|
|
845
845
|
return env;
|
|
846
|
-
return
|
|
846
|
+
return 5000;
|
|
847
847
|
})();
|
|
848
848
|
const SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = (() => {
|
|
849
849
|
const env = Number(process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS);
|
|
@@ -852,10 +852,17 @@ const SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = (() => {
|
|
|
852
852
|
return 1000;
|
|
853
853
|
})();
|
|
854
854
|
const SDK_BACKGROUND_MAX_DEFER_MS = (() => {
|
|
855
|
-
const
|
|
855
|
+
const raw = String(process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS ?? '').trim().toLowerCase();
|
|
856
|
+
if (!raw) {
|
|
857
|
+
return Number.POSITIVE_INFINITY;
|
|
858
|
+
}
|
|
859
|
+
if (raw === 'off' || raw === 'never' || raw === 'infinite' || raw === 'infinity' || raw === '-1') {
|
|
860
|
+
return Number.POSITIVE_INFINITY;
|
|
861
|
+
}
|
|
862
|
+
const env = Number(raw);
|
|
856
863
|
if (Number.isFinite(env) && env >= 0)
|
|
857
864
|
return Math.trunc(env);
|
|
858
|
-
return
|
|
865
|
+
return Number.POSITIVE_INFINITY;
|
|
859
866
|
})();
|
|
860
867
|
const SDK_BACKGROUND_WORK_TIMEOUT_MS = (() => {
|
|
861
868
|
const env = Number(process.env.REPRO_SDK_BACKGROUND_WORK_TIMEOUT_MS);
|
|
@@ -915,7 +922,9 @@ function noteClientActivity(now = Date.now()) {
|
|
|
915
922
|
}
|
|
916
923
|
function getSdkBackgroundDelayMs(now = Date.now()) {
|
|
917
924
|
if (hasActiveClientRequests()) {
|
|
918
|
-
if (oldestActiveClientRequestAt !== null &&
|
|
925
|
+
if (oldestActiveClientRequestAt !== null &&
|
|
926
|
+
Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS) &&
|
|
927
|
+
SDK_BACKGROUND_MAX_DEFER_MS >= 0) {
|
|
919
928
|
const activeForMs = now - oldestActiveClientRequestAt;
|
|
920
929
|
if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS)
|
|
921
930
|
return 0;
|
|
@@ -2259,54 +2268,69 @@ async function materializePendingTraceEvents(events, params) {
|
|
|
2259
2268
|
parentSpanId: evt.parentSpanId ?? null,
|
|
2260
2269
|
};
|
|
2261
2270
|
if (!omitJsonBuiltinValues && pending.argsRaw !== undefined) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
const argsValueCapture = await maybeCaptureTraceValueAsync({
|
|
2265
|
-
target: 'trace.args',
|
|
2266
|
-
rawValue: pending.argsRaw,
|
|
2267
|
-
previewValue: evt.args,
|
|
2268
|
-
capture: captureContext,
|
|
2269
|
-
metadata: traceValueMetadata,
|
|
2270
|
-
});
|
|
2271
|
-
if (argsValueCapture?.entry) {
|
|
2272
|
-
attachTraceValueEntry(evt, argsValueCapture.entry);
|
|
2271
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.argsRaw)) {
|
|
2272
|
+
evt.args = makeOmittedValue('inline-size-limit');
|
|
2273
2273
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2274
|
+
else {
|
|
2275
|
+
const argsMaterialized = await materializeTracePrivacyValueAsync('trace.args', sanitizeTraceArgsForPrivacy(pending.argsRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
|
|
2276
|
+
evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
|
|
2277
|
+
const argsValueCapture = await maybeCaptureTraceValueAsync({
|
|
2278
|
+
target: 'trace.args',
|
|
2279
|
+
rawValue: pending.argsRaw,
|
|
2280
|
+
previewValue: evt.args,
|
|
2281
|
+
capture: captureContext,
|
|
2282
|
+
metadata: traceValueMetadata,
|
|
2283
|
+
});
|
|
2284
|
+
if (argsValueCapture?.entry) {
|
|
2285
|
+
attachTraceValueEntry(evt, argsValueCapture.entry);
|
|
2286
|
+
}
|
|
2287
|
+
if (argsValueCapture?.captureRef) {
|
|
2288
|
+
evt.argsValueCapture = argsValueCapture.captureRef;
|
|
2289
|
+
}
|
|
2276
2290
|
}
|
|
2277
2291
|
}
|
|
2278
2292
|
if (!omitJsonBuiltinValues && pending.returnValueRaw !== undefined) {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
const returnValueCapture = await maybeCaptureTraceValueAsync({
|
|
2282
|
-
target: 'trace.returnValue',
|
|
2283
|
-
rawValue: pending.returnValueRaw,
|
|
2284
|
-
previewValue: evt.returnValue,
|
|
2285
|
-
capture: captureContext,
|
|
2286
|
-
metadata: traceValueMetadata,
|
|
2287
|
-
});
|
|
2288
|
-
if (returnValueCapture?.entry) {
|
|
2289
|
-
attachTraceValueEntry(evt, returnValueCapture.entry);
|
|
2293
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.returnValueRaw)) {
|
|
2294
|
+
evt.returnValue = makeOmittedValue('inline-size-limit');
|
|
2290
2295
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2296
|
+
else {
|
|
2297
|
+
const returnValueMaterialized = await materializeTracePrivacyValueAsync('trace.returnValue', sanitizeTraceValueForPrivacy(pending.returnValueRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
|
|
2298
|
+
evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
|
|
2299
|
+
const returnValueCapture = await maybeCaptureTraceValueAsync({
|
|
2300
|
+
target: 'trace.returnValue',
|
|
2301
|
+
rawValue: pending.returnValueRaw,
|
|
2302
|
+
previewValue: evt.returnValue,
|
|
2303
|
+
capture: captureContext,
|
|
2304
|
+
metadata: traceValueMetadata,
|
|
2305
|
+
});
|
|
2306
|
+
if (returnValueCapture?.entry) {
|
|
2307
|
+
attachTraceValueEntry(evt, returnValueCapture.entry);
|
|
2308
|
+
}
|
|
2309
|
+
if (returnValueCapture?.captureRef) {
|
|
2310
|
+
evt.returnValueCapture = returnValueCapture.captureRef;
|
|
2311
|
+
}
|
|
2293
2312
|
}
|
|
2294
2313
|
}
|
|
2295
2314
|
if (pending.errorRaw !== undefined) {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
const errorValueCapture = await maybeCaptureTraceValueAsync({
|
|
2299
|
-
target: 'trace.error',
|
|
2300
|
-
rawValue: pending.errorRaw,
|
|
2301
|
-
previewValue: evt.error,
|
|
2302
|
-
capture: captureContext,
|
|
2303
|
-
metadata: traceValueMetadata,
|
|
2304
|
-
});
|
|
2305
|
-
if (errorValueCapture?.entry) {
|
|
2306
|
-
attachTraceValueEntry(evt, errorValueCapture.entry);
|
|
2315
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.errorRaw)) {
|
|
2316
|
+
evt.error = makeOmittedValue('inline-size-limit');
|
|
2307
2317
|
}
|
|
2308
|
-
|
|
2309
|
-
|
|
2318
|
+
else {
|
|
2319
|
+
const errorMaterialized = await materializeTracePrivacyValueAsync('trace.error', sanitizeTraceValueForPrivacy(pending.errorRaw), params.cfg, params.maskReq, candidate, params.masking, activePrivacy);
|
|
2320
|
+
evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
|
|
2321
|
+
const errorValueCapture = await maybeCaptureTraceValueAsync({
|
|
2322
|
+
target: 'trace.error',
|
|
2323
|
+
rawValue: pending.errorRaw,
|
|
2324
|
+
previewValue: evt.error,
|
|
2325
|
+
capture: captureContext,
|
|
2326
|
+
metadata: traceValueMetadata,
|
|
2327
|
+
});
|
|
2328
|
+
if (errorValueCapture?.entry) {
|
|
2329
|
+
attachTraceValueEntry(evt, errorValueCapture.entry);
|
|
2330
|
+
}
|
|
2331
|
+
if (errorValueCapture?.captureRef) {
|
|
2332
|
+
evt.errorValueCapture = errorValueCapture.captureRef;
|
|
2333
|
+
}
|
|
2310
2334
|
}
|
|
2311
2335
|
}
|
|
2312
2336
|
}
|
|
@@ -2808,7 +2832,22 @@ function traceInlineValueExceedsLimit(value) {
|
|
|
2808
2832
|
return boundedSerializedTraceValueLength(value, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS).exceeded;
|
|
2809
2833
|
}
|
|
2810
2834
|
function limitInlinePrivacyValue(target, value) {
|
|
2811
|
-
|
|
2835
|
+
if (value === undefined) {
|
|
2836
|
+
return { value };
|
|
2837
|
+
}
|
|
2838
|
+
const serialized = boundedSerializedTraceValueLength(value, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS);
|
|
2839
|
+
if (serialized.exceeded) {
|
|
2840
|
+
return {
|
|
2841
|
+
value: undefined,
|
|
2842
|
+
skipped: {
|
|
2843
|
+
status: 'skipped',
|
|
2844
|
+
reason: 'inline-size-limit',
|
|
2845
|
+
target,
|
|
2846
|
+
serializedLength: Math.max(serialized.length, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS + 1),
|
|
2847
|
+
limit: TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS,
|
|
2848
|
+
},
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2812
2851
|
return { value };
|
|
2813
2852
|
}
|
|
2814
2853
|
function limitRawInlinePrivacyValue(target, value) {
|
|
@@ -4175,13 +4214,16 @@ function reproMiddleware(cfg) {
|
|
|
4175
4214
|
privacy: activePrivacy,
|
|
4176
4215
|
},
|
|
4177
4216
|
}, requestValueEntries);
|
|
4217
|
+
const responseDurationMs = finishedAt !== null && Number.isFinite(finishedAt)
|
|
4218
|
+
? Math.max(0, finishedAt - t0)
|
|
4219
|
+
: Date.now() - t0;
|
|
4178
4220
|
const requestPayload = {
|
|
4179
4221
|
rid,
|
|
4180
4222
|
method: req.method,
|
|
4181
4223
|
url,
|
|
4182
4224
|
path,
|
|
4183
4225
|
status: res.statusCode,
|
|
4184
|
-
durMs:
|
|
4226
|
+
durMs: responseDurationMs,
|
|
4185
4227
|
headers: maskedHeaders,
|
|
4186
4228
|
key,
|
|
4187
4229
|
respBody: responseBody,
|
|
@@ -4217,6 +4259,12 @@ function reproMiddleware(cfg) {
|
|
|
4217
4259
|
return;
|
|
4218
4260
|
requestCaptureScheduled = true;
|
|
4219
4261
|
try {
|
|
4262
|
+
if (capturedBody === undefined && chunks.length) {
|
|
4263
|
+
const buf = Buffer.isBuffer(chunks[0])
|
|
4264
|
+
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
4265
|
+
: Buffer.from(chunks.map(String).join(''));
|
|
4266
|
+
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
4267
|
+
}
|
|
4220
4268
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
4221
4269
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
|
|
4222
4270
|
post(cfg, sid, {
|
|
@@ -4333,13 +4381,7 @@ function reproMiddleware(cfg) {
|
|
|
4333
4381
|
finishedAt = Date.now();
|
|
4334
4382
|
finishedAtAligned = alignTimestamp(finishedAt);
|
|
4335
4383
|
}
|
|
4336
|
-
|
|
4337
|
-
const buf = Buffer.isBuffer(chunks[0])
|
|
4338
|
-
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
4339
|
-
: Buffer.from(chunks.map(String).join(''));
|
|
4340
|
-
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
4341
|
-
}
|
|
4342
|
-
void emitRequestCaptureAsync();
|
|
4384
|
+
scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
|
|
4343
4385
|
if (!flushPayload) {
|
|
4344
4386
|
flushPayload = async () => {
|
|
4345
4387
|
let traceBatches = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
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/request-manifest-priority.test.js && node test/runtime-no-double-invoke.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 test/ingest-queue-retention.test.js && node test/ingest-timeout.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/wrap-plugin-super-call.test.js && node test/function-arg-identity.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/request-capture-background.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.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 test/ingest-queue-retention.test.js && node test/ingest-timeout.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/wrap-plugin-super-call.test.js && node test/function-arg-identity.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
|
"test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1239,7 +1239,7 @@ function nextRequestId(base: number | string): string {
|
|
|
1239
1239
|
const SDK_BACKGROUND_IDLE_DELAY_MS = (() => {
|
|
1240
1240
|
const env = Number(process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS);
|
|
1241
1241
|
if (Number.isFinite(env) && env >= 0) return env;
|
|
1242
|
-
return
|
|
1242
|
+
return 5000;
|
|
1243
1243
|
})();
|
|
1244
1244
|
|
|
1245
1245
|
const SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = (() => {
|
|
@@ -1249,9 +1249,16 @@ const SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = (() => {
|
|
|
1249
1249
|
})();
|
|
1250
1250
|
|
|
1251
1251
|
const SDK_BACKGROUND_MAX_DEFER_MS = (() => {
|
|
1252
|
-
const
|
|
1252
|
+
const raw = String(process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS ?? '').trim().toLowerCase();
|
|
1253
|
+
if (!raw) {
|
|
1254
|
+
return Number.POSITIVE_INFINITY;
|
|
1255
|
+
}
|
|
1256
|
+
if (raw === 'off' || raw === 'never' || raw === 'infinite' || raw === 'infinity' || raw === '-1') {
|
|
1257
|
+
return Number.POSITIVE_INFINITY;
|
|
1258
|
+
}
|
|
1259
|
+
const env = Number(raw);
|
|
1253
1260
|
if (Number.isFinite(env) && env >= 0) return Math.trunc(env);
|
|
1254
|
-
return
|
|
1261
|
+
return Number.POSITIVE_INFINITY;
|
|
1255
1262
|
})();
|
|
1256
1263
|
|
|
1257
1264
|
const SDK_BACKGROUND_WORK_TIMEOUT_MS = (() => {
|
|
@@ -1319,7 +1326,11 @@ function noteClientActivity(now = Date.now()): void {
|
|
|
1319
1326
|
|
|
1320
1327
|
function getSdkBackgroundDelayMs(now = Date.now()): number {
|
|
1321
1328
|
if (hasActiveClientRequests()) {
|
|
1322
|
-
if (
|
|
1329
|
+
if (
|
|
1330
|
+
oldestActiveClientRequestAt !== null &&
|
|
1331
|
+
Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS) &&
|
|
1332
|
+
SDK_BACKGROUND_MAX_DEFER_MS >= 0
|
|
1333
|
+
) {
|
|
1323
1334
|
const activeForMs = now - oldestActiveClientRequestAt;
|
|
1324
1335
|
if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS) return 0;
|
|
1325
1336
|
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, Math.max(0, SDK_BACKGROUND_MAX_DEFER_MS - activeForMs));
|
|
@@ -2704,78 +2715,90 @@ async function materializePendingTraceEvents(
|
|
|
2704
2715
|
};
|
|
2705
2716
|
|
|
2706
2717
|
if (!omitJsonBuiltinValues && pending.argsRaw !== undefined) {
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2718
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.argsRaw)) {
|
|
2719
|
+
evt.args = makeOmittedValue('inline-size-limit');
|
|
2720
|
+
} else {
|
|
2721
|
+
const argsMaterialized = await materializeTracePrivacyValueAsync(
|
|
2722
|
+
'trace.args',
|
|
2723
|
+
sanitizeTraceArgsForPrivacy(pending.argsRaw),
|
|
2724
|
+
params.cfg,
|
|
2725
|
+
params.maskReq,
|
|
2726
|
+
candidate,
|
|
2727
|
+
params.masking,
|
|
2728
|
+
activePrivacy,
|
|
2729
|
+
);
|
|
2730
|
+
evt.args = limitMaterializedTraceInlineValue(argsMaterialized);
|
|
2731
|
+
const argsValueCapture = await maybeCaptureTraceValueAsync({
|
|
2732
|
+
target: 'trace.args',
|
|
2733
|
+
rawValue: pending.argsRaw,
|
|
2734
|
+
previewValue: evt.args,
|
|
2735
|
+
capture: captureContext,
|
|
2736
|
+
metadata: traceValueMetadata,
|
|
2737
|
+
});
|
|
2738
|
+
if (argsValueCapture?.entry) {
|
|
2739
|
+
attachTraceValueEntry(evt, argsValueCapture.entry);
|
|
2740
|
+
}
|
|
2741
|
+
if (argsValueCapture?.captureRef) {
|
|
2742
|
+
evt.argsValueCapture = argsValueCapture.captureRef;
|
|
2743
|
+
}
|
|
2729
2744
|
}
|
|
2730
2745
|
}
|
|
2731
2746
|
if (!omitJsonBuiltinValues && pending.returnValueRaw !== undefined) {
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2747
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.returnValueRaw)) {
|
|
2748
|
+
evt.returnValue = makeOmittedValue('inline-size-limit');
|
|
2749
|
+
} else {
|
|
2750
|
+
const returnValueMaterialized = await materializeTracePrivacyValueAsync(
|
|
2751
|
+
'trace.returnValue',
|
|
2752
|
+
sanitizeTraceValueForPrivacy(pending.returnValueRaw),
|
|
2753
|
+
params.cfg,
|
|
2754
|
+
params.maskReq,
|
|
2755
|
+
candidate,
|
|
2756
|
+
params.masking,
|
|
2757
|
+
activePrivacy,
|
|
2758
|
+
);
|
|
2759
|
+
evt.returnValue = limitMaterializedTraceInlineValue(returnValueMaterialized);
|
|
2760
|
+
const returnValueCapture = await maybeCaptureTraceValueAsync({
|
|
2761
|
+
target: 'trace.returnValue',
|
|
2762
|
+
rawValue: pending.returnValueRaw,
|
|
2763
|
+
previewValue: evt.returnValue,
|
|
2764
|
+
capture: captureContext,
|
|
2765
|
+
metadata: traceValueMetadata,
|
|
2766
|
+
});
|
|
2767
|
+
if (returnValueCapture?.entry) {
|
|
2768
|
+
attachTraceValueEntry(evt, returnValueCapture.entry);
|
|
2769
|
+
}
|
|
2770
|
+
if (returnValueCapture?.captureRef) {
|
|
2771
|
+
evt.returnValueCapture = returnValueCapture.captureRef;
|
|
2772
|
+
}
|
|
2754
2773
|
}
|
|
2755
2774
|
}
|
|
2756
2775
|
if (pending.errorRaw !== undefined) {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2776
|
+
if (!__FULL_VALUE_CAPTURE_ENABLED && traceInlineValueExceedsLimit(pending.errorRaw)) {
|
|
2777
|
+
evt.error = makeOmittedValue('inline-size-limit');
|
|
2778
|
+
} else {
|
|
2779
|
+
const errorMaterialized = await materializeTracePrivacyValueAsync(
|
|
2780
|
+
'trace.error',
|
|
2781
|
+
sanitizeTraceValueForPrivacy(pending.errorRaw),
|
|
2782
|
+
params.cfg,
|
|
2783
|
+
params.maskReq,
|
|
2784
|
+
candidate,
|
|
2785
|
+
params.masking,
|
|
2786
|
+
activePrivacy,
|
|
2787
|
+
);
|
|
2788
|
+
evt.error = limitMaterializedTraceInlineValue(errorMaterialized);
|
|
2789
|
+
const errorValueCapture = await maybeCaptureTraceValueAsync({
|
|
2790
|
+
target: 'trace.error',
|
|
2791
|
+
rawValue: pending.errorRaw,
|
|
2792
|
+
previewValue: evt.error,
|
|
2793
|
+
capture: captureContext,
|
|
2794
|
+
metadata: traceValueMetadata,
|
|
2795
|
+
});
|
|
2796
|
+
if (errorValueCapture?.entry) {
|
|
2797
|
+
attachTraceValueEntry(evt, errorValueCapture.entry);
|
|
2798
|
+
}
|
|
2799
|
+
if (errorValueCapture?.captureRef) {
|
|
2800
|
+
evt.errorValueCapture = errorValueCapture.captureRef;
|
|
2801
|
+
}
|
|
2779
2802
|
}
|
|
2780
2803
|
}
|
|
2781
2804
|
}
|
|
@@ -3405,7 +3428,24 @@ function limitInlinePrivacyValue(
|
|
|
3405
3428
|
target: RuntimePrivacySurface,
|
|
3406
3429
|
value: any,
|
|
3407
3430
|
): InlinePrivacyMaterializationResult {
|
|
3408
|
-
|
|
3431
|
+
if (value === undefined) {
|
|
3432
|
+
return { value };
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
const serialized = boundedSerializedTraceValueLength(value, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS);
|
|
3436
|
+
if (serialized.exceeded) {
|
|
3437
|
+
return {
|
|
3438
|
+
value: undefined,
|
|
3439
|
+
skipped: {
|
|
3440
|
+
status: 'skipped',
|
|
3441
|
+
reason: 'inline-size-limit',
|
|
3442
|
+
target,
|
|
3443
|
+
serializedLength: Math.max(serialized.length, TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS + 1),
|
|
3444
|
+
limit: TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS,
|
|
3445
|
+
},
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3409
3449
|
return { value };
|
|
3410
3450
|
}
|
|
3411
3451
|
|
|
@@ -5099,13 +5139,17 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5099
5139
|
},
|
|
5100
5140
|
}, requestValueEntries);
|
|
5101
5141
|
|
|
5142
|
+
const responseDurationMs =
|
|
5143
|
+
finishedAt !== null && Number.isFinite(finishedAt)
|
|
5144
|
+
? Math.max(0, finishedAt - t0)
|
|
5145
|
+
: Date.now() - t0;
|
|
5102
5146
|
const requestPayload: Record<string, any> = {
|
|
5103
5147
|
rid,
|
|
5104
5148
|
method: req.method,
|
|
5105
5149
|
url,
|
|
5106
5150
|
path,
|
|
5107
5151
|
status: res.statusCode,
|
|
5108
|
-
durMs:
|
|
5152
|
+
durMs: responseDurationMs,
|
|
5109
5153
|
headers: maskedHeaders,
|
|
5110
5154
|
key,
|
|
5111
5155
|
respBody: responseBody,
|
|
@@ -5134,6 +5178,13 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5134
5178
|
if (requestCaptureScheduled) return;
|
|
5135
5179
|
requestCaptureScheduled = true;
|
|
5136
5180
|
try {
|
|
5181
|
+
if (capturedBody === undefined && chunks.length) {
|
|
5182
|
+
const buf = Buffer.isBuffer(chunks[0])
|
|
5183
|
+
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
5184
|
+
: Buffer.from(chunks.map(String).join(''));
|
|
5185
|
+
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5137
5188
|
const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
|
|
5138
5189
|
const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
|
|
5139
5190
|
chosenEndpoint,
|
|
@@ -5259,14 +5310,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
5259
5310
|
finishedAt = Date.now();
|
|
5260
5311
|
finishedAtAligned = alignTimestamp(finishedAt);
|
|
5261
5312
|
}
|
|
5262
|
-
if (capturedBody === undefined && chunks.length) {
|
|
5263
|
-
const buf = Buffer.isBuffer(chunks[0])
|
|
5264
|
-
? Buffer.concat(chunks.map(c => (Buffer.isBuffer(c) ? c : Buffer.from(String(c)))))
|
|
5265
|
-
: Buffer.from(chunks.map(String).join(''));
|
|
5266
|
-
capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
|
|
5267
|
-
}
|
|
5268
5313
|
|
|
5269
|
-
|
|
5314
|
+
scheduleSdkBackgroundWork(emitRequestCaptureAsync, { priority: 'high' });
|
|
5270
5315
|
|
|
5271
5316
|
if (!flushPayload) {
|
|
5272
5317
|
flushPayload = async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
2
2
|
process.env.TRACE_IDLE_FLUSH_MS = '25';
|
|
3
3
|
process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
|
|
4
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
4
5
|
|
|
5
6
|
const assert = require('assert');
|
|
6
7
|
const { EventEmitter } = require('events');
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
delete process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS;
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.REPRO_SDK_TRACE_INLINE_VALUE_MAX_SERIALIZED_CHARS = '2048';
|
|
5
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
6
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
7
|
+
|
|
8
|
+
const assert = require('node:assert/strict');
|
|
9
|
+
const { EventEmitter } = require('node:events');
|
|
10
|
+
const { initReproTracing, reproMiddleware, __reproTestHooks } = require('../dist');
|
|
11
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
12
|
+
|
|
13
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
async function waitFor(predicate, timeoutMs = 2500) {
|
|
16
|
+
const deadline = Date.now() + timeoutMs;
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
if (predicate()) return;
|
|
19
|
+
await sleep(20);
|
|
20
|
+
}
|
|
21
|
+
assert(predicate(), 'timed out waiting for expected condition');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeReqRes(sessionId, actionId, path = '/captured') {
|
|
25
|
+
const req = new EventEmitter();
|
|
26
|
+
req.method = 'GET';
|
|
27
|
+
req.url = path;
|
|
28
|
+
req.headers = sessionId && actionId
|
|
29
|
+
? {
|
|
30
|
+
'x-bug-session-id': sessionId,
|
|
31
|
+
'x-bug-action-id': actionId,
|
|
32
|
+
'x-bug-request-start': String(Date.now()),
|
|
33
|
+
}
|
|
34
|
+
: {};
|
|
35
|
+
req.body = undefined;
|
|
36
|
+
req.params = {};
|
|
37
|
+
req.query = {};
|
|
38
|
+
|
|
39
|
+
const res = new EventEmitter();
|
|
40
|
+
res.statusCode = 200;
|
|
41
|
+
res.getHeader = () => 'application/json';
|
|
42
|
+
res.setHeader = () => {};
|
|
43
|
+
res.json = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
44
|
+
res.send = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
45
|
+
res.write = () => true;
|
|
46
|
+
res.end = () => { res.emit('finish'); return true; };
|
|
47
|
+
|
|
48
|
+
return { req, res };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function invokeMiddleware(middleware, req, res, handler) {
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
void middleware(req, res, async (err) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
reject(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await handler();
|
|
60
|
+
resolve();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
reject(error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function flattenEvents(posts) {
|
|
69
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function eventsOf(posts, eventType) {
|
|
73
|
+
return flattenEvents(posts).filter((event) => event?.event_type === eventType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function largePayload() {
|
|
77
|
+
return {
|
|
78
|
+
rows: Array.from({ length: 400 }, (_, index) => ({
|
|
79
|
+
index,
|
|
80
|
+
depot: `depot-${index}`,
|
|
81
|
+
description: `large response row ${index} `.repeat(4),
|
|
82
|
+
})),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const capturedBodies = [];
|
|
88
|
+
const originalFetch = global.fetch;
|
|
89
|
+
global.fetch = async (_url, init = {}) => {
|
|
90
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
status: 202,
|
|
94
|
+
json: async () => ({ ok: true }),
|
|
95
|
+
text: async () => '{"ok":true}',
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const cfg = {
|
|
101
|
+
tenantId: 'TENANT_test',
|
|
102
|
+
appId: 'APP_test',
|
|
103
|
+
appSecret: 'secret',
|
|
104
|
+
captureHeaders: false,
|
|
105
|
+
privacy: { environment: 'dev' },
|
|
106
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
107
|
+
};
|
|
108
|
+
const middleware = reproMiddleware(cfg);
|
|
109
|
+
|
|
110
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
111
|
+
const held = makeReqRes(null, null, '/held-open');
|
|
112
|
+
await invokeMiddleware(middleware, held.req, held.res, async () => {
|
|
113
|
+
// Keep one app endpoint open. Background capture from other requests must not run.
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const tagged = makeReqRes('S_perf_regression', 'A_perf_regression');
|
|
117
|
+
await invokeMiddleware(middleware, tagged.req, tagged.res, async () => {
|
|
118
|
+
tagged.res.json({ ok: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await sleep(2200);
|
|
122
|
+
assert.equal(
|
|
123
|
+
eventsOf(capturedBodies, 'backend_request').length,
|
|
124
|
+
0,
|
|
125
|
+
`background request capture ran while a client endpoint was still active: ${JSON.stringify(capturedBodies)}`,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
held.res.emit('finish');
|
|
129
|
+
await waitFor(() => eventsOf(capturedBodies, 'backend_request').length === 1);
|
|
130
|
+
|
|
131
|
+
capturedBodies.length = 0;
|
|
132
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
133
|
+
initReproTracing({ instrument: false, logFunctionCalls: false });
|
|
134
|
+
|
|
135
|
+
function largeTraceReturn() {
|
|
136
|
+
return largePayload();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const large = makeReqRes('S_perf_large_payload', 'A_perf_large_payload', '/large-payload');
|
|
140
|
+
await invokeMiddleware(middleware, large.req, large.res, async () => {
|
|
141
|
+
const traced = global.__repro_call(
|
|
142
|
+
largeTraceReturn,
|
|
143
|
+
null,
|
|
144
|
+
[],
|
|
145
|
+
'app',
|
|
146
|
+
1,
|
|
147
|
+
'largeTraceReturn',
|
|
148
|
+
false,
|
|
149
|
+
);
|
|
150
|
+
large.res.json(traced);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await waitFor(() => eventsOf(capturedBodies, 'backend_request').length === 1);
|
|
154
|
+
await waitFor(() => eventsOf(capturedBodies, 'trace_batch').length >= 1);
|
|
155
|
+
|
|
156
|
+
const backendRequest = eventsOf(capturedBodies, 'backend_request')[0]?.payload?.request;
|
|
157
|
+
assert(backendRequest, JSON.stringify(capturedBodies));
|
|
158
|
+
assert.equal(backendRequest.respBody, undefined, JSON.stringify(backendRequest));
|
|
159
|
+
assert.equal(
|
|
160
|
+
backendRequest.respBodyMaterialization?.reason,
|
|
161
|
+
'inline-size-limit',
|
|
162
|
+
JSON.stringify(backendRequest),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const traceEvents = eventsOf(capturedBodies, 'trace_batch')
|
|
166
|
+
.flatMap((event) => Array.isArray(event?.payload?.trace) ? event.payload.trace : []);
|
|
167
|
+
const largeReturnExit = traceEvents.find((event) =>
|
|
168
|
+
event?.fn === 'largeTraceReturn' &&
|
|
169
|
+
event?.type === 'exit' &&
|
|
170
|
+
event?.returnValue,
|
|
171
|
+
);
|
|
172
|
+
assert(largeReturnExit, JSON.stringify(traceEvents));
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
largeReturnExit.returnValue,
|
|
175
|
+
{ __kind: 'omitted', reason: 'inline-size-limit' },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
console.log('client performance regression guards OK');
|
|
179
|
+
} finally {
|
|
180
|
+
await flushIngestQueue();
|
|
181
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
182
|
+
global.fetch = originalFetch;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((error) => {
|
|
187
|
+
console.error(error);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
3
3
|
process.env.TRACE_IDLE_FLUSH_MS = '25';
|
|
4
4
|
process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
|
|
5
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
5
6
|
|
|
6
7
|
const assert = require('assert');
|
|
7
8
|
const { EventEmitter } = require('events');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
2
|
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
3
4
|
|
|
4
5
|
const { EventEmitter } = require('events');
|
|
5
6
|
const { normalizeRuntimePrivacyPolicy } = require('../dist/privacy');
|
|
@@ -306,7 +307,7 @@ async function testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest() {
|
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
async function
|
|
310
|
+
async function testLargeHttpResponseBodyKeepsRequestMetadataWithoutInlineBody() {
|
|
310
311
|
const marker = 'OVERSIZED_HTTP_RESPONSE_MARKER';
|
|
311
312
|
const capturedBodies = [];
|
|
312
313
|
const originalFetch = global.fetch;
|
|
@@ -341,13 +342,15 @@ async function testLargeHttpResponseBodyIsCapturedWithoutDroppingRequest() {
|
|
|
341
342
|
await waitFor(() => capturedBodies.some((body) => {
|
|
342
343
|
const text = JSON.stringify(body);
|
|
343
344
|
return text.includes('"event_type":"backend_request"') &&
|
|
344
|
-
text.includes(
|
|
345
|
+
text.includes('"reason":"inline-size-limit"');
|
|
345
346
|
}));
|
|
346
347
|
const text = JSON.stringify(capturedBodies);
|
|
347
|
-
assert(text.includes(marker), text);
|
|
348
|
-
assert(
|
|
349
|
-
assert(
|
|
350
|
-
|
|
348
|
+
assert(!text.includes(marker), text);
|
|
349
|
+
assert(text.includes('respBodyMaterialization'), text);
|
|
350
|
+
assert(text.includes('"target":"response.body"'), text);
|
|
351
|
+
assert(!text.includes('__type":"json-string'), text);
|
|
352
|
+
assert(!text.includes('__truncatedKeys'), text);
|
|
353
|
+
assert(!text.includes('more chars'), text);
|
|
351
354
|
} finally {
|
|
352
355
|
await flushIngestQueue();
|
|
353
356
|
global.fetch = originalFetch;
|
|
@@ -743,7 +746,7 @@ async function main() {
|
|
|
743
746
|
await testKafkaConsumerTraceUsesConsumeTraceId();
|
|
744
747
|
await testKafkaProducerCapturesFullPublishedMessageValue();
|
|
745
748
|
await testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest();
|
|
746
|
-
await
|
|
749
|
+
await testLargeHttpResponseBodyKeepsRequestMetadataWithoutInlineBody();
|
|
747
750
|
await testJsonBuiltinTracePayloadsAreOmitted();
|
|
748
751
|
await testLargeStringTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
|
|
749
752
|
await testLargeStructuredTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '500';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
5
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const { EventEmitter } = require('events');
|
|
9
|
+
const { reproMiddleware } = require('../dist');
|
|
10
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
11
|
+
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
async function waitFor(predicate, timeoutMs = 1500) {
|
|
15
|
+
const deadline = Date.now() + timeoutMs;
|
|
16
|
+
while (Date.now() < deadline) {
|
|
17
|
+
if (predicate()) return;
|
|
18
|
+
await sleep(20);
|
|
19
|
+
}
|
|
20
|
+
assert(predicate(), 'timed out waiting for expected condition');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeReqRes(sessionId, actionId) {
|
|
24
|
+
const req = new EventEmitter();
|
|
25
|
+
req.method = 'GET';
|
|
26
|
+
req.url = sessionId ? '/captured' : '/held-open';
|
|
27
|
+
req.headers = sessionId && actionId
|
|
28
|
+
? {
|
|
29
|
+
'x-bug-session-id': sessionId,
|
|
30
|
+
'x-bug-action-id': actionId,
|
|
31
|
+
'x-bug-request-start': String(Date.now()),
|
|
32
|
+
}
|
|
33
|
+
: {};
|
|
34
|
+
req.body = undefined;
|
|
35
|
+
req.params = {};
|
|
36
|
+
req.query = {};
|
|
37
|
+
|
|
38
|
+
const res = new EventEmitter();
|
|
39
|
+
res.statusCode = 200;
|
|
40
|
+
res.getHeader = () => 'application/json';
|
|
41
|
+
res.setHeader = () => {};
|
|
42
|
+
res.json = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
43
|
+
res.send = function (body) { this.body = body; this.emit('finish'); return body; };
|
|
44
|
+
res.write = () => true;
|
|
45
|
+
res.end = () => { res.emit('finish'); return true; };
|
|
46
|
+
|
|
47
|
+
return { req, res };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function flattenEvents(posts) {
|
|
51
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function countEvents(posts, eventType) {
|
|
55
|
+
return flattenEvents(posts).filter((event) => event?.event_type === eventType).length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function invokeMiddleware(middleware, req, res, handler) {
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
void middleware(req, res, async (err) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
reject(err);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await handler();
|
|
67
|
+
resolve();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
reject(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
const capturedBodies = [];
|
|
77
|
+
const originalFetch = global.fetch;
|
|
78
|
+
global.fetch = async (_url, init = {}) => {
|
|
79
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
status: 202,
|
|
83
|
+
json: async () => ({ ok: true }),
|
|
84
|
+
text: async () => '{"ok":true}',
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const cfg = {
|
|
90
|
+
tenantId: 'TENANT_test',
|
|
91
|
+
appId: 'APP_test',
|
|
92
|
+
appSecret: 'secret',
|
|
93
|
+
captureHeaders: false,
|
|
94
|
+
privacy: { environment: 'dev' },
|
|
95
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
96
|
+
};
|
|
97
|
+
const middleware = reproMiddleware(cfg);
|
|
98
|
+
|
|
99
|
+
const held = makeReqRes(null, null);
|
|
100
|
+
await invokeMiddleware(middleware, held.req, held.res, async () => {
|
|
101
|
+
// Keep this request open to simulate another client endpoint still in flight.
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const tagged = makeReqRes('S_request_capture_background', 'A_request_capture_background');
|
|
105
|
+
await invokeMiddleware(middleware, tagged.req, tagged.res, async () => {
|
|
106
|
+
tagged.res.json({ ok: true, rows: Array.from({ length: 50 }, (_, i) => ({ i, value: `row-${i}` })) });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await sleep(100);
|
|
110
|
+
assert.equal(
|
|
111
|
+
countEvents(capturedBodies, 'backend_request'),
|
|
112
|
+
0,
|
|
113
|
+
`request capture should defer while another client request is active: ${JSON.stringify(capturedBodies)}`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
held.res.emit('finish');
|
|
117
|
+
await waitFor(() => countEvents(capturedBodies, 'backend_request') === 1);
|
|
118
|
+
await flushIngestQueue();
|
|
119
|
+
|
|
120
|
+
assert.equal(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
|
|
121
|
+
console.log('request capture background OK');
|
|
122
|
+
} finally {
|
|
123
|
+
global.fetch = originalFetch;
|
|
124
|
+
await flushIngestQueue();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch((error) => {
|
|
129
|
+
console.error(error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|