@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.
- package/README.md +4 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +592 -2
- package/dist/ingest/client.d.ts +1 -1
- package/dist/ingest/client.js +156 -8
- package/dist/ingest/mapper.js +2 -0
- package/dist/ingest/types.d.ts +1 -0
- package/package.json +10 -4
- package/src/index.ts +651 -1
- package/src/ingest/client.ts +187 -10
- package/src/ingest/mapper.ts +1 -0
- package/src/ingest/types.ts +1 -0
- package/test/backend-live-mode.test.js +20 -2
- package/test/client-performance-regression.test.js +5 -0
- package/test/ingest-batching.test.js +67 -0
- package/test/postgresql-integration.test.js +302 -0
- package/test/postgresql-integrations.testcontainers.test.js +261 -0
- package/test/request-capture-background.test.js +6 -1
- package/test/request-manifest-priority.test.js +6 -0
package/dist/ingest/client.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
299
|
+
ingestQueue.splice(0, selected.queueItems);
|
|
165
300
|
backoffMs = 0;
|
|
166
301
|
continue;
|
|
167
302
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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;
|
package/dist/ingest/mapper.js
CHANGED
|
@@ -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)
|
package/dist/ingest/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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
|
-
"@
|
|
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"
|