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