@reproapp/node-sdk 0.0.27 → 0.0.29

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.js CHANGED
@@ -1028,10 +1028,6 @@ function scheduleTraceMaterializerIdleShutdown() {
1028
1028
  catch { }
1029
1029
  }
1030
1030
  }, TRACE_MATERIALIZE_WORKER_IDLE_SHUTDOWN_MS);
1031
- try {
1032
- traceMaterializerIdleTimer.unref?.();
1033
- }
1034
- catch { }
1035
1031
  }
1036
1032
  function rejectTraceMaterializerPending(error) {
1037
1033
  for (const [taskId, pending] of traceMaterializerPending.entries()) {
@@ -1373,11 +1369,43 @@ function waitForSessionDrain(sessionId) {
1373
1369
  if (remaining <= 0)
1374
1370
  return Promise.resolve();
1375
1371
  return new Promise(resolve => {
1372
+ let settled = false;
1373
+ let timeout = null;
1374
+ const removeWaiter = (waiter) => {
1375
+ const list = sessionWaiters.get(sessionId);
1376
+ if (!list)
1377
+ return;
1378
+ const next = list.filter(fn => fn !== waiter);
1379
+ if (next.length) {
1380
+ sessionWaiters.set(sessionId, next);
1381
+ }
1382
+ else {
1383
+ sessionWaiters.delete(sessionId);
1384
+ }
1385
+ };
1386
+ const done = () => {
1387
+ if (settled)
1388
+ return;
1389
+ settled = true;
1390
+ if (timeout) {
1391
+ try {
1392
+ clearTimeout(timeout);
1393
+ }
1394
+ catch { }
1395
+ timeout = null;
1396
+ }
1397
+ removeWaiter(done);
1398
+ resolve();
1399
+ };
1376
1400
  const list = sessionWaiters.get(sessionId) || [];
1377
- list.push(resolve);
1401
+ list.push(done);
1378
1402
  sessionWaiters.set(sessionId, list);
1379
1403
  if (SESSION_DRAIN_TIMEOUT_MS > 0 && Number.isFinite(SESSION_DRAIN_TIMEOUT_MS)) {
1380
- setTimeout(resolve, SESSION_DRAIN_TIMEOUT_MS);
1404
+ timeout = setTimeout(done, SESSION_DRAIN_TIMEOUT_MS);
1405
+ try {
1406
+ timeout.unref?.();
1407
+ }
1408
+ catch { }
1381
1409
  }
1382
1410
  });
1383
1411
  }
@@ -3997,6 +4025,7 @@ async function maybeCaptureDbValueAsync(params, sink) {
3997
4025
  }
3998
4026
  function reproMiddleware(cfg) {
3999
4027
  const masking = normalizeMaskingConfig(cfg.masking);
4028
+ patchMongoDb(cfg);
4000
4029
  return async function (req, res, next) {
4001
4030
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4002
4031
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
@@ -4545,6 +4574,7 @@ function nativeMongoOpsForMongooseOp(op) {
4545
4574
  return [op];
4546
4575
  }
4547
4576
  function reproMongoosePlugin(cfg) {
4577
+ patchMongoDb(cfg);
4548
4578
  return function (schema) {
4549
4579
  // -------- pre/post save (unchanged) --------
4550
4580
  schema.pre('save', { document: true }, function (next) {
@@ -7941,6 +7971,50 @@ function patchMongoCollectionPrototype(proto, cfg) {
7941
7971
  patchMongoCollectionMethod(proto, op, true, cfg);
7942
7972
  }
7943
7973
  }
7974
+ function patchMongoCollectionInstance(collection, cfg) {
7975
+ if (!collection || typeof collection !== 'object')
7976
+ return collection;
7977
+ patchMongoCollectionPrototype(Object.getPrototypeOf(collection), cfg);
7978
+ patchMongoCollectionPrototype(collection, cfg);
7979
+ return collection;
7980
+ }
7981
+ function patchMongoDbPrototype(proto, cfg) {
7982
+ if (!proto)
7983
+ return;
7984
+ const marker = '__repro_mongodb_db_collection_patched';
7985
+ if (proto[marker])
7986
+ return;
7987
+ const original = proto.collection;
7988
+ if (typeof original !== 'function')
7989
+ return;
7990
+ proto[marker] = true;
7991
+ proto.collection = function reproPatchedMongoDbCollection(...args) {
7992
+ const collection = original.apply(this, args);
7993
+ return patchMongoCollectionInstance(collection, cfg);
7994
+ };
7995
+ }
7996
+ function patchMongoDbInstance(db, cfg) {
7997
+ if (!db || typeof db !== 'object')
7998
+ return db;
7999
+ patchMongoDbPrototype(Object.getPrototypeOf(db), cfg);
8000
+ patchMongoDbPrototype(db, cfg);
8001
+ return db;
8002
+ }
8003
+ function patchMongoClientPrototype(proto, cfg) {
8004
+ if (!proto)
8005
+ return;
8006
+ const marker = '__repro_mongodb_client_db_patched';
8007
+ if (proto[marker])
8008
+ return;
8009
+ const original = proto.db;
8010
+ if (typeof original !== 'function')
8011
+ return;
8012
+ proto[marker] = true;
8013
+ proto.db = function reproPatchedMongoClientDb(...args) {
8014
+ const db = original.apply(this, args);
8015
+ return patchMongoDbInstance(db, cfg);
8016
+ };
8017
+ }
7944
8018
  function patchMongoDbFromModuleExports(mongoModule, cfg) {
7945
8019
  if (!mongoModule)
7946
8020
  return;
@@ -7955,6 +8029,28 @@ function patchMongoDbFromModuleExports(mongoModule, cfg) {
7955
8029
  for (const ctor of constructors) {
7956
8030
  patchMongoCollectionPrototype(ctor.prototype, cfg);
7957
8031
  }
8032
+ const dbConstructors = new Set();
8033
+ const clientConstructors = new Set();
8034
+ const addDbCtor = (candidate) => {
8035
+ if (typeof candidate === 'function' && candidate.prototype) {
8036
+ dbConstructors.add(candidate);
8037
+ }
8038
+ };
8039
+ const addClientCtor = (candidate) => {
8040
+ if (typeof candidate === 'function' && candidate.prototype) {
8041
+ clientConstructors.add(candidate);
8042
+ }
8043
+ };
8044
+ addDbCtor(mongoModule?.Db);
8045
+ addDbCtor(mongoModule?.default?.Db);
8046
+ addClientCtor(mongoModule?.MongoClient);
8047
+ addClientCtor(mongoModule?.default?.MongoClient);
8048
+ for (const ctor of dbConstructors) {
8049
+ patchMongoDbPrototype(ctor.prototype, cfg);
8050
+ }
8051
+ for (const ctor of clientConstructors) {
8052
+ patchMongoClientPrototype(ctor.prototype, cfg);
8053
+ }
7958
8054
  }
7959
8055
  function installMongoDbRequireHook() {
7960
8056
  if (mongoDbRequireHookInstalled)
@@ -7979,10 +8075,6 @@ function patchMongoDb(cfg) {
7979
8075
  mongoDbPatchConfigs.push(cfg);
7980
8076
  installMongoDbRequireHook();
7981
8077
  const mongoModules = new Set();
7982
- const resolvedMongoModule = requireOptionalHostModule('mongodb');
7983
- if (resolvedMongoModule) {
7984
- mongoModules.add(resolvedMongoModule);
7985
- }
7986
8078
  for (const loadedMongoModule of getLoadedModuleExportsByPackage('mongodb')) {
7987
8079
  mongoModules.add(loadedMongoModule);
7988
8080
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -1429,7 +1429,6 @@ function scheduleTraceMaterializerIdleShutdown(): void {
1429
1429
  try { void worker.terminate?.(); } catch {}
1430
1430
  }
1431
1431
  }, TRACE_MATERIALIZE_WORKER_IDLE_SHUTDOWN_MS);
1432
- try { (traceMaterializerIdleTimer as any).unref?.(); } catch {}
1433
1432
  }
1434
1433
 
1435
1434
  function rejectTraceMaterializerPending(error: Error): void {
@@ -1745,11 +1744,34 @@ function waitForSessionDrain(sessionId: string): Promise<void> {
1745
1744
  if (remaining <= 0) return Promise.resolve();
1746
1745
 
1747
1746
  return new Promise(resolve => {
1747
+ let settled = false;
1748
+ let timeout: NodeJS.Timeout | null = null;
1749
+ const removeWaiter = (waiter: () => void) => {
1750
+ const list = sessionWaiters.get(sessionId);
1751
+ if (!list) return;
1752
+ const next = list.filter(fn => fn !== waiter);
1753
+ if (next.length) {
1754
+ sessionWaiters.set(sessionId, next);
1755
+ } else {
1756
+ sessionWaiters.delete(sessionId);
1757
+ }
1758
+ };
1759
+ const done = () => {
1760
+ if (settled) return;
1761
+ settled = true;
1762
+ if (timeout) {
1763
+ try { clearTimeout(timeout); } catch {}
1764
+ timeout = null;
1765
+ }
1766
+ removeWaiter(done);
1767
+ resolve();
1768
+ };
1748
1769
  const list = sessionWaiters.get(sessionId) || [];
1749
- list.push(resolve);
1770
+ list.push(done);
1750
1771
  sessionWaiters.set(sessionId, list);
1751
1772
  if (SESSION_DRAIN_TIMEOUT_MS > 0 && Number.isFinite(SESSION_DRAIN_TIMEOUT_MS)) {
1752
- setTimeout(resolve, SESSION_DRAIN_TIMEOUT_MS);
1773
+ timeout = setTimeout(done, SESSION_DRAIN_TIMEOUT_MS);
1774
+ try { (timeout as any).unref?.(); } catch {}
1753
1775
  }
1754
1776
  });
1755
1777
  }
@@ -4892,6 +4914,7 @@ export type ReproInitConfig = ReproMiddlewareConfig & {
4892
4914
 
4893
4915
  export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4894
4916
  const masking = normalizeMaskingConfig(cfg.masking);
4917
+ patchMongoDb(cfg);
4895
4918
  return async function (req: Request, res: Response, next: NextFunction) {
4896
4919
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4897
4920
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
@@ -5489,6 +5512,7 @@ function nativeMongoOpsForMongooseOp(op: string): string[] {
5489
5512
  }
5490
5513
 
5491
5514
  export function reproMongoosePlugin(cfg: ReproMiddlewareConfig) {
5515
+ patchMongoDb(cfg);
5492
5516
  return function (schema: Schema) {
5493
5517
  // -------- pre/post save (unchanged) --------
5494
5518
  schema.pre('save', { document: true }, function (next) {
@@ -9184,6 +9208,46 @@ function patchMongoCollectionPrototype(proto: any, cfg: MongoDbPatchConfig): voi
9184
9208
  }
9185
9209
  }
9186
9210
 
9211
+ function patchMongoCollectionInstance(collection: any, cfg: MongoDbPatchConfig): any {
9212
+ if (!collection || typeof collection !== 'object') return collection;
9213
+ patchMongoCollectionPrototype(Object.getPrototypeOf(collection), cfg);
9214
+ patchMongoCollectionPrototype(collection, cfg);
9215
+ return collection;
9216
+ }
9217
+
9218
+ function patchMongoDbPrototype(proto: any, cfg: MongoDbPatchConfig): void {
9219
+ if (!proto) return;
9220
+ const marker = '__repro_mongodb_db_collection_patched';
9221
+ if (proto[marker]) return;
9222
+ const original = proto.collection;
9223
+ if (typeof original !== 'function') return;
9224
+ proto[marker] = true;
9225
+ proto.collection = function reproPatchedMongoDbCollection(this: any, ...args: any[]) {
9226
+ const collection = original.apply(this, args);
9227
+ return patchMongoCollectionInstance(collection, cfg);
9228
+ };
9229
+ }
9230
+
9231
+ function patchMongoDbInstance(db: any, cfg: MongoDbPatchConfig): any {
9232
+ if (!db || typeof db !== 'object') return db;
9233
+ patchMongoDbPrototype(Object.getPrototypeOf(db), cfg);
9234
+ patchMongoDbPrototype(db, cfg);
9235
+ return db;
9236
+ }
9237
+
9238
+ function patchMongoClientPrototype(proto: any, cfg: MongoDbPatchConfig): void {
9239
+ if (!proto) return;
9240
+ const marker = '__repro_mongodb_client_db_patched';
9241
+ if (proto[marker]) return;
9242
+ const original = proto.db;
9243
+ if (typeof original !== 'function') return;
9244
+ proto[marker] = true;
9245
+ proto.db = function reproPatchedMongoClientDb(this: any, ...args: any[]) {
9246
+ const db = original.apply(this, args);
9247
+ return patchMongoDbInstance(db, cfg);
9248
+ };
9249
+ }
9250
+
9187
9251
  function patchMongoDbFromModuleExports(mongoModule: any, cfg: MongoDbPatchConfig): void {
9188
9252
  if (!mongoModule) return;
9189
9253
  const constructors = new Set<any>();
@@ -9199,6 +9263,31 @@ function patchMongoDbFromModuleExports(mongoModule: any, cfg: MongoDbPatchConfig
9199
9263
  for (const ctor of constructors) {
9200
9264
  patchMongoCollectionPrototype(ctor.prototype, cfg);
9201
9265
  }
9266
+
9267
+ const dbConstructors = new Set<any>();
9268
+ const clientConstructors = new Set<any>();
9269
+ const addDbCtor = (candidate: any) => {
9270
+ if (typeof candidate === 'function' && candidate.prototype) {
9271
+ dbConstructors.add(candidate);
9272
+ }
9273
+ };
9274
+ const addClientCtor = (candidate: any) => {
9275
+ if (typeof candidate === 'function' && candidate.prototype) {
9276
+ clientConstructors.add(candidate);
9277
+ }
9278
+ };
9279
+
9280
+ addDbCtor(mongoModule?.Db);
9281
+ addDbCtor(mongoModule?.default?.Db);
9282
+ addClientCtor(mongoModule?.MongoClient);
9283
+ addClientCtor(mongoModule?.default?.MongoClient);
9284
+
9285
+ for (const ctor of dbConstructors) {
9286
+ patchMongoDbPrototype(ctor.prototype, cfg);
9287
+ }
9288
+ for (const ctor of clientConstructors) {
9289
+ patchMongoClientPrototype(ctor.prototype, cfg);
9290
+ }
9202
9291
  }
9203
9292
 
9204
9293
  function installMongoDbRequireHook(): void {
@@ -9225,10 +9314,6 @@ export function patchMongoDb(cfg: MongoDbPatchConfig): void {
9225
9314
  installMongoDbRequireHook();
9226
9315
 
9227
9316
  const mongoModules = new Set<any>();
9228
- const resolvedMongoModule = requireOptionalHostModule('mongodb');
9229
- if (resolvedMongoModule) {
9230
- mongoModules.add(resolvedMongoModule);
9231
- }
9232
9317
  for (const loadedMongoModule of getLoadedModuleExportsByPackage('mongodb')) {
9233
9318
  mongoModules.add(loadedMongoModule);
9234
9319
  }
@@ -7,6 +7,16 @@ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
7
7
 
8
8
  const assert = require('assert');
9
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
+
10
20
  const { flushIngestQueue } = require('../dist/ingest/client');
11
21
  const { reproMiddleware, __reproTestHooks } = require('../dist');
12
22
 
@@ -122,6 +132,127 @@ function dbChanges(events) {
122
132
  .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
123
133
  }
124
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 testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport() {
180
+ await withCapturedFetch(async (capturedBodies) => {
181
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
182
+ const sessionId = 'S_mongo_client_chain';
183
+ const actionId = 'A_mongo_client_chain';
184
+ const cfg = makeConfig(sessionId, actionId);
185
+
186
+ class FakeCollection {
187
+ constructor(dbName, collectionName) {
188
+ this.dbName = dbName;
189
+ this.collectionName = collectionName;
190
+ }
191
+
192
+ async insertOne(doc, options) {
193
+ assert.strictEqual(options.session.id, 'native-chain-session-1');
194
+ doc._id = doc._id ?? 'subject-chain-1';
195
+ return { acknowledged: true, insertedId: doc._id };
196
+ }
197
+ }
198
+
199
+ class FakeDb {
200
+ constructor(dbName) {
201
+ this.databaseName = dbName;
202
+ }
203
+
204
+ collection(collectionName) {
205
+ return new FakeCollection(this.databaseName, collectionName);
206
+ }
207
+ }
208
+
209
+ class FakeMongoClient {
210
+ db(dbName) {
211
+ return new FakeDb(dbName);
212
+ }
213
+ }
214
+
215
+ __reproTestHooks.patchMongoDbModuleForTest({
216
+ MongoClient: FakeMongoClient,
217
+ Db: FakeDb,
218
+ }, cfg);
219
+
220
+ const dbClient = new FakeMongoClient();
221
+ const subjectsCollection = dbClient.db('clinical').collection('subjects');
222
+
223
+ await runCapturedRequest(cfg, sessionId, actionId, async () => {
224
+ const result = await subjectsCollection.insertOne(
225
+ { subjectNumber: '10013038', status: 'screened' },
226
+ {
227
+ session: {
228
+ id: 'native-chain-session-1',
229
+ hasEnded: false,
230
+ inTransaction: () => true,
231
+ },
232
+ },
233
+ );
234
+ assert.deepStrictEqual(result, { acknowledged: true, insertedId: 'subject-chain-1' });
235
+ });
236
+
237
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
238
+ await drainSdk();
239
+
240
+ const [change] = dbChanges(flattenEvents(capturedBodies));
241
+ assert.strictEqual(change.type, 'mongo');
242
+ assert.strictEqual(change.collection, 'subjects');
243
+ assert.strictEqual(change.op, 'insertOne');
244
+ assert.strictEqual(change.pk._id, 'subject-chain-1');
245
+ assert.strictEqual(change.query.driver, 'mongodb');
246
+ assert.strictEqual(change.query.db, 'clinical');
247
+ assert.strictEqual(change.query.doc.subjectNumber, '10013038');
248
+ assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
249
+ assert.strictEqual(change.query.options.session.id, 'native-chain-session-1');
250
+ assert.strictEqual(change.after.subjectNumber, '10013038');
251
+ assert.strictEqual(change.resultMeta.insertedId, 'subject-chain-1');
252
+ assert.strictEqual(change.resultMeta.acknowledged, true);
253
+ });
254
+ }
255
+
125
256
  async function testNativeInsertOneIsCaptured() {
126
257
  await withCapturedFetch(async (capturedBodies) => {
127
258
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -321,6 +452,8 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
321
452
  }
322
453
 
323
454
  (async () => {
455
+ await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
456
+ await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
324
457
  await testNativeInsertOneIsCaptured();
325
458
  await testNativeUpdateOneUpsertIsCaptured();
326
459
  await testNativeFindCursorToArrayIsCaptured();
@@ -329,4 +462,6 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
329
462
  })().catch((err) => {
330
463
  console.error(err);
331
464
  process.exit(1);
465
+ }).finally(() => {
466
+ mongodb.Collection.prototype.insertOne = originalNativeMongoInsertOne;
332
467
  });