@reproapp/node-sdk 0.0.26 → 0.0.28
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.d.ts +11 -0
- package/dist/index.js +585 -1
- package/dist/ingest/client.js +18 -3
- package/package.json +2 -2
- package/src/index.ts +651 -0
- package/src/ingest/client.ts +25 -3
- package/test/ingest-critical-retention.test.js +147 -0
- package/test/ingest-critical-retry-retention.test.js +57 -0
- package/test/ingest-high-priority-retention.test.js +75 -0
- package/test/mongodb-integration.test.js +389 -0
package/src/ingest/client.ts
CHANGED
|
@@ -56,6 +56,25 @@ export const kickIngestQueueDrain = (): void => {
|
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const isCriticalIngestEntry = (entry: LegacyEntry): boolean => (
|
|
60
|
+
entry.requestCaptureManifest !== undefined ||
|
|
61
|
+
entry.sessionCaptureManifest !== undefined ||
|
|
62
|
+
entry.request !== undefined ||
|
|
63
|
+
entry.db !== undefined ||
|
|
64
|
+
entry.trace !== undefined ||
|
|
65
|
+
entry.email !== undefined
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const resolveBatchPriority = (
|
|
69
|
+
entries: LegacyEntry[],
|
|
70
|
+
options: EnqueueIngestOptions,
|
|
71
|
+
): QueuedIngestBatch['priority'] => {
|
|
72
|
+
if (options.priority === 'high') {
|
|
73
|
+
return 'high';
|
|
74
|
+
}
|
|
75
|
+
return entries.some(isCriticalIngestEntry) ? 'high' : 'normal';
|
|
76
|
+
};
|
|
77
|
+
|
|
59
78
|
const resolveIngestUrl = (config: IngestClientConfig): string => {
|
|
60
79
|
const envBase = typeof process !== 'undefined' ? (process as any)?.env?.REPRO_INGEST_BASE : undefined;
|
|
61
80
|
const ingestBase = String(envBase || config.ingestBase || DEFAULT_INGEST_BASE).replace(/\/+$/, '');
|
|
@@ -185,7 +204,7 @@ const drainIngestQueue = async (): Promise<void> => {
|
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
batch.attempts += 1;
|
|
188
|
-
if (batch.attempts >= DEFAULT_QUEUE_MAX_ATTEMPTS) {
|
|
207
|
+
if (batch.attempts >= DEFAULT_QUEUE_MAX_ATTEMPTS && batch.priority !== 'high') {
|
|
189
208
|
ingestQueue.shift();
|
|
190
209
|
backoffMs = 0;
|
|
191
210
|
continue;
|
|
@@ -215,7 +234,7 @@ export const enqueueIngestEntries = (
|
|
|
215
234
|
if (!sessionId || !Array.isArray(entries) || entries.length === 0) {
|
|
216
235
|
return false;
|
|
217
236
|
}
|
|
218
|
-
const priority = options
|
|
237
|
+
const priority = resolveBatchPriority(entries, options);
|
|
219
238
|
const batch: QueuedIngestBatch = {
|
|
220
239
|
config: { ...config },
|
|
221
240
|
sessionId,
|
|
@@ -235,13 +254,16 @@ export const enqueueIngestEntries = (
|
|
|
235
254
|
}
|
|
236
255
|
if (DEFAULT_QUEUE_MAX_ITEMS > 0 && ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
|
|
237
256
|
while (ingestQueue.length > DEFAULT_QUEUE_MAX_ITEMS) {
|
|
238
|
-
let removeIndex =
|
|
257
|
+
let removeIndex = -1;
|
|
239
258
|
for (let index = ingestQueue.length - 1; index >= 0; index -= 1) {
|
|
240
259
|
if (ingestQueue[index]?.priority !== 'high') {
|
|
241
260
|
removeIndex = index;
|
|
242
261
|
break;
|
|
243
262
|
}
|
|
244
263
|
}
|
|
264
|
+
if (removeIndex === -1) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
245
267
|
ingestQueue.splice(removeIndex, 1);
|
|
246
268
|
}
|
|
247
269
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
process.env.REPRO_INGEST_QUEUE_MAX_ITEMS = '3';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
|
|
5
|
+
const originalFetch = global.fetch;
|
|
6
|
+
const capturedBodies = [];
|
|
7
|
+
|
|
8
|
+
global.fetch = async (_url, init = {}) => {
|
|
9
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
status: 200,
|
|
13
|
+
json: async () => ({ ok: true }),
|
|
14
|
+
text: async () => '{"ok":true}',
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const { enqueueIngestEntries, flushIngestQueue } = require('../dist/ingest/client');
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const cfg = {
|
|
22
|
+
tenantId: 'TENANT_test',
|
|
23
|
+
appId: 'APP_test',
|
|
24
|
+
appSecret: 'secret',
|
|
25
|
+
serviceName: 'critical-retention-test',
|
|
26
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
27
|
+
};
|
|
28
|
+
const sessionId = 'S_critical_retention';
|
|
29
|
+
|
|
30
|
+
for (let index = 0; index < 5; index += 1) {
|
|
31
|
+
enqueueIngestEntries(cfg, sessionId, [{
|
|
32
|
+
actionId: 'A_optional',
|
|
33
|
+
message: `optional-${index}`,
|
|
34
|
+
t: Date.now(),
|
|
35
|
+
}]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const criticalEntries = [
|
|
39
|
+
{
|
|
40
|
+
actionId: 'A_critical',
|
|
41
|
+
request: {
|
|
42
|
+
rid: 'R_request',
|
|
43
|
+
method: 'GET',
|
|
44
|
+
url: '/critical-request',
|
|
45
|
+
status: 200,
|
|
46
|
+
},
|
|
47
|
+
t: Date.now(),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
actionId: 'A_critical',
|
|
51
|
+
requestRid: 'R_trace',
|
|
52
|
+
trace: [{
|
|
53
|
+
t: Date.now(),
|
|
54
|
+
type: 'enter',
|
|
55
|
+
fn: 'criticalTrace',
|
|
56
|
+
}],
|
|
57
|
+
traceBatch: {
|
|
58
|
+
rid: 'R_trace',
|
|
59
|
+
index: 0,
|
|
60
|
+
total: 1,
|
|
61
|
+
},
|
|
62
|
+
t: Date.now(),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
actionId: 'A_critical',
|
|
66
|
+
requestRid: 'R_db',
|
|
67
|
+
db: [{
|
|
68
|
+
collection: 'critical_collection',
|
|
69
|
+
op: 'findOne',
|
|
70
|
+
}],
|
|
71
|
+
t: Date.now(),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
actionId: 'A_critical',
|
|
75
|
+
requestRid: 'R_email',
|
|
76
|
+
email: {
|
|
77
|
+
provider: 'critical-email',
|
|
78
|
+
subject: 'critical',
|
|
79
|
+
},
|
|
80
|
+
t: Date.now(),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
actionId: 'A_critical',
|
|
84
|
+
requestRid: 'R_manifest',
|
|
85
|
+
requestCaptureManifest: {
|
|
86
|
+
scopeId: 'req:R_manifest',
|
|
87
|
+
requestRid: 'R_manifest',
|
|
88
|
+
transport: 'http',
|
|
89
|
+
expected: {
|
|
90
|
+
backendRequest: 1,
|
|
91
|
+
traceBatch: 0,
|
|
92
|
+
dbChange: 0,
|
|
93
|
+
emailEvent: 0,
|
|
94
|
+
},
|
|
95
|
+
closedAt: Date.now(),
|
|
96
|
+
},
|
|
97
|
+
t: Date.now(),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
actionId: 'A_critical',
|
|
101
|
+
sessionCaptureManifest: {
|
|
102
|
+
mode: 'staging',
|
|
103
|
+
expected: {
|
|
104
|
+
frontendActionDoc: 0,
|
|
105
|
+
rrwebChunk: 0,
|
|
106
|
+
frontendSessionEnd: 1,
|
|
107
|
+
},
|
|
108
|
+
closedAt: Date.now(),
|
|
109
|
+
},
|
|
110
|
+
t: Date.now(),
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const entry of criticalEntries) {
|
|
115
|
+
const enqueued = enqueueIngestEntries(cfg, sessionId, [entry]);
|
|
116
|
+
assert.strictEqual(enqueued, true, `critical event was dropped: ${JSON.stringify(Object.keys(entry))}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await flushIngestQueue();
|
|
120
|
+
|
|
121
|
+
const eventTypes = capturedBodies
|
|
122
|
+
.flatMap((body) => Array.isArray(body?.events) ? body.events : [])
|
|
123
|
+
.map((event) => event?.event_type)
|
|
124
|
+
.sort();
|
|
125
|
+
|
|
126
|
+
assert.deepStrictEqual(eventTypes, [
|
|
127
|
+
'backend_request',
|
|
128
|
+
'db_change',
|
|
129
|
+
'email_event',
|
|
130
|
+
'request_capture_manifest',
|
|
131
|
+
'session_capture_manifest',
|
|
132
|
+
'trace_batch',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log('ingest critical retention OK');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main()
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
// eslint-disable-next-line no-console
|
|
142
|
+
console.error(error);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
})
|
|
145
|
+
.finally(() => {
|
|
146
|
+
global.fetch = originalFetch;
|
|
147
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
|
|
3
|
+
const originalFetch = global.fetch;
|
|
4
|
+
let attempts = 0;
|
|
5
|
+
|
|
6
|
+
global.fetch = async () => {
|
|
7
|
+
attempts += 1;
|
|
8
|
+
return {
|
|
9
|
+
ok: attempts > 6,
|
|
10
|
+
status: attempts > 6 ? 200 : 503,
|
|
11
|
+
json: async () => ({ ok: attempts > 6 }),
|
|
12
|
+
text: async () => attempts > 6 ? '{"ok":true}' : '{"ok":false}',
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const { enqueueIngestEntries, flushIngestQueue } = require('../dist/ingest/client');
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const cfg = {
|
|
20
|
+
tenantId: 'TENANT_test',
|
|
21
|
+
appId: 'APP_test',
|
|
22
|
+
appSecret: 'secret',
|
|
23
|
+
serviceName: 'critical-retry-retention-test',
|
|
24
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const enqueued = enqueueIngestEntries(cfg, 'S_critical_retry_retention', [{
|
|
28
|
+
actionId: 'A_critical_retry',
|
|
29
|
+
request: {
|
|
30
|
+
rid: 'R_critical_retry',
|
|
31
|
+
method: 'GET',
|
|
32
|
+
url: '/critical-retry',
|
|
33
|
+
status: 200,
|
|
34
|
+
},
|
|
35
|
+
t: Date.now(),
|
|
36
|
+
}]);
|
|
37
|
+
assert.strictEqual(enqueued, true);
|
|
38
|
+
|
|
39
|
+
for (let index = 0; index < 7; index += 1) {
|
|
40
|
+
await flushIngestQueue();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assert.strictEqual(attempts, 7, `critical batch was dropped before eventual success; attempts=${attempts}`);
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.log('ingest critical retry retention OK');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main()
|
|
50
|
+
.catch((error) => {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.error(error);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
})
|
|
55
|
+
.finally(() => {
|
|
56
|
+
global.fetch = originalFetch;
|
|
57
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
process.env.REPRO_INGEST_QUEUE_MAX_ITEMS = '3';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
|
|
5
|
+
const originalFetch = global.fetch;
|
|
6
|
+
const capturedBodies = [];
|
|
7
|
+
|
|
8
|
+
global.fetch = async (_url, init = {}) => {
|
|
9
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
status: 200,
|
|
13
|
+
json: async () => ({ ok: true }),
|
|
14
|
+
text: async () => '{"ok":true}',
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const { enqueueIngestEntries, flushIngestQueue } = require('../dist/ingest/client');
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const cfg = {
|
|
22
|
+
tenantId: 'TENANT_test',
|
|
23
|
+
appId: 'APP_test',
|
|
24
|
+
appSecret: 'secret',
|
|
25
|
+
serviceName: 'high-priority-retention-test',
|
|
26
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
27
|
+
};
|
|
28
|
+
const sessionId = 'S_high_priority_retention';
|
|
29
|
+
|
|
30
|
+
for (let index = 0; index < 5; index += 1) {
|
|
31
|
+
const enqueued = enqueueIngestEntries(cfg, sessionId, [{
|
|
32
|
+
actionId: 'A_high_priority_retention',
|
|
33
|
+
requestRid: `R_high_priority_${index}`,
|
|
34
|
+
requestCaptureManifest: {
|
|
35
|
+
scopeId: `req:R_high_priority_${index}`,
|
|
36
|
+
requestRid: `R_high_priority_${index}`,
|
|
37
|
+
transport: 'http',
|
|
38
|
+
expected: {
|
|
39
|
+
backendRequest: 1,
|
|
40
|
+
traceBatch: 0,
|
|
41
|
+
dbChange: 0,
|
|
42
|
+
emailEvent: 0,
|
|
43
|
+
},
|
|
44
|
+
closedAt: Date.now(),
|
|
45
|
+
},
|
|
46
|
+
t: Date.now(),
|
|
47
|
+
}], { priority: 'high' });
|
|
48
|
+
assert.strictEqual(enqueued, true, `high-priority batch ${index} was dropped`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await flushIngestQueue();
|
|
52
|
+
|
|
53
|
+
const manifests = capturedBodies
|
|
54
|
+
.flatMap((body) => Array.isArray(body?.events) ? body.events : [])
|
|
55
|
+
.filter((event) => event?.event_type === 'request_capture_manifest');
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(manifests.length, 5, JSON.stringify({
|
|
58
|
+
got: manifests.length,
|
|
59
|
+
expected: 5,
|
|
60
|
+
requestIds: manifests.map((event) => event?.request_id),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.log('ingest high priority retention OK');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main()
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.error(error);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
})
|
|
73
|
+
.finally(() => {
|
|
74
|
+
global.fetch = originalFetch;
|
|
75
|
+
});
|