@reproapp/node-sdk 0.0.37 → 0.0.39
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 +225 -310
- package/dist/index.d.ts +42 -0
- package/dist/index.js +488 -22
- package/package.json +2 -2
- package/src/index.ts +721 -22
- package/test/backend-live-mode.test.js +403 -0
- package/test/mongodb-integration.test.js +164 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '0';
|
|
6
|
+
process.env.REPRO_SDK_BACKGROUND_BUSY_IDLE_DELAY_MS = '0';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
reproMiddleware,
|
|
10
|
+
__reproTestHooks,
|
|
11
|
+
} = require('../dist');
|
|
12
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
13
|
+
|
|
14
|
+
async function listen(server) {
|
|
15
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
16
|
+
return server.address().port;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function close(server) {
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function request(port, path = '/untagged', options = {}) {
|
|
26
|
+
const method = options.method || 'POST';
|
|
27
|
+
const headers = {
|
|
28
|
+
...(method === 'POST' ? { 'content-type': 'application/json' } : {}),
|
|
29
|
+
...(options.headers || {}),
|
|
30
|
+
};
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
const req = http.request({
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
port,
|
|
35
|
+
path,
|
|
36
|
+
method,
|
|
37
|
+
headers,
|
|
38
|
+
}, (res) => {
|
|
39
|
+
res.resume();
|
|
40
|
+
res.on('end', resolve);
|
|
41
|
+
});
|
|
42
|
+
req.on('error', reject);
|
|
43
|
+
if (method === 'POST') {
|
|
44
|
+
req.end(JSON.stringify({ ok: true }));
|
|
45
|
+
} else {
|
|
46
|
+
req.end();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function drainAll() {
|
|
52
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
53
|
+
await flushIngestQueue();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function drainUntil(predicate, timeoutMs = 1000) {
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
while (Date.now() <= deadline) {
|
|
59
|
+
await drainAll();
|
|
60
|
+
if (predicate()) return true;
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
62
|
+
}
|
|
63
|
+
await drainAll();
|
|
64
|
+
return predicate();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const posts = [];
|
|
73
|
+
const claimCounts = new Map();
|
|
74
|
+
const originalFetch = global.fetch;
|
|
75
|
+
global.fetch = async (url, init = {}) => {
|
|
76
|
+
const body = init.body ? JSON.parse(String(init.body)) : null;
|
|
77
|
+
posts.push({ url: String(url), body, headers: init.headers || {} });
|
|
78
|
+
if (String(url) === 'http://repro-api.test/api/v1/live-sessions/claim') {
|
|
79
|
+
const actor = String(body?.actorId || 'unknown');
|
|
80
|
+
const count = (claimCounts.get(actor) || 0) + 1;
|
|
81
|
+
claimCounts.set(actor, count);
|
|
82
|
+
const safeActor = actor.replace(/[^A-Za-z0-9_.:-]/g, '_');
|
|
83
|
+
const childSuffix = actor === 'actor-overlap' ? `_${count}` : '';
|
|
84
|
+
const childSessionId = `S_claim_child_${safeActor}${childSuffix}`;
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
status: 200,
|
|
88
|
+
json: async () => ({
|
|
89
|
+
parentSessionId: `S_claim_parent_${safeActor}`,
|
|
90
|
+
childSessionId,
|
|
91
|
+
sessionId: childSessionId,
|
|
92
|
+
actionId: `A_claim_${safeActor}_${count}`,
|
|
93
|
+
clockOffsetMs: 0,
|
|
94
|
+
}),
|
|
95
|
+
text: async () => '{"ok":true}',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
status: 202,
|
|
101
|
+
json: async () => ({ ok: true }),
|
|
102
|
+
text: async () => '{"ok":true}',
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
107
|
+
__reproTestHooks.setBackendLiveTimingForTest({
|
|
108
|
+
segmentIdleMs: 1,
|
|
109
|
+
sessionMaxDurationMs: 20,
|
|
110
|
+
segmentMaxDurationMs: 20,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const cfg = {
|
|
114
|
+
appId: 'APP_backend_live',
|
|
115
|
+
tenantId: 'TENANT_backend_live',
|
|
116
|
+
appSecret: 'secret',
|
|
117
|
+
appName: 'Backend Live Test',
|
|
118
|
+
serviceName: 'Backend API',
|
|
119
|
+
apiBase: 'http://repro-api.test/api',
|
|
120
|
+
ingestBase: 'http://log-collector.test',
|
|
121
|
+
liveMode: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
backendOnly: true,
|
|
124
|
+
defineActorId: ({ req }) => req.headers['x-test-actor'] || 'actor-1',
|
|
125
|
+
shouldCapture: ({ path }) => path !== '/skip',
|
|
126
|
+
actorType: 'user',
|
|
127
|
+
actorLabels: { tier: 'enterprise' },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const app = express();
|
|
132
|
+
app.use(express.json());
|
|
133
|
+
app.use(reproMiddleware(cfg));
|
|
134
|
+
app.post('/untagged', (_req, res) => {
|
|
135
|
+
res.json({ ok: true });
|
|
136
|
+
});
|
|
137
|
+
const server = http.createServer(app);
|
|
138
|
+
const port = await listen(server);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await request(port, '/preflight', {
|
|
142
|
+
method: 'OPTIONS',
|
|
143
|
+
headers: {
|
|
144
|
+
origin: 'http://client.test',
|
|
145
|
+
'access-control-request-method': 'POST',
|
|
146
|
+
'access-control-request-headers': 'x-bug-session-id,x-bug-action-id,x-bug-request-start',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await drainAll();
|
|
150
|
+
assert.equal(
|
|
151
|
+
posts.length,
|
|
152
|
+
0,
|
|
153
|
+
'uncorrelated OPTIONS preflight should not create live sessions or ingest events',
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await request(port, '/preflight', {
|
|
157
|
+
method: 'OPTIONS',
|
|
158
|
+
headers: {
|
|
159
|
+
'x-bug-request-start': String(Date.now()),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
await drainAll();
|
|
163
|
+
assert.equal(
|
|
164
|
+
posts.length,
|
|
165
|
+
0,
|
|
166
|
+
'OPTIONS with only Repro timing should not create backend-only live sessions',
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const correlatedOptionsSid = 'S_frontend_options';
|
|
170
|
+
const correlatedOptionsAid = 'A_frontend_options';
|
|
171
|
+
await request(port, '/preflight', {
|
|
172
|
+
method: 'OPTIONS',
|
|
173
|
+
headers: {
|
|
174
|
+
'x-bug-session-id': correlatedOptionsSid,
|
|
175
|
+
'x-bug-action-id': correlatedOptionsAid,
|
|
176
|
+
'x-bug-request-start': String(Date.now()),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
await drainUntil(() => {
|
|
180
|
+
const events = posts
|
|
181
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
182
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
183
|
+
return events.some((event) =>
|
|
184
|
+
event.event_type === 'backend_request' &&
|
|
185
|
+
event.session_id === correlatedOptionsSid &&
|
|
186
|
+
event.payload?.action_id === correlatedOptionsAid
|
|
187
|
+
);
|
|
188
|
+
}, 2000);
|
|
189
|
+
assert(
|
|
190
|
+
posts
|
|
191
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
192
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : [])
|
|
193
|
+
.some((event) =>
|
|
194
|
+
event.event_type === 'backend_request' &&
|
|
195
|
+
event.session_id === correlatedOptionsSid &&
|
|
196
|
+
event.payload?.action_id === correlatedOptionsAid
|
|
197
|
+
),
|
|
198
|
+
'correlated OPTIONS request should still be captured',
|
|
199
|
+
);
|
|
200
|
+
assert.equal(
|
|
201
|
+
posts.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions').length,
|
|
202
|
+
0,
|
|
203
|
+
'frontend-correlated OPTIONS should not create a backend-only live parent',
|
|
204
|
+
);
|
|
205
|
+
assert.equal(
|
|
206
|
+
posts.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim').length,
|
|
207
|
+
0,
|
|
208
|
+
'frontend-correlated OPTIONS should not claim a backend-only live session',
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
posts.length = 0;
|
|
212
|
+
const correlatedPostSid = 'S_frontend_post';
|
|
213
|
+
const correlatedPostAid = 'A_frontend_post';
|
|
214
|
+
await request(port, '/untagged', {
|
|
215
|
+
headers: {
|
|
216
|
+
'x-bug-session-id': correlatedPostSid,
|
|
217
|
+
'x-bug-action-id': correlatedPostAid,
|
|
218
|
+
'x-bug-request-start': String(Date.now()),
|
|
219
|
+
'x-test-actor': 'backend-user-1',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
await drainUntil(() => {
|
|
223
|
+
const events = posts
|
|
224
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
225
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
226
|
+
return events.some((event) =>
|
|
227
|
+
event.event_type === 'backend_event' &&
|
|
228
|
+
event.session_id === correlatedPostSid &&
|
|
229
|
+
event.payload?.sessionActor?.actorId === 'backend-user-1'
|
|
230
|
+
);
|
|
231
|
+
}, 2000);
|
|
232
|
+
const correlatedEvents = posts
|
|
233
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
234
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
235
|
+
assert(
|
|
236
|
+
correlatedEvents.some((event) =>
|
|
237
|
+
event.event_type === 'backend_event' &&
|
|
238
|
+
event.session_id === correlatedPostSid &&
|
|
239
|
+
event.actor_id === 'backend-user-1' &&
|
|
240
|
+
event.payload?.sessionActor?.actorId === 'backend-user-1'
|
|
241
|
+
),
|
|
242
|
+
'frontend-correlated request should emit backend actor alias evidence',
|
|
243
|
+
);
|
|
244
|
+
assert.equal(
|
|
245
|
+
posts.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions').length,
|
|
246
|
+
0,
|
|
247
|
+
'frontend-correlated request should not create backend-only live sessions',
|
|
248
|
+
);
|
|
249
|
+
assert.equal(
|
|
250
|
+
posts.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim').length,
|
|
251
|
+
0,
|
|
252
|
+
'frontend-correlated request should not claim backend-only live sessions',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
posts.length = 0;
|
|
256
|
+
await request(port);
|
|
257
|
+
await drainUntil(() => {
|
|
258
|
+
const claims = posts
|
|
259
|
+
.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim')
|
|
260
|
+
.map((post) => post.body);
|
|
261
|
+
const events = posts
|
|
262
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
263
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
264
|
+
return claims.length >= 1 &&
|
|
265
|
+
events.some((event) => event.event_type === 'backend_request') &&
|
|
266
|
+
events.some((event) => event.event_type === 'request_capture_manifest');
|
|
267
|
+
}, 2000);
|
|
268
|
+
|
|
269
|
+
const claims = posts
|
|
270
|
+
.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim')
|
|
271
|
+
.map((post) => post.body);
|
|
272
|
+
assert.equal(claims.length, 1, 'backend-only request should claim one central live session');
|
|
273
|
+
assert.equal(claims[0].captureMode, 'backend-only');
|
|
274
|
+
assert.equal(claims[0].actorId, 'actor-1');
|
|
275
|
+
assert.deepEqual(claims[0].actorIds, ['actor-1']);
|
|
276
|
+
|
|
277
|
+
const ingestEvents = posts
|
|
278
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
279
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
280
|
+
assert(
|
|
281
|
+
ingestEvents.some((event) =>
|
|
282
|
+
event.event_type === 'backend_request' &&
|
|
283
|
+
event.session_id === 'S_claim_child_actor-1' &&
|
|
284
|
+
event.actor_id === 'actor-1'
|
|
285
|
+
),
|
|
286
|
+
'backend request event should be captured for claimed child session and actor',
|
|
287
|
+
);
|
|
288
|
+
assert(
|
|
289
|
+
ingestEvents.some((event) =>
|
|
290
|
+
event.event_type === 'request_capture_manifest' &&
|
|
291
|
+
event.session_id === 'S_claim_child_actor-1'
|
|
292
|
+
),
|
|
293
|
+
'request capture manifest should be emitted for claimed backend session',
|
|
294
|
+
);
|
|
295
|
+
assert.equal(
|
|
296
|
+
posts.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions').length,
|
|
297
|
+
0,
|
|
298
|
+
'SDK should not locally create backend-only sessions after central claim',
|
|
299
|
+
);
|
|
300
|
+
assert.equal(
|
|
301
|
+
posts.some((post) => String(post.url).includes('/finish')),
|
|
302
|
+
false,
|
|
303
|
+
'SDK should not locally finish centrally claimed backend-only sessions',
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
console.log('backend live mode capture OK');
|
|
307
|
+
} finally {
|
|
308
|
+
await close(server);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
posts.length = 0;
|
|
312
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
313
|
+
__reproTestHooks.setBackendLiveTimingForTest({
|
|
314
|
+
sessionMaxDurationMs: 30,
|
|
315
|
+
segmentMaxDurationMs: 30,
|
|
316
|
+
segmentIdleMs: 1,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const overlapCfg = {
|
|
320
|
+
...cfg,
|
|
321
|
+
appId: 'APP_backend_live_overlap',
|
|
322
|
+
liveMode: {
|
|
323
|
+
...cfg.liveMode,
|
|
324
|
+
defineActorId: () => 'actor-overlap',
|
|
325
|
+
shouldCapture: ({ path }) => path !== '/skip',
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const overlapApp = express();
|
|
330
|
+
overlapApp.use(express.json());
|
|
331
|
+
overlapApp.use(reproMiddleware(overlapCfg));
|
|
332
|
+
overlapApp.post('/slow', async (_req, res) => {
|
|
333
|
+
await sleep(80);
|
|
334
|
+
res.json({ ok: true, kind: 'slow' });
|
|
335
|
+
});
|
|
336
|
+
overlapApp.post('/fast', (_req, res) => {
|
|
337
|
+
res.json({ ok: true, kind: 'fast' });
|
|
338
|
+
});
|
|
339
|
+
const overlapServer = http.createServer(overlapApp);
|
|
340
|
+
const overlapPort = await listen(overlapServer);
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const slowRequest = request(overlapPort, '/slow');
|
|
344
|
+
await sleep(50);
|
|
345
|
+
await request(overlapPort, '/fast');
|
|
346
|
+
await slowRequest;
|
|
347
|
+
|
|
348
|
+
await drainUntil(() => {
|
|
349
|
+
const claims = posts
|
|
350
|
+
.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim');
|
|
351
|
+
const events = posts
|
|
352
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
353
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
354
|
+
const requestSessionIds = new Set(
|
|
355
|
+
events
|
|
356
|
+
.filter((event) =>
|
|
357
|
+
event.event_type === 'backend_request' &&
|
|
358
|
+
event.actor_id === 'actor-overlap'
|
|
359
|
+
)
|
|
360
|
+
.map((event) => event.session_id),
|
|
361
|
+
);
|
|
362
|
+
return claims.length >= 2 && requestSessionIds.size >= 2;
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const claims = posts
|
|
366
|
+
.filter((post) => post.url === 'http://repro-api.test/api/v1/live-sessions/claim')
|
|
367
|
+
.map((post) => post.body);
|
|
368
|
+
assert.equal(claims.length, 2, 'each backend-only request should ask the API for ownership');
|
|
369
|
+
assert(
|
|
370
|
+
claims.every((claim) => claim.actorId === 'actor-overlap'),
|
|
371
|
+
'overlap claims should keep the actor identity',
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const ingestEvents = posts
|
|
375
|
+
.filter((post) => post.url === 'http://log-collector.test/v1/ingest/events')
|
|
376
|
+
.flatMap((post) => Array.isArray(post.body?.events) ? post.body.events : []);
|
|
377
|
+
const requestSessionIds = new Set(
|
|
378
|
+
ingestEvents
|
|
379
|
+
.filter((event) =>
|
|
380
|
+
event.event_type === 'backend_request' &&
|
|
381
|
+
event.actor_id === 'actor-overlap'
|
|
382
|
+
)
|
|
383
|
+
.map((event) => event.session_id),
|
|
384
|
+
);
|
|
385
|
+
assert.deepEqual(
|
|
386
|
+
new Set(['S_claim_child_actor-overlap_1', 'S_claim_child_actor-overlap_2']),
|
|
387
|
+
requestSessionIds,
|
|
388
|
+
'backend requests should use exactly the child sessions returned by the API claim',
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
console.log('backend live mode central claim overlap OK');
|
|
392
|
+
} finally {
|
|
393
|
+
await close(overlapServer);
|
|
394
|
+
global.fetch = originalFetch;
|
|
395
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
396
|
+
__reproTestHooks.setBackendLiveTimingForTest(null);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
main().catch((err) => {
|
|
401
|
+
console.error(err);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
});
|
|
@@ -133,12 +133,24 @@ function execPre(hooks, name, context) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function execPost(hooks, name, context, args = []) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
hooks.execPost(name, context, args, {}, (error) => (error ? reject(error) : resolve()));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
function dbChanges(events) {
|
|
137
143
|
return events
|
|
138
144
|
.filter((event) => event.event_type === 'db_change')
|
|
139
145
|
.flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
function dbValueEntries(events) {
|
|
149
|
+
return events
|
|
150
|
+
.filter((event) => event.event_type === 'db_change')
|
|
151
|
+
.flatMap((event) => Array.isArray(event.payload?.dbValues) ? event.payload.dbValues : []);
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
function requestCaptureManifests(events) {
|
|
143
155
|
return events
|
|
144
156
|
.filter((event) => event.event_type === 'request_capture_manifest')
|
|
@@ -146,6 +158,81 @@ function requestCaptureManifests(events) {
|
|
|
146
158
|
.filter(Boolean);
|
|
147
159
|
}
|
|
148
160
|
|
|
161
|
+
function makeNotificationModuleDocument() {
|
|
162
|
+
const notificationPreferences = Array.from({ length: 24 }, (_, index) => ({
|
|
163
|
+
trigger: 'SubjectScreened',
|
|
164
|
+
enabled: true,
|
|
165
|
+
channels: [
|
|
166
|
+
{
|
|
167
|
+
type: 'email',
|
|
168
|
+
template: {
|
|
169
|
+
subject: `Participant Screening ${index}`,
|
|
170
|
+
html: `<p>Screening notification ${index}</p>`,
|
|
171
|
+
},
|
|
172
|
+
recipients: {
|
|
173
|
+
to: ['noreply@korioclinical.com'],
|
|
174
|
+
cc: [
|
|
175
|
+
`pobrien+dev-conf-csm${index}@korioclinical.com`,
|
|
176
|
+
`mnebot+site-monitor${index}@korioclinical.com`,
|
|
177
|
+
],
|
|
178
|
+
bcc: [`audit-${index}@korioclinical.com`],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}));
|
|
183
|
+
const notificationTrigger = Array.from({ length: 24 }, (_, index) => ({
|
|
184
|
+
active: true,
|
|
185
|
+
event: 'SubjectScreened',
|
|
186
|
+
visitType: 'Screening',
|
|
187
|
+
subjectType: 'Participant',
|
|
188
|
+
rules: [
|
|
189
|
+
{
|
|
190
|
+
condition: {
|
|
191
|
+
path: 'subject.subjectStatusOption',
|
|
192
|
+
operator: 'equals',
|
|
193
|
+
value: 'Screened',
|
|
194
|
+
},
|
|
195
|
+
action: {
|
|
196
|
+
module: 'NotificationGeneratorService',
|
|
197
|
+
method: 'SubjectScreened',
|
|
198
|
+
order: index,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
__t: 'NotificationModule',
|
|
206
|
+
_id: '69e77c77ab541cfac917b37a',
|
|
207
|
+
active: true,
|
|
208
|
+
environment: 'dev-configure-pulmovant',
|
|
209
|
+
notificationPreferences,
|
|
210
|
+
notification_trigger: notificationTrigger,
|
|
211
|
+
protocol: {
|
|
212
|
+
id: '69e77c77ab541cfac917b36b',
|
|
213
|
+
name: 'RVT-2301-301',
|
|
214
|
+
},
|
|
215
|
+
schema_version: 1,
|
|
216
|
+
sponsor_name: 'Pulmovant',
|
|
217
|
+
study_name: 'RVT-2301-301',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class NotificationModuleHostDocument {
|
|
222
|
+
constructor(value) {
|
|
223
|
+
this.$__ = {};
|
|
224
|
+
this.value = value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
toObject() {
|
|
228
|
+
return JSON.parse(JSON.stringify(this.value));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
toJSON() {
|
|
232
|
+
return this.toObject();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
149
236
|
async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
|
|
150
237
|
await withCapturedFetch(async (capturedBodies) => {
|
|
151
238
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -477,6 +564,82 @@ async function testClientScreenNativeMongoInsertOneShapeIsCaptured() {
|
|
|
477
564
|
});
|
|
478
565
|
}
|
|
479
566
|
|
|
567
|
+
async function testLargeMongooseFindOneResultStoresFullResultMetaSidecar() {
|
|
568
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
569
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
570
|
+
const sessionId = 'S_mongoose_large_result_meta';
|
|
571
|
+
const actionId = 'A_mongoose_large_result_meta';
|
|
572
|
+
const cfg = {
|
|
573
|
+
...makeConfig(sessionId, actionId),
|
|
574
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const schema = new mongoose.Schema({}, { strict: false });
|
|
578
|
+
reproMongoosePlugin(cfg)(schema);
|
|
579
|
+
|
|
580
|
+
const notificationModule = makeNotificationModuleDocument();
|
|
581
|
+
const rawResult = new NotificationModuleHostDocument(notificationModule);
|
|
582
|
+
const queryContext = {
|
|
583
|
+
model: {
|
|
584
|
+
collection: {
|
|
585
|
+
name: 'configStudy',
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
getFilter: () => ({
|
|
589
|
+
__t: 'NotificationModule',
|
|
590
|
+
active: true,
|
|
591
|
+
protocol: {
|
|
592
|
+
id: '69e77c77ab541cfac917b36b',
|
|
593
|
+
},
|
|
594
|
+
}),
|
|
595
|
+
getUpdate: () => undefined,
|
|
596
|
+
projection: () => undefined,
|
|
597
|
+
getOptions: () => ({ lean: false }),
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
601
|
+
await execPre(schema.s.hooks, 'findOne', queryContext);
|
|
602
|
+
await execPost(schema.s.hooks, 'findOne', queryContext, [rawResult]);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
606
|
+
await waitFor(() => dbValueEntries(flattenEvents(capturedBodies)).some((entry) => entry.target === 'db.resultMeta'));
|
|
607
|
+
await drainSdk();
|
|
608
|
+
|
|
609
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
610
|
+
const values = dbValueEntries(flattenEvents(capturedBodies));
|
|
611
|
+
const resultMetaValue = values.find((entry) => entry.id === change.resultMetaValueCapture?.valueId);
|
|
612
|
+
|
|
613
|
+
assert.strictEqual(change.type, 'mongo');
|
|
614
|
+
assert.strictEqual(change.collection, 'configStudy');
|
|
615
|
+
assert.strictEqual(change.op, 'findOne');
|
|
616
|
+
assert.strictEqual(change.query.filter.__t, 'NotificationModule');
|
|
617
|
+
assert.strictEqual(change.resultMeta.docsCount, 1);
|
|
618
|
+
assert.deepStrictEqual(change.resultMeta.result.notificationPreferences[0], {
|
|
619
|
+
__kind: 'omitted',
|
|
620
|
+
reason: 'depth>3',
|
|
621
|
+
});
|
|
622
|
+
assert.deepStrictEqual(change.resultMeta.result.notification_trigger[0], {
|
|
623
|
+
__kind: 'omitted',
|
|
624
|
+
reason: 'depth>3',
|
|
625
|
+
});
|
|
626
|
+
assert.strictEqual(change.resultMetaValueCapture.status, 'stored');
|
|
627
|
+
assert.ok(change.resultMetaValueCapture.valueId, 'expected resultMeta value id');
|
|
628
|
+
assert.ok(resultMetaValue, 'expected matching db.resultMeta sidecar');
|
|
629
|
+
assert.strictEqual(resultMetaValue.target, 'db.resultMeta');
|
|
630
|
+
assert.strictEqual(resultMetaValue.value.__t, 'NotificationModule');
|
|
631
|
+
assert.strictEqual(resultMetaValue.value.notificationPreferences.length, 24);
|
|
632
|
+
assert.strictEqual(
|
|
633
|
+
resultMetaValue.value.notificationPreferences[0].channels[0].recipients.cc[0],
|
|
634
|
+
'pobrien+dev-conf-csm0@korioclinical.com',
|
|
635
|
+
);
|
|
636
|
+
assert.strictEqual(
|
|
637
|
+
resultMetaValue.value.notification_trigger[0].rules[0].action.method,
|
|
638
|
+
'SubjectScreened',
|
|
639
|
+
);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
480
643
|
async function testNativeInsertOneIsCaptured() {
|
|
481
644
|
await withCapturedFetch(async (capturedBodies) => {
|
|
482
645
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -742,6 +905,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
|
|
|
742
905
|
await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
|
|
743
906
|
await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
|
|
744
907
|
await testClientScreenNativeMongoInsertOneShapeIsCaptured();
|
|
908
|
+
await testLargeMongooseFindOneResultStoresFullResultMetaSidecar();
|
|
745
909
|
await testNativeInsertOneIsCaptured();
|
|
746
910
|
await testCaptureRawNativeInsertOneUsesJsonSafeTransportValues();
|
|
747
911
|
await testNativeUpdateOneUpsertIsCaptured();
|