@reproapp/node-sdk 0.0.50 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +21 -25
- package/dist/ingest/client.d.ts +2 -2
- package/dist/ingest/client.js +6 -5
- package/package.json +2 -2
- package/src/index.ts +28 -25
- package/src/ingest/client.ts +9 -7
- package/test/background-continuous-traffic.testcontainers.test.js +100 -0
package/dist/index.js
CHANGED
|
@@ -1121,22 +1121,20 @@ function hasActiveClientRequests() {
|
|
|
1121
1121
|
function noteClientActivity(now = Date.now()) {
|
|
1122
1122
|
lastClientActivityAt = now;
|
|
1123
1123
|
}
|
|
1124
|
-
function getSdkBackgroundDelayMs(now = Date.now()) {
|
|
1124
|
+
function getSdkBackgroundDelayMs(now = Date.now(), ingestEnqueuedAt) {
|
|
1125
|
+
// Bound deferral by work age, not just by a continuously active request.
|
|
1126
|
+
// Short non-overlapping requests reset oldestActiveClientRequestAt forever.
|
|
1127
|
+
const oldestWorkAt = Math.min(sdkBackgroundHighQueue[0]?.enqueuedAt ?? Infinity, sdkBackgroundQueue[0]?.enqueuedAt ?? Infinity, ingestEnqueuedAt ?? Infinity, oldestActiveClientRequestAt ?? Infinity);
|
|
1128
|
+
const remainingDeferMs = Number.isFinite(oldestWorkAt) && Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS)
|
|
1129
|
+
? Math.max(0, oldestWorkAt + SDK_BACKGROUND_MAX_DEFER_MS - now)
|
|
1130
|
+
: Infinity;
|
|
1125
1131
|
if (hasActiveClientRequests()) {
|
|
1126
|
-
|
|
1127
|
-
Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS) &&
|
|
1128
|
-
SDK_BACKGROUND_MAX_DEFER_MS >= 0) {
|
|
1129
|
-
const activeForMs = now - oldestActiveClientRequestAt;
|
|
1130
|
-
if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS)
|
|
1131
|
-
return 0;
|
|
1132
|
-
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, Math.max(0, SDK_BACKGROUND_MAX_DEFER_MS - activeForMs));
|
|
1133
|
-
}
|
|
1134
|
-
return SDK_BACKGROUND_IDLE_DELAY_MS;
|
|
1132
|
+
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, remainingDeferMs);
|
|
1135
1133
|
}
|
|
1136
1134
|
const sinceLastActivity = now - lastClientActivityAt;
|
|
1137
1135
|
const idleDelayRemaining = Math.max(0, SDK_BACKGROUND_IDLE_DELAY_MS - sinceLastActivity);
|
|
1138
1136
|
const quietWindowRemaining = Math.max(0, sdkBackgroundQuietUntil - now);
|
|
1139
|
-
return Math.max(idleDelayRemaining, quietWindowRemaining);
|
|
1137
|
+
return Math.min(Math.max(idleDelayRemaining, quietWindowRemaining), remainingDeferMs);
|
|
1140
1138
|
}
|
|
1141
1139
|
function shouldDeferSdkBackgroundWork(now = Date.now()) {
|
|
1142
1140
|
return getSdkBackgroundDelayMs(now) > 0;
|
|
@@ -1372,13 +1370,8 @@ function beginClientRequest() {
|
|
|
1372
1370
|
oldestActiveClientRequestAt = now;
|
|
1373
1371
|
}
|
|
1374
1372
|
activeClientRequestCount += 1;
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
clearTimeout(sdkBackgroundTimer);
|
|
1378
|
-
}
|
|
1379
|
-
catch { }
|
|
1380
|
-
sdkBackgroundTimer = null;
|
|
1381
|
-
}
|
|
1373
|
+
// The scheduled drain rechecks activity; cancelling it here loses the
|
|
1374
|
+
// queued work's deadline under sustained traffic.
|
|
1382
1375
|
}
|
|
1383
1376
|
function endClientRequest() {
|
|
1384
1377
|
activeClientRequestCount = Math.max(0, activeClientRequestCount - 1);
|
|
@@ -1397,15 +1390,18 @@ function endClientRequest() {
|
|
|
1397
1390
|
}
|
|
1398
1391
|
}
|
|
1399
1392
|
function scheduleSdkBackgroundWork(work, options = {}) {
|
|
1393
|
+
const item = { work, enqueuedAt: Date.now() };
|
|
1400
1394
|
if (options.priority === 'high') {
|
|
1401
|
-
sdkBackgroundHighQueue.push(
|
|
1395
|
+
sdkBackgroundHighQueue.push(item);
|
|
1402
1396
|
}
|
|
1403
1397
|
else {
|
|
1404
|
-
sdkBackgroundQueue.push(
|
|
1398
|
+
sdkBackgroundQueue.push(item);
|
|
1405
1399
|
}
|
|
1406
1400
|
scheduleSdkBackgroundDrain();
|
|
1407
1401
|
}
|
|
1408
1402
|
function scheduleSdkBackgroundDrain(delayMs = 0) {
|
|
1403
|
+
if (!sdkBackgroundHighQueue.length && !sdkBackgroundQueue.length)
|
|
1404
|
+
return;
|
|
1409
1405
|
if (sdkBackgroundTimer)
|
|
1410
1406
|
return;
|
|
1411
1407
|
const nextDelay = Math.max(delayMs, getSdkBackgroundDelayMs());
|
|
@@ -1441,12 +1437,12 @@ async function drainSdkBackgroundQueue() {
|
|
|
1441
1437
|
scheduleSdkBackgroundDrain();
|
|
1442
1438
|
return;
|
|
1443
1439
|
}
|
|
1444
|
-
const
|
|
1445
|
-
if (!
|
|
1440
|
+
const item = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
|
|
1441
|
+
if (!item)
|
|
1446
1442
|
continue;
|
|
1447
1443
|
try {
|
|
1448
1444
|
await drainQueuedIngestBeforeBackgroundWork();
|
|
1449
|
-
await runSdkBackgroundWork(work);
|
|
1445
|
+
await runSdkBackgroundWork(item.work);
|
|
1450
1446
|
}
|
|
1451
1447
|
catch {
|
|
1452
1448
|
// never break user code
|
|
@@ -1464,8 +1460,8 @@ async function drainSdkBackgroundQueue() {
|
|
|
1464
1460
|
}
|
|
1465
1461
|
}
|
|
1466
1462
|
(0, client_1.configureIngestQueueDrain)({
|
|
1467
|
-
shouldDefer:
|
|
1468
|
-
deferMs: SDK_BACKGROUND_IDLE_DELAY_MS,
|
|
1463
|
+
shouldDefer: (enqueuedAt) => getSdkBackgroundDelayMs(Date.now(), enqueuedAt) > 0,
|
|
1464
|
+
deferMs: Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, SDK_BACKGROUND_MAX_DEFER_MS),
|
|
1469
1465
|
});
|
|
1470
1466
|
// Track in-flight requests per session so we can delay flushing until a session is drained.
|
|
1471
1467
|
const sessionInFlight = new Map();
|
package/dist/ingest/client.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type EnqueueIngestOptions = {
|
|
|
3
3
|
priority?: 'normal' | 'high' | 'critical';
|
|
4
4
|
};
|
|
5
5
|
export declare const configureIngestQueueDrain: (options: {
|
|
6
|
-
shouldDefer?: (() => boolean) | null;
|
|
7
|
-
deferMs?: number;
|
|
6
|
+
shouldDefer?: ((enqueuedAt?: number) => boolean) | null | undefined;
|
|
7
|
+
deferMs?: number | undefined;
|
|
8
8
|
}) => void;
|
|
9
9
|
export declare const kickIngestQueueDrain: () => void;
|
|
10
10
|
export declare const postIngestEntries: (config: IngestClientConfig, sessionId: string, entries: LegacyEntry[]) => Promise<void>;
|
package/dist/ingest/client.js
CHANGED
|
@@ -61,7 +61,7 @@ const kickIngestQueueDrain = () => {
|
|
|
61
61
|
scheduleCriticalDrain();
|
|
62
62
|
}
|
|
63
63
|
if (ingestQueue.length > 0 && !drainTimer) {
|
|
64
|
-
scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
|
|
64
|
+
scheduleDrain(shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt) ? deferDrainMs : 0);
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
exports.kickIngestQueueDrain = kickIngestQueueDrain;
|
|
@@ -233,12 +233,12 @@ const scheduleDrain = (delayMs = 0) => {
|
|
|
233
233
|
if (drainTimer) {
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
|
-
const nextDelay = shouldDeferDrain?.()
|
|
236
|
+
const nextDelay = shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)
|
|
237
237
|
? Math.max(delayMs, deferDrainMs)
|
|
238
238
|
: delayMs;
|
|
239
239
|
drainTimer = setTimeout(() => {
|
|
240
240
|
drainTimer = null;
|
|
241
|
-
if (shouldDeferDrain?.()) {
|
|
241
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
242
242
|
scheduleDrain(deferDrainMs);
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
@@ -302,14 +302,14 @@ const drainIngestQueue = async () => {
|
|
|
302
302
|
if (draining) {
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
|
-
if (shouldDeferDrain?.()) {
|
|
305
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
306
306
|
scheduleDrain(deferDrainMs);
|
|
307
307
|
return;
|
|
308
308
|
}
|
|
309
309
|
draining = true;
|
|
310
310
|
try {
|
|
311
311
|
while (ingestQueue.length > 0) {
|
|
312
|
-
if (shouldDeferDrain?.()) {
|
|
312
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
313
313
|
scheduleDrain(deferDrainMs);
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
@@ -367,6 +367,7 @@ const enqueueIngestEntries = (config, sessionId, entries, options = {}) => {
|
|
|
367
367
|
sessionId,
|
|
368
368
|
entries: entries.slice(),
|
|
369
369
|
attempts: 0,
|
|
370
|
+
enqueuedAt: Date.now(),
|
|
370
371
|
priority,
|
|
371
372
|
};
|
|
372
373
|
if (priority === 'critical') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
16
16
|
"prepublishOnly": "npm run build",
|
|
17
17
|
"test": "npm run build && node test/semantic-serialization.test.js && node test/semantic-serialization-trace.test.js && node test/content-addressed-trace-values.test.js && node test/trace-observation-snapshots.test.js && node test/session-storage-budget.test.js && node test/trace-source-exclude.test.js && 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/init-auto-http-server.test.js && node test/backend-live-mode.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/sdk-background-timeout-isolation.test.js && node test/runtime-service-registration.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-high-priority-fairness.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-batching.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-service-scope.test.js && node test/privacy-policy-index.test.js && node test/policy-capture-attestation.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
|
|
18
|
-
"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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
|
|
18
|
+
"test:containers": "npm run build && node test/background-continuous-traffic.testcontainers.test.js && 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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/content-addressed-mongodb.testcontainers.test.js && node test/policy-session-completion.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"express": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -1552,8 +1552,9 @@ let activeClientRequestCount = 0;
|
|
|
1552
1552
|
let oldestActiveClientRequestAt: number | null = null;
|
|
1553
1553
|
let lastClientActivityAt = 0;
|
|
1554
1554
|
let sdkBackgroundQuietUntil = 0;
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1555
|
+
type SdkBackgroundWork = { work: () => Promise<void> | void; enqueuedAt: number };
|
|
1556
|
+
const sdkBackgroundHighQueue: SdkBackgroundWork[] = [];
|
|
1557
|
+
const sdkBackgroundQueue: SdkBackgroundWork[] = [];
|
|
1557
1558
|
let sdkBackgroundTimer: NodeJS.Timeout | null = null;
|
|
1558
1559
|
let sdkBackgroundDraining = false;
|
|
1559
1560
|
let traceMaterializerWorker: any = null;
|
|
@@ -1575,23 +1576,25 @@ function noteClientActivity(now = Date.now()): void {
|
|
|
1575
1576
|
lastClientActivityAt = now;
|
|
1576
1577
|
}
|
|
1577
1578
|
|
|
1578
|
-
function getSdkBackgroundDelayMs(now = Date.now()): number {
|
|
1579
|
+
function getSdkBackgroundDelayMs(now = Date.now(), ingestEnqueuedAt?: number): number {
|
|
1580
|
+
// Bound deferral by work age, not just by a continuously active request.
|
|
1581
|
+
// Short non-overlapping requests reset oldestActiveClientRequestAt forever.
|
|
1582
|
+
const oldestWorkAt = Math.min(
|
|
1583
|
+
sdkBackgroundHighQueue[0]?.enqueuedAt ?? Infinity,
|
|
1584
|
+
sdkBackgroundQueue[0]?.enqueuedAt ?? Infinity,
|
|
1585
|
+
ingestEnqueuedAt ?? Infinity,
|
|
1586
|
+
oldestActiveClientRequestAt ?? Infinity,
|
|
1587
|
+
);
|
|
1588
|
+
const remainingDeferMs = Number.isFinite(oldestWorkAt) && Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS)
|
|
1589
|
+
? Math.max(0, oldestWorkAt + SDK_BACKGROUND_MAX_DEFER_MS - now)
|
|
1590
|
+
: Infinity;
|
|
1579
1591
|
if (hasActiveClientRequests()) {
|
|
1580
|
-
|
|
1581
|
-
oldestActiveClientRequestAt !== null &&
|
|
1582
|
-
Number.isFinite(SDK_BACKGROUND_MAX_DEFER_MS) &&
|
|
1583
|
-
SDK_BACKGROUND_MAX_DEFER_MS >= 0
|
|
1584
|
-
) {
|
|
1585
|
-
const activeForMs = now - oldestActiveClientRequestAt;
|
|
1586
|
-
if (activeForMs >= SDK_BACKGROUND_MAX_DEFER_MS) return 0;
|
|
1587
|
-
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, Math.max(0, SDK_BACKGROUND_MAX_DEFER_MS - activeForMs));
|
|
1588
|
-
}
|
|
1589
|
-
return SDK_BACKGROUND_IDLE_DELAY_MS;
|
|
1592
|
+
return Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, remainingDeferMs);
|
|
1590
1593
|
}
|
|
1591
1594
|
const sinceLastActivity = now - lastClientActivityAt;
|
|
1592
1595
|
const idleDelayRemaining = Math.max(0, SDK_BACKGROUND_IDLE_DELAY_MS - sinceLastActivity);
|
|
1593
1596
|
const quietWindowRemaining = Math.max(0, sdkBackgroundQuietUntil - now);
|
|
1594
|
-
return Math.max(idleDelayRemaining, quietWindowRemaining);
|
|
1597
|
+
return Math.min(Math.max(idleDelayRemaining, quietWindowRemaining), remainingDeferMs);
|
|
1595
1598
|
}
|
|
1596
1599
|
|
|
1597
1600
|
function shouldDeferSdkBackgroundWork(now = Date.now()): boolean {
|
|
@@ -1804,10 +1807,8 @@ function beginClientRequest(): void {
|
|
|
1804
1807
|
oldestActiveClientRequestAt = now;
|
|
1805
1808
|
}
|
|
1806
1809
|
activeClientRequestCount += 1;
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
sdkBackgroundTimer = null;
|
|
1810
|
-
}
|
|
1810
|
+
// The scheduled drain rechecks activity; cancelling it here loses the
|
|
1811
|
+
// queued work's deadline under sustained traffic.
|
|
1811
1812
|
}
|
|
1812
1813
|
|
|
1813
1814
|
function endClientRequest(): void {
|
|
@@ -1828,15 +1829,17 @@ function scheduleSdkBackgroundWork(
|
|
|
1828
1829
|
work: () => Promise<void> | void,
|
|
1829
1830
|
options: { priority?: 'high' | 'normal' } = {},
|
|
1830
1831
|
): void {
|
|
1832
|
+
const item = { work, enqueuedAt: Date.now() };
|
|
1831
1833
|
if (options.priority === 'high') {
|
|
1832
|
-
sdkBackgroundHighQueue.push(
|
|
1834
|
+
sdkBackgroundHighQueue.push(item);
|
|
1833
1835
|
} else {
|
|
1834
|
-
sdkBackgroundQueue.push(
|
|
1836
|
+
sdkBackgroundQueue.push(item);
|
|
1835
1837
|
}
|
|
1836
1838
|
scheduleSdkBackgroundDrain();
|
|
1837
1839
|
}
|
|
1838
1840
|
|
|
1839
1841
|
function scheduleSdkBackgroundDrain(delayMs = 0): void {
|
|
1842
|
+
if (!sdkBackgroundHighQueue.length && !sdkBackgroundQueue.length) return;
|
|
1840
1843
|
if (sdkBackgroundTimer) return;
|
|
1841
1844
|
const nextDelay = Math.max(delayMs, getSdkBackgroundDelayMs());
|
|
1842
1845
|
sdkBackgroundTimer = setTimeout(() => {
|
|
@@ -1871,11 +1874,11 @@ async function drainSdkBackgroundQueue(): Promise<void> {
|
|
|
1871
1874
|
scheduleSdkBackgroundDrain();
|
|
1872
1875
|
return;
|
|
1873
1876
|
}
|
|
1874
|
-
const
|
|
1875
|
-
if (!
|
|
1877
|
+
const item = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
|
|
1878
|
+
if (!item) continue;
|
|
1876
1879
|
try {
|
|
1877
1880
|
await drainQueuedIngestBeforeBackgroundWork();
|
|
1878
|
-
await runSdkBackgroundWork(work);
|
|
1881
|
+
await runSdkBackgroundWork(item.work);
|
|
1879
1882
|
} catch {
|
|
1880
1883
|
// never break user code
|
|
1881
1884
|
}
|
|
@@ -1891,8 +1894,8 @@ async function drainSdkBackgroundQueue(): Promise<void> {
|
|
|
1891
1894
|
}
|
|
1892
1895
|
|
|
1893
1896
|
configureIngestQueueDrain({
|
|
1894
|
-
shouldDefer:
|
|
1895
|
-
deferMs: SDK_BACKGROUND_IDLE_DELAY_MS,
|
|
1897
|
+
shouldDefer: (enqueuedAt) => getSdkBackgroundDelayMs(Date.now(), enqueuedAt) > 0,
|
|
1898
|
+
deferMs: Math.min(SDK_BACKGROUND_IDLE_DELAY_MS, SDK_BACKGROUND_MAX_DEFER_MS),
|
|
1896
1899
|
});
|
|
1897
1900
|
|
|
1898
1901
|
// Track in-flight requests per session so we can delay flushing until a session is drained.
|
package/src/ingest/client.ts
CHANGED
|
@@ -44,6 +44,7 @@ type QueuedIngestBatch = {
|
|
|
44
44
|
sessionId: string;
|
|
45
45
|
entries: LegacyEntry[];
|
|
46
46
|
attempts: number;
|
|
47
|
+
enqueuedAt: number;
|
|
47
48
|
priority: 'normal' | 'high' | 'critical';
|
|
48
49
|
};
|
|
49
50
|
|
|
@@ -59,13 +60,13 @@ let draining = false;
|
|
|
59
60
|
let criticalDraining = false;
|
|
60
61
|
let backoffMs = 0;
|
|
61
62
|
let criticalBackoffMs = 0;
|
|
62
|
-
let shouldDeferDrain: (() => boolean) | null = null;
|
|
63
|
+
let shouldDeferDrain: ((enqueuedAt?: number) => boolean) | null = null;
|
|
63
64
|
let deferDrainMs = DEFAULT_DEFER_DRAIN_MS;
|
|
64
65
|
const throttledUntil = new Map<string, number>();
|
|
65
66
|
const throttleDelay = (config: IngestClientConfig) => Math.max(0, (throttledUntil.get(resolveIngestUrl(config)) ?? 0) - Date.now());
|
|
66
67
|
|
|
67
68
|
export const configureIngestQueueDrain = (options: {
|
|
68
|
-
shouldDefer?: (() => boolean) | null;
|
|
69
|
+
shouldDefer?: ((enqueuedAt?: number) => boolean) | null;
|
|
69
70
|
deferMs?: number;
|
|
70
71
|
}): void => {
|
|
71
72
|
shouldDeferDrain = options.shouldDefer ?? null;
|
|
@@ -79,7 +80,7 @@ export const kickIngestQueueDrain = (): void => {
|
|
|
79
80
|
scheduleCriticalDrain();
|
|
80
81
|
}
|
|
81
82
|
if (ingestQueue.length > 0 && !drainTimer) {
|
|
82
|
-
scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
|
|
83
|
+
scheduleDrain(shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt) ? deferDrainMs : 0);
|
|
83
84
|
}
|
|
84
85
|
};
|
|
85
86
|
|
|
@@ -288,12 +289,12 @@ const scheduleDrain = (delayMs = 0): void => {
|
|
|
288
289
|
if (drainTimer) {
|
|
289
290
|
return;
|
|
290
291
|
}
|
|
291
|
-
const nextDelay = shouldDeferDrain?.()
|
|
292
|
+
const nextDelay = shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)
|
|
292
293
|
? Math.max(delayMs, deferDrainMs)
|
|
293
294
|
: delayMs;
|
|
294
295
|
drainTimer = setTimeout(() => {
|
|
295
296
|
drainTimer = null;
|
|
296
|
-
if (shouldDeferDrain?.()) {
|
|
297
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
297
298
|
scheduleDrain(deferDrainMs);
|
|
298
299
|
return;
|
|
299
300
|
}
|
|
@@ -365,14 +366,14 @@ const drainIngestQueue = async (): Promise<void> => {
|
|
|
365
366
|
if (draining) {
|
|
366
367
|
return;
|
|
367
368
|
}
|
|
368
|
-
if (shouldDeferDrain?.()) {
|
|
369
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
369
370
|
scheduleDrain(deferDrainMs);
|
|
370
371
|
return;
|
|
371
372
|
}
|
|
372
373
|
draining = true;
|
|
373
374
|
try {
|
|
374
375
|
while (ingestQueue.length > 0) {
|
|
375
|
-
if (shouldDeferDrain?.()) {
|
|
376
|
+
if (shouldDeferDrain?.(ingestQueue[0]?.enqueuedAt)) {
|
|
376
377
|
scheduleDrain(deferDrainMs);
|
|
377
378
|
return;
|
|
378
379
|
}
|
|
@@ -444,6 +445,7 @@ export const enqueueIngestEntries = (
|
|
|
444
445
|
sessionId,
|
|
445
446
|
entries: entries.slice(),
|
|
446
447
|
attempts: 0,
|
|
448
|
+
enqueuedAt: Date.now(),
|
|
447
449
|
priority,
|
|
448
450
|
};
|
|
449
451
|
if (priority === 'critical') {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '2000';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '200';
|
|
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 http = require('node:http');
|
|
9
|
+
const { GenericContainer, Wait } = require('testcontainers');
|
|
10
|
+
const { initRepro } = require('../dist');
|
|
11
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
12
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const events = [];
|
|
16
|
+
let rejectedOnce = false;
|
|
17
|
+
const sink = http.createServer((req, res) => {
|
|
18
|
+
const chunks = [];
|
|
19
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
20
|
+
req.on('end', () => {
|
|
21
|
+
const batch = JSON.parse(Buffer.concat(chunks)).events;
|
|
22
|
+
if (!rejectedOnce && batch.some(event => event.event_type === 'backend_request')) {
|
|
23
|
+
rejectedOnce = true;
|
|
24
|
+
res.writeHead(503); res.end('{}'); return;
|
|
25
|
+
}
|
|
26
|
+
events.push(...batch);
|
|
27
|
+
res.writeHead(202); res.end('{}');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
await new Promise(resolve => sink.listen(0, '127.0.0.1', resolve));
|
|
31
|
+
let mongo, app, polling;
|
|
32
|
+
let stopped = false, polls = 0, maxPollGap = 0;
|
|
33
|
+
try {
|
|
34
|
+
mongo = await new GenericContainer('mongo:7').withExposedPorts(27017)
|
|
35
|
+
.withWaitStrategy(Wait.forListeningPorts()).start();
|
|
36
|
+
const config = { tenantId: 'TENANT_busy', appId: 'APP_busy', appSecret: 'test',
|
|
37
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
38
|
+
ingestBase: `http://127.0.0.1:${sink.address().port}`,
|
|
39
|
+
tracing: { fullValueCapture: true, disableFunctionTypes: ['constructor'], logFunctionCalls: false } };
|
|
40
|
+
await initRepro(config);
|
|
41
|
+
app = await require('./fixtures/live-capture-limits-server').start(config,
|
|
42
|
+
`mongodb://${mongo.getHost()}:${mongo.getMappedPort(27017)}/busy`);
|
|
43
|
+
const base = `http://127.0.0.1:${app.server.address().port}`;
|
|
44
|
+
polling = (async () => {
|
|
45
|
+
let previous = Date.now();
|
|
46
|
+
while (!stopped) {
|
|
47
|
+
// Untagged, short requests also participate in the SDK's activity tracking.
|
|
48
|
+
const response = await fetch(`${base}/uncorrelated`);
|
|
49
|
+
await response.text();
|
|
50
|
+
maxPollGap = Math.max(maxPollGap, Date.now() - previous);
|
|
51
|
+
previous = Date.now();
|
|
52
|
+
polls++;
|
|
53
|
+
await sleep(20);
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
for (const mode of ['live', 'staging']) {
|
|
57
|
+
const response = await fetch(`${base}/shipment?__repro_sid=S_busy_${mode}&__repro_aid=A_${mode}&__repro_mode=${mode}`);
|
|
58
|
+
assert.equal(response.status, 200);
|
|
59
|
+
const body = await response.json();
|
|
60
|
+
assert.equal(body.rows.length, 157, 'capture must not change business responses');
|
|
61
|
+
assert.equal(body.rows[156].id, 156);
|
|
62
|
+
}
|
|
63
|
+
const complete = () => ['live', 'staging'].every(mode => {
|
|
64
|
+
const scoped = events.filter(event => event.session_id === `S_busy_${mode}`);
|
|
65
|
+
return ['request_capture_start', 'backend_request', 'request_capture_manifest', 'trace_batch', 'db_change']
|
|
66
|
+
.every(type => scoped.some(event => event.event_type === type));
|
|
67
|
+
});
|
|
68
|
+
// Do not call flushIngestQueue: automatic delivery must work while traffic continues.
|
|
69
|
+
const deadline = Date.now() + 6000;
|
|
70
|
+
while (!complete() && Date.now() < deadline) await sleep(25);
|
|
71
|
+
assert(polls > 5, 'continuous traffic must overlap capture finalization');
|
|
72
|
+
assert(maxPollGap < 2000, `traffic must not accidentally create an idle window: ${maxPollGap}ms`);
|
|
73
|
+
assert(complete(), `uploads starved during short requests: ${JSON.stringify(events.map(e => [e.session_id, e.event_type]))}`);
|
|
74
|
+
assert(rejectedOnce, 'the test must exercise a real collector retry');
|
|
75
|
+
for (const mode of ['live', 'staging']) {
|
|
76
|
+
const scoped = events.filter(event => event.session_id === `S_busy_${mode}`);
|
|
77
|
+
const requests = scoped.filter(event => event.event_type === 'backend_request');
|
|
78
|
+
assert.equal(requests.length, 1, 'finish and close must not duplicate requests');
|
|
79
|
+
const request = requests[0].payload.request;
|
|
80
|
+
const values = scoped.flatMap(e => [...(e.payload.requestValues || []), ...(e.payload.traceValues || []), ...(e.payload.dbValues || [])]);
|
|
81
|
+
const body = values.find(v => v.id === request.respBodyValueCapture?.valueId)?.value || request.respBody;
|
|
82
|
+
assert.equal(body.rows.length, mode === 'live' ? 101 : 157);
|
|
83
|
+
const trace = scoped.flatMap(e => e.payload.trace || []).find(e => e.fn === 'inspectShipment' && e.type === 'enter');
|
|
84
|
+
assert(trace, 'instrumented function arguments must survive background delivery');
|
|
85
|
+
const args = values.find(v => v.id === trace.argsValueCapture?.valueId)?.value || trace.args;
|
|
86
|
+
assert.equal(args[0].rows.length, mode === 'live' ? 101 : 157);
|
|
87
|
+
assert(scoped.flatMap(e => e.payload.db || []).some(change => change.op === 'findOne'));
|
|
88
|
+
}
|
|
89
|
+
console.log(`continuous short HTTP traffic: complete live/staging captures delivered during ${polls} requests`);
|
|
90
|
+
} finally {
|
|
91
|
+
stopped = true;
|
|
92
|
+
if (polling) await polling;
|
|
93
|
+
if (app) await app.close();
|
|
94
|
+
await sleep(2100);
|
|
95
|
+
await flushIngestQueue();
|
|
96
|
+
await new Promise(resolve => sink.close(resolve));
|
|
97
|
+
if (mongo) await mongo.stop();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
main().catch(error => { console.error(error); process.exitCode = 1; });
|