@reproapp/node-sdk 0.0.26 → 0.0.27
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 +588 -1
- package/dist/ingest/client.js +18 -3
- package/package.json +2 -2
- package/src/index.ts +654 -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 +332 -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
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
process.env.REPRO_INGEST_REQUEST_TIMEOUT_MS = '50';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '25';
|
|
3
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
|
|
4
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
5
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
6
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
7
|
+
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const { EventEmitter } = require('events');
|
|
10
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
11
|
+
const { reproMiddleware, __reproTestHooks } = require('../dist');
|
|
12
|
+
|
|
13
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
async function waitFor(predicate, timeoutMs = 2000) {
|
|
16
|
+
const deadline = Date.now() + timeoutMs;
|
|
17
|
+
while (Date.now() < deadline) {
|
|
18
|
+
if (predicate()) return;
|
|
19
|
+
await sleep(20);
|
|
20
|
+
}
|
|
21
|
+
assert(predicate(), 'timed out waiting for expected ingest payload');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function flattenEvents(posts) {
|
|
25
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function drainSdk() {
|
|
29
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
30
|
+
await flushIngestQueue();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function withCapturedFetch(fn) {
|
|
34
|
+
const capturedBodies = [];
|
|
35
|
+
const originalFetch = global.fetch;
|
|
36
|
+
global.fetch = async (_url, init) => {
|
|
37
|
+
capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
status: 202,
|
|
41
|
+
json: async () => ({ ok: true }),
|
|
42
|
+
text: async () => '{"ok":true}',
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await fn(capturedBodies);
|
|
48
|
+
} finally {
|
|
49
|
+
await drainSdk();
|
|
50
|
+
global.fetch = originalFetch;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeConfig(sessionId, actionId) {
|
|
55
|
+
return {
|
|
56
|
+
tenantId: 'TENANT_test',
|
|
57
|
+
appId: 'APP_test',
|
|
58
|
+
appSecret: 'secret',
|
|
59
|
+
captureHeaders: false,
|
|
60
|
+
privacy: { environment: 'dev' },
|
|
61
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
62
|
+
resolveContext: () => ({ sid: sessionId, aid: actionId }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeReqRes(sessionId, actionId) {
|
|
67
|
+
const req = new EventEmitter();
|
|
68
|
+
req.method = 'GET';
|
|
69
|
+
req.url = '/native-mongodb';
|
|
70
|
+
req.originalUrl = '/native-mongodb';
|
|
71
|
+
req.headers = {
|
|
72
|
+
'x-bug-session-id': sessionId,
|
|
73
|
+
'x-bug-action-id': actionId,
|
|
74
|
+
};
|
|
75
|
+
req.query = {};
|
|
76
|
+
req.params = {};
|
|
77
|
+
req.body = undefined;
|
|
78
|
+
|
|
79
|
+
const res = new EventEmitter();
|
|
80
|
+
res.statusCode = 200;
|
|
81
|
+
res.getHeader = () => undefined;
|
|
82
|
+
res.getHeaders = () => ({});
|
|
83
|
+
res.json = function json(body) {
|
|
84
|
+
this.emit('finish');
|
|
85
|
+
return body;
|
|
86
|
+
};
|
|
87
|
+
res.send = function send(body) {
|
|
88
|
+
this.emit('finish');
|
|
89
|
+
return body;
|
|
90
|
+
};
|
|
91
|
+
res.write = function write() {
|
|
92
|
+
return true;
|
|
93
|
+
};
|
|
94
|
+
res.end = function end() {
|
|
95
|
+
this.emit('finish');
|
|
96
|
+
return this;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { req, res };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runCapturedRequest(cfg, sessionId, actionId, fn) {
|
|
103
|
+
const middleware = reproMiddleware(cfg);
|
|
104
|
+
const { req, res } = makeReqRes(sessionId, actionId);
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
middleware(req, res, (err) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
reject(err);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
Promise.resolve()
|
|
112
|
+
.then(fn)
|
|
113
|
+
.then(resolve, reject);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
res.end();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function dbChanges(events) {
|
|
120
|
+
return events
|
|
121
|
+
.filter((event) => event.event_type === 'db_change')
|
|
122
|
+
.flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function testNativeInsertOneIsCaptured() {
|
|
126
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
127
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
128
|
+
const sessionId = 'S_mongo_insert';
|
|
129
|
+
const actionId = 'A_mongo_insert';
|
|
130
|
+
const cfg = makeConfig(sessionId, actionId);
|
|
131
|
+
|
|
132
|
+
class FakeCollection {
|
|
133
|
+
constructor() {
|
|
134
|
+
this.collectionName = 'subjects';
|
|
135
|
+
this.dbName = 'clinical';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async insertOne(doc, options) {
|
|
139
|
+
assert.strictEqual(options.session.id, 'native-session-1');
|
|
140
|
+
doc._id = doc._id ?? 'subject-1';
|
|
141
|
+
return { acknowledged: true, insertedId: doc._id };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
__reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
|
|
146
|
+
const collection = new FakeCollection();
|
|
147
|
+
|
|
148
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
149
|
+
const result = await collection.insertOne(
|
|
150
|
+
{ subjectNumber: '10013038', status: 'screened' },
|
|
151
|
+
{
|
|
152
|
+
session: {
|
|
153
|
+
id: 'native-session-1',
|
|
154
|
+
hasEnded: false,
|
|
155
|
+
inTransaction: () => true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
assert.deepStrictEqual(result, { acknowledged: true, insertedId: 'subject-1' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
163
|
+
await drainSdk();
|
|
164
|
+
|
|
165
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
166
|
+
assert.strictEqual(change.type, 'mongo');
|
|
167
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
168
|
+
assert.strictEqual(change.op, 'insertOne');
|
|
169
|
+
assert.strictEqual(change.pk._id, 'subject-1');
|
|
170
|
+
assert.strictEqual(change.query.driver, 'mongodb');
|
|
171
|
+
assert.strictEqual(change.query.db, 'clinical');
|
|
172
|
+
assert.strictEqual(change.query.doc.subjectNumber, '10013038');
|
|
173
|
+
assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
|
|
174
|
+
assert.strictEqual(change.query.options.session.id, 'native-session-1');
|
|
175
|
+
assert.strictEqual(change.query.options.session.inTransaction, true);
|
|
176
|
+
assert.strictEqual(change.after.subjectNumber, '10013038');
|
|
177
|
+
assert.strictEqual(change.resultMeta.insertedId, 'subject-1');
|
|
178
|
+
assert.strictEqual(change.resultMeta.acknowledged, true);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function testNativeUpdateOneUpsertIsCaptured() {
|
|
183
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
184
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
185
|
+
const sessionId = 'S_mongo_update';
|
|
186
|
+
const actionId = 'A_mongo_update';
|
|
187
|
+
const cfg = makeConfig(sessionId, actionId);
|
|
188
|
+
|
|
189
|
+
class FakeCollection {
|
|
190
|
+
constructor() {
|
|
191
|
+
this.collectionName = 'subjects';
|
|
192
|
+
this.dbName = 'clinical';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async updateOne() {
|
|
196
|
+
return {
|
|
197
|
+
acknowledged: true,
|
|
198
|
+
matchedCount: 0,
|
|
199
|
+
modifiedCount: 0,
|
|
200
|
+
upsertedId: 'subject-2',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
__reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
|
|
206
|
+
const collection = new FakeCollection();
|
|
207
|
+
|
|
208
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
209
|
+
await collection.updateOne(
|
|
210
|
+
{ _id: 'subject-2' },
|
|
211
|
+
{ $set: { status: 'randomized' } },
|
|
212
|
+
{ upsert: true },
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
217
|
+
await drainSdk();
|
|
218
|
+
|
|
219
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
220
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
221
|
+
assert.strictEqual(change.op, 'updateOne');
|
|
222
|
+
assert.strictEqual(change.pk._id, 'subject-2');
|
|
223
|
+
assert.strictEqual(change.query.filter._id, 'subject-2');
|
|
224
|
+
assert.strictEqual(change.query.update.$set.status, 'randomized');
|
|
225
|
+
assert.strictEqual(change.query.options.upsert, true);
|
|
226
|
+
assert.strictEqual(change.resultMeta.matched, 0);
|
|
227
|
+
assert.strictEqual(change.resultMeta.modified, 0);
|
|
228
|
+
assert.strictEqual(change.resultMeta.upsertedId, 'subject-2');
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function testNativeFindCursorToArrayIsCaptured() {
|
|
233
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
234
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
235
|
+
const sessionId = 'S_mongo_find';
|
|
236
|
+
const actionId = 'A_mongo_find';
|
|
237
|
+
const cfg = makeConfig(sessionId, actionId);
|
|
238
|
+
|
|
239
|
+
class FakeCollection {
|
|
240
|
+
constructor() {
|
|
241
|
+
this.collectionName = 'subjects';
|
|
242
|
+
this.dbName = 'clinical';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
find() {
|
|
246
|
+
return {
|
|
247
|
+
async toArray() {
|
|
248
|
+
return [
|
|
249
|
+
{ _id: 'subject-3', status: 'screened' },
|
|
250
|
+
{ _id: 'subject-4', status: 'randomized' },
|
|
251
|
+
];
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
__reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
|
|
258
|
+
const collection = new FakeCollection();
|
|
259
|
+
|
|
260
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
261
|
+
const docs = await collection.find({ siteId: 'site-1001' }, { projection: { status: 1 } }).toArray();
|
|
262
|
+
assert.strictEqual(docs.length, 2);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
266
|
+
await drainSdk();
|
|
267
|
+
|
|
268
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
269
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
270
|
+
assert.strictEqual(change.op, 'find');
|
|
271
|
+
assert.strictEqual(change.query.filter.siteId, 'site-1001');
|
|
272
|
+
assert.deepStrictEqual(change.query.options.projection, { status: 1 });
|
|
273
|
+
assert.strictEqual(change.query.cursorMethod, 'toArray');
|
|
274
|
+
assert.strictEqual(change.resultMeta.docsCount, 2);
|
|
275
|
+
assert.strictEqual(change.resultMeta.result[0]._id, 'subject-3');
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function testNativeOperationFailureIsCapturedAndRethrown() {
|
|
280
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
281
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
282
|
+
const sessionId = 'S_mongo_error';
|
|
283
|
+
const actionId = 'A_mongo_error';
|
|
284
|
+
const cfg = makeConfig(sessionId, actionId);
|
|
285
|
+
|
|
286
|
+
class FakeCollection {
|
|
287
|
+
constructor() {
|
|
288
|
+
this.collectionName = 'subjects';
|
|
289
|
+
this.dbName = 'clinical';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async deleteOne() {
|
|
293
|
+
const err = new Error('delete failed');
|
|
294
|
+
err.code = 12345;
|
|
295
|
+
err.codeName = 'DeleteFailed';
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
__reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
|
|
301
|
+
const collection = new FakeCollection();
|
|
302
|
+
|
|
303
|
+
await assert.rejects(
|
|
304
|
+
() => runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
305
|
+
await collection.deleteOne({ _id: 'subject-5' });
|
|
306
|
+
}),
|
|
307
|
+
/delete failed/,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
311
|
+
await drainSdk();
|
|
312
|
+
|
|
313
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
314
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
315
|
+
assert.strictEqual(change.op, 'deleteOne');
|
|
316
|
+
assert.strictEqual(change.query.filter._id, 'subject-5');
|
|
317
|
+
assert.strictEqual(change.error.message, 'delete failed');
|
|
318
|
+
assert.strictEqual(change.error.code, 12345);
|
|
319
|
+
assert.strictEqual(change.error.codeName, 'DeleteFailed');
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
(async () => {
|
|
324
|
+
await testNativeInsertOneIsCaptured();
|
|
325
|
+
await testNativeUpdateOneUpsertIsCaptured();
|
|
326
|
+
await testNativeFindCursorToArrayIsCaptured();
|
|
327
|
+
await testNativeOperationFailureIsCapturedAndRethrown();
|
|
328
|
+
console.log('mongodb integration tests passed');
|
|
329
|
+
})().catch((err) => {
|
|
330
|
+
console.error(err);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
});
|