@reproapp/node-sdk 0.0.39 → 0.0.42

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.
@@ -14,6 +14,22 @@ const DEFAULT_QUEUE_MAX_ITEMS = (() => {
14
14
  const DEFAULT_QUEUE_MAX_ATTEMPTS = 5;
15
15
  const DEFAULT_QUEUE_BACKOFF_MAX_MS = 4000;
16
16
  const DEFAULT_DEFER_DRAIN_MS = 25;
17
+ const DEFAULT_BATCH_MAX_ENTRIES = (() => {
18
+ const env = Number(typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_BATCH_MAX_ENTRIES : undefined);
19
+ if (Number.isFinite(env) && env > 0) {
20
+ return Math.trunc(env);
21
+ }
22
+ return 100;
23
+ })();
24
+ const DEFAULT_BATCH_MAX_SERIALIZED_BYTES = (() => {
25
+ const env = Number(typeof process !== 'undefined'
26
+ ? process?.env?.REPRO_INGEST_BATCH_MAX_SERIALIZED_BYTES
27
+ : undefined);
28
+ if (Number.isFinite(env) && env > 0) {
29
+ return Math.trunc(env);
30
+ }
31
+ return 1024 * 1024;
32
+ })();
17
33
  const DEFAULT_INGEST_REQUEST_TIMEOUT_MS = (() => {
18
34
  const env = Number(typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_REQUEST_TIMEOUT_MS : undefined);
19
35
  if (Number.isFinite(env) && env >= 0) {
@@ -22,9 +38,13 @@ const DEFAULT_INGEST_REQUEST_TIMEOUT_MS = (() => {
22
38
  return 120000;
23
39
  })();
24
40
  const ingestQueue = [];
41
+ const criticalIngestQueue = [];
25
42
  let drainTimer = null;
43
+ let criticalDrainTimer = null;
26
44
  let draining = false;
45
+ let criticalDraining = false;
27
46
  let backoffMs = 0;
47
+ let criticalBackoffMs = 0;
28
48
  let shouldDeferDrain = null;
29
49
  let deferDrainMs = DEFAULT_DEFER_DRAIN_MS;
30
50
  const configureIngestQueueDrain = (options) => {
@@ -35,22 +55,88 @@ const configureIngestQueueDrain = (options) => {
35
55
  };
36
56
  exports.configureIngestQueueDrain = configureIngestQueueDrain;
37
57
  const kickIngestQueueDrain = () => {
58
+ if (criticalIngestQueue.length > 0 && !criticalDrainTimer) {
59
+ scheduleCriticalDrain();
60
+ }
38
61
  if (ingestQueue.length > 0 && !drainTimer) {
39
62
  scheduleDrain(shouldDeferDrain?.() ? deferDrainMs : 0);
40
63
  }
41
64
  };
42
65
  exports.kickIngestQueueDrain = kickIngestQueueDrain;
43
- const isCriticalIngestEntry = (entry) => (entry.requestCaptureManifest !== undefined ||
66
+ const isHighPriorityIngestEntry = (entry) => (entry.requestCaptureStart !== undefined ||
67
+ entry.requestCaptureManifest !== undefined ||
44
68
  entry.sessionCaptureManifest !== undefined ||
45
69
  entry.request !== undefined ||
46
70
  entry.db !== undefined ||
47
71
  entry.trace !== undefined ||
48
72
  entry.email !== undefined);
49
73
  const resolveBatchPriority = (entries, options) => {
50
- if (options.priority === 'high') {
51
- return 'high';
74
+ if (options.priority === 'high' || options.priority === 'critical') {
75
+ return options.priority;
76
+ }
77
+ return entries.some(isHighPriorityIngestEntry) ? 'high' : 'normal';
78
+ };
79
+ const sameIngestDestination = (left, right) => (left.sessionId === right.sessionId &&
80
+ left.priority === right.priority &&
81
+ left.config.appId === right.config.appId &&
82
+ left.config.tenantId === right.config.tenantId &&
83
+ left.config.appSecret === right.config.appSecret &&
84
+ left.config.appName === right.config.appName &&
85
+ left.config.serviceName === right.config.serviceName &&
86
+ left.config.ingestBase === right.config.ingestBase &&
87
+ left.config.ingestPath === right.config.ingestPath &&
88
+ left.config.actorId === right.config.actorId &&
89
+ left.config.actorType === right.config.actorType &&
90
+ JSON.stringify(left.config.actorLabels ?? null) ===
91
+ JSON.stringify(right.config.actorLabels ?? null) &&
92
+ left.config.source === right.config.source &&
93
+ left.config.schemaVersion === right.config.schemaVersion);
94
+ const serializedBytes = (value) => {
95
+ try {
96
+ return Buffer.byteLength(JSON.stringify(value), 'utf8');
97
+ }
98
+ catch {
99
+ return DEFAULT_BATCH_MAX_SERIALIZED_BYTES + 1;
52
100
  }
53
- return entries.some(isCriticalIngestEntry) ? 'high' : 'normal';
101
+ };
102
+ const selectSendBatch = (queue) => {
103
+ const first = queue[0];
104
+ if (!first) {
105
+ return null;
106
+ }
107
+ if (first.priority === 'normal') {
108
+ return { batch: first, queueItems: 1 };
109
+ }
110
+ const entries = first.entries.slice();
111
+ let bytes = serializedBytes(entries);
112
+ let queueItems = 1;
113
+ for (let index = 1; index < queue.length && entries.length < DEFAULT_BATCH_MAX_ENTRIES; index += 1) {
114
+ const candidate = queue[index];
115
+ if (!candidate || !sameIngestDestination(first, candidate)) {
116
+ break;
117
+ }
118
+ if (entries.length + candidate.entries.length > DEFAULT_BATCH_MAX_ENTRIES) {
119
+ break;
120
+ }
121
+ const candidateBytes = serializedBytes(candidate.entries);
122
+ if (bytes + candidateBytes > DEFAULT_BATCH_MAX_SERIALIZED_BYTES) {
123
+ break;
124
+ }
125
+ entries.push(...candidate.entries);
126
+ bytes += candidateBytes;
127
+ queueItems += 1;
128
+ }
129
+ if (queueItems === 1) {
130
+ return { batch: first, queueItems };
131
+ }
132
+ return {
133
+ batch: {
134
+ ...first,
135
+ entries,
136
+ attempts: Math.max(...queue.slice(0, queueItems).map((item) => item.attempts)),
137
+ },
138
+ queueItems,
139
+ };
54
140
  };
55
141
  const resolveIngestUrl = (config) => {
56
142
  const envBase = typeof process !== 'undefined' ? process?.env?.REPRO_INGEST_BASE : undefined;
@@ -143,6 +229,51 @@ const scheduleDrain = (delayMs = 0) => {
143
229
  }
144
230
  catch { }
145
231
  };
232
+ const scheduleCriticalDrain = (delayMs = 0) => {
233
+ if (criticalDrainTimer) {
234
+ return;
235
+ }
236
+ criticalDrainTimer = setTimeout(() => {
237
+ criticalDrainTimer = null;
238
+ void drainCriticalIngestQueue();
239
+ }, Math.max(0, delayMs));
240
+ try {
241
+ criticalDrainTimer.unref?.();
242
+ }
243
+ catch { }
244
+ };
245
+ const drainCriticalIngestQueue = async () => {
246
+ if (criticalDraining) {
247
+ return;
248
+ }
249
+ criticalDraining = true;
250
+ try {
251
+ while (criticalIngestQueue.length > 0) {
252
+ const selected = selectSendBatch(criticalIngestQueue);
253
+ if (!selected) {
254
+ return;
255
+ }
256
+ const ok = await sendIngestEntries(selected.batch.config, selected.batch.sessionId, selected.batch.entries);
257
+ if (ok) {
258
+ criticalIngestQueue.splice(0, selected.queueItems);
259
+ criticalBackoffMs = 0;
260
+ continue;
261
+ }
262
+ for (let index = 0; index < selected.queueItems; index += 1) {
263
+ criticalIngestQueue[index].attempts += 1;
264
+ }
265
+ criticalBackoffMs = Math.min(DEFAULT_QUEUE_BACKOFF_MAX_MS, criticalBackoffMs ? criticalBackoffMs * 2 : 250);
266
+ scheduleCriticalDrain(criticalBackoffMs);
267
+ return;
268
+ }
269
+ }
270
+ finally {
271
+ criticalDraining = false;
272
+ if (criticalIngestQueue.length > 0 && !criticalDrainTimer) {
273
+ scheduleCriticalDrain(criticalBackoffMs);
274
+ }
275
+ }
276
+ };
146
277
  const drainIngestQueue = async () => {
147
278
  if (draining) {
148
279
  return;
@@ -158,15 +289,22 @@ const drainIngestQueue = async () => {
158
289
  scheduleDrain(deferDrainMs);
159
290
  return;
160
291
  }
161
- const batch = ingestQueue[0];
292
+ const selected = selectSendBatch(ingestQueue);
293
+ if (!selected) {
294
+ return;
295
+ }
296
+ const batch = selected.batch;
162
297
  const ok = await sendIngestEntries(batch.config, batch.sessionId, batch.entries);
163
298
  if (ok) {
164
- ingestQueue.shift();
299
+ ingestQueue.splice(0, selected.queueItems);
165
300
  backoffMs = 0;
166
301
  continue;
167
302
  }
168
- batch.attempts += 1;
169
- if (batch.attempts >= DEFAULT_QUEUE_MAX_ATTEMPTS && batch.priority !== 'high') {
303
+ for (let index = 0; index < selected.queueItems; index += 1) {
304
+ ingestQueue[index].attempts += 1;
305
+ }
306
+ if (ingestQueue[0].attempts >= DEFAULT_QUEUE_MAX_ATTEMPTS &&
307
+ ingestQueue[0].priority === 'normal') {
170
308
  ingestQueue.shift();
171
309
  backoffMs = 0;
172
310
  continue;
@@ -195,6 +333,11 @@ const enqueueIngestEntries = (config, sessionId, entries, options = {}) => {
195
333
  attempts: 0,
196
334
  priority,
197
335
  };
336
+ if (priority === 'critical') {
337
+ criticalIngestQueue.push(batch);
338
+ scheduleCriticalDrain();
339
+ return true;
340
+ }
198
341
  if (priority === 'high') {
199
342
  const firstNormalIndex = ingestQueue.findIndex((queued) => queued.priority !== 'high');
200
343
  if (firstNormalIndex === -1) {
@@ -227,10 +370,15 @@ const enqueueIngestEntries = (config, sessionId, entries, options = {}) => {
227
370
  };
228
371
  exports.enqueueIngestEntries = enqueueIngestEntries;
229
372
  const flushIngestQueue = async () => {
373
+ if (criticalDrainTimer) {
374
+ clearTimeout(criticalDrainTimer);
375
+ criticalDrainTimer = null;
376
+ }
230
377
  if (drainTimer) {
231
378
  clearTimeout(drainTimer);
232
379
  drainTimer = null;
233
380
  }
381
+ await drainCriticalIngestQueue();
234
382
  await drainIngestQueue();
235
383
  };
236
384
  exports.flushIngestQueue = flushIngestQueue;
@@ -6,6 +6,8 @@ const toIso = (timestamp) => {
6
6
  return new Date(numeric).toISOString();
7
7
  };
8
8
  const inferEventType = (entry) => {
9
+ if (entry.requestCaptureStart !== undefined)
10
+ return 'request_capture_start';
9
11
  if (entry.requestCaptureManifest !== undefined)
10
12
  return 'request_capture_manifest';
11
13
  if (entry.sessionCaptureManifest !== undefined)
@@ -17,6 +17,7 @@ export interface LegacyEntry {
17
17
  requestRid?: string | null;
18
18
  request?: unknown;
19
19
  requestValues?: unknown[];
20
+ requestCaptureStart?: unknown;
20
21
  requestCaptureManifest?: unknown;
21
22
  sessionCaptureManifest?: unknown;
22
23
  db?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.39",
3
+ "version": "0.0.42",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,15 +12,21 @@
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-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/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-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 && 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",
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 && node test/mongodb-mongoose-native-suppression.testcontainers.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/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/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.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-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",
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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "express": "^5.1.0",
20
20
  "mongoose": ">=6"
21
21
  },
22
22
  "devDependencies": {
23
- "@types/express": "^5.0.3"
23
+ "@nestjs/typeorm": "^11.0.0",
24
+ "@types/express": "^5.0.3",
25
+ "@types/pg": "^8.20.0",
26
+ "mongodb": "^6.21.0",
27
+ "pg": "^8.22.0",
28
+ "postgres": "^3.4.9",
29
+ "typeorm": "^0.3.28"
24
30
  },
25
31
  "publishConfig": {
26
32
  "access": "public"