@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 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
- if (oldestActiveClientRequestAt !== null &&
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
- if (sdkBackgroundTimer) {
1376
- try {
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(work);
1395
+ sdkBackgroundHighQueue.push(item);
1402
1396
  }
1403
1397
  else {
1404
- sdkBackgroundQueue.push(work);
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 work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
1445
- if (!work)
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: shouldDeferSdkBackgroundWork,
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();
@@ -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>;
@@ -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.50",
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
- const sdkBackgroundHighQueue: Array<() => Promise<void> | void> = [];
1556
- const sdkBackgroundQueue: Array<() => Promise<void> | void> = [];
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
- if (
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
- if (sdkBackgroundTimer) {
1808
- try { clearTimeout(sdkBackgroundTimer); } catch {}
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(work);
1834
+ sdkBackgroundHighQueue.push(item);
1833
1835
  } else {
1834
- sdkBackgroundQueue.push(work);
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 work = sdkBackgroundHighQueue.shift() ?? sdkBackgroundQueue.shift();
1875
- if (!work) continue;
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: shouldDeferSdkBackgroundWork,
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.
@@ -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; });