@reproapp/node-sdk 0.0.32 → 0.0.33

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
@@ -869,6 +869,18 @@ function releaseNativeMongoSuppression(target) {
869
869
  catch { }
870
870
  }
871
871
  }
872
+ function isMongooseInternalNativeMongoCall() {
873
+ let stack = '';
874
+ try {
875
+ stack = new Error().stack ?? '';
876
+ }
877
+ catch {
878
+ stack = '';
879
+ }
880
+ return stack.includes('/node_modules/mongoose/') ||
881
+ stack.includes('\\node_modules\\mongoose\\') ||
882
+ stack.includes('mongoose/lib/');
883
+ }
872
884
  function isNativeMongoSuppressed(collection, op) {
873
885
  const suppressions = getCtx().nativeMongoSuppressions;
874
886
  if (!Array.isArray(suppressions) || suppressions.length === 0)
@@ -876,6 +888,7 @@ function isNativeMongoSuppressed(collection, op) {
876
888
  const normalizedCollection = collection == null ? null : String(collection);
877
889
  const normalizedOp = normalizeNativeMongoSuppressionOp(op);
878
890
  const now = Date.now();
891
+ const matchingTokens = [];
879
892
  for (let i = suppressions.length - 1; i >= 0; i -= 1) {
880
893
  const token = suppressions[i];
881
894
  if (!token || now - token.startedAt > 60000) {
@@ -886,9 +899,17 @@ function isNativeMongoSuppressed(collection, op) {
886
899
  continue;
887
900
  if (token.ops && token.ops.size > 0 && !token.ops.has(normalizedOp))
888
901
  continue;
889
- return true;
902
+ matchingTokens.push(token);
890
903
  }
891
- return false;
904
+ if (matchingTokens.length === 0)
905
+ return false;
906
+ if (!isMongooseInternalNativeMongoCall()) {
907
+ return false;
908
+ }
909
+ for (const token of matchingTokens) {
910
+ popNativeMongoSuppression(token);
911
+ }
912
+ return true;
892
913
  }
893
914
  function currentClockSkewMs() {
894
915
  const store = als.getStore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
14
14
  "prepublishOnly": "npm run build",
15
15
  "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js",
16
- "test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js"
16
+ "test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js && node test/mongodb-mongoose-native-suppression.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "express": "^5.1.0",
package/src/index.ts CHANGED
@@ -1267,12 +1267,25 @@ function releaseNativeMongoSuppression(target: any): void {
1267
1267
  }
1268
1268
  }
1269
1269
 
1270
+ function isMongooseInternalNativeMongoCall(): boolean {
1271
+ let stack = '';
1272
+ try {
1273
+ stack = new Error().stack ?? '';
1274
+ } catch {
1275
+ stack = '';
1276
+ }
1277
+ return stack.includes('/node_modules/mongoose/') ||
1278
+ stack.includes('\\node_modules\\mongoose\\') ||
1279
+ stack.includes('mongoose/lib/');
1280
+ }
1281
+
1270
1282
  function isNativeMongoSuppressed(collection: any, op: string): boolean {
1271
1283
  const suppressions = (getCtx() as Ctx).nativeMongoSuppressions;
1272
1284
  if (!Array.isArray(suppressions) || suppressions.length === 0) return false;
1273
1285
  const normalizedCollection = collection == null ? null : String(collection);
1274
1286
  const normalizedOp = normalizeNativeMongoSuppressionOp(op);
1275
1287
  const now = Date.now();
1288
+ const matchingTokens: NativeMongoSuppression[] = [];
1276
1289
  for (let i = suppressions.length - 1; i >= 0; i -= 1) {
1277
1290
  const token = suppressions[i];
1278
1291
  if (!token || now - token.startedAt > 60000) {
@@ -1281,9 +1294,18 @@ function isNativeMongoSuppressed(collection: any, op: string): boolean {
1281
1294
  }
1282
1295
  if (token.collection && normalizedCollection && token.collection !== normalizedCollection) continue;
1283
1296
  if (token.ops && token.ops.size > 0 && !token.ops.has(normalizedOp)) continue;
1284
- return true;
1297
+ matchingTokens.push(token);
1285
1298
  }
1286
- return false;
1299
+ if (matchingTokens.length === 0) return false;
1300
+
1301
+ if (!isMongooseInternalNativeMongoCall()) {
1302
+ return false;
1303
+ }
1304
+
1305
+ for (const token of matchingTokens) {
1306
+ popNativeMongoSuppression(token);
1307
+ }
1308
+ return true;
1287
1309
  }
1288
1310
 
1289
1311
  function currentClockSkewMs(): number {
@@ -8,6 +8,7 @@ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
8
8
  const assert = require('assert');
9
9
  const { EventEmitter } = require('events');
10
10
  const mongodb = require('mongodb');
11
+ const mongoose = require('mongoose');
11
12
 
12
13
  const originalNativeMongoInsertOne = mongodb.Collection.prototype.insertOne;
13
14
 
@@ -18,7 +19,7 @@ mongodb.Collection.prototype.insertOne = async function reproFakeNativeMongoInse
18
19
  };
19
20
 
20
21
  const { flushIngestQueue } = require('../dist/ingest/client');
21
- const { reproMiddleware, __reproTestHooks } = require('../dist');
22
+ const { reproMiddleware, reproMongoosePlugin, __reproTestHooks } = require('../dist');
22
23
 
23
24
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
24
25
 
@@ -126,12 +127,80 @@ async function runCapturedRequest(cfg, sessionId, actionId, fn) {
126
127
  res.end();
127
128
  }
128
129
 
130
+ function execPre(hooks, name, context) {
131
+ return new Promise((resolve, reject) => {
132
+ hooks.execPre(name, context, [], (error) => (error ? reject(error) : resolve()));
133
+ });
134
+ }
135
+
129
136
  function dbChanges(events) {
130
137
  return events
131
138
  .filter((event) => event.event_type === 'db_change')
132
139
  .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
133
140
  }
134
141
 
142
+ async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
143
+ await withCapturedFetch(async (capturedBodies) => {
144
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
145
+ const sessionId = 'S_mongo_mixed_driver_insert';
146
+ const actionId = 'A_mongo_mixed_driver_insert';
147
+ const cfg = makeConfig(sessionId, actionId);
148
+
149
+ const schema = new mongoose.Schema({ subjectNumber: String, status: String });
150
+ reproMongoosePlugin(cfg)(schema);
151
+ const Model = mongoose.model(`ReproMixedNativeMongo${Date.now()}`, schema, 'subjects');
152
+ const pendingMongooseDoc = new Model({ subjectNumber: 'pre-save', status: 'pending' });
153
+
154
+ class FakeCollection {
155
+ constructor() {
156
+ this.collectionName = 'subjects';
157
+ this.dbName = 'clinical';
158
+ }
159
+
160
+ async insertOne(doc, options) {
161
+ assert.strictEqual(options.session.id, 'native-explicit-session-1');
162
+ doc._id = doc._id ?? 'subject-explicit-1';
163
+ return { acknowledged: true, insertedId: doc._id };
164
+ }
165
+ }
166
+
167
+ __reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
168
+ const subjectsCollection = new FakeCollection();
169
+
170
+ await runCapturedRequest(cfg, sessionId, actionId, async () => {
171
+ await execPre(schema.s.hooks, 'save', pendingMongooseDoc);
172
+ assert.strictEqual(__reproTestHooks.getNativeMongoSuppressionCountForTest(), 1);
173
+
174
+ const result = await subjectsCollection.insertOne(
175
+ { subjectNumber: '10013038', status: 'screened' },
176
+ {
177
+ session: {
178
+ id: 'native-explicit-session-1',
179
+ hasEnded: false,
180
+ inTransaction: () => true,
181
+ },
182
+ },
183
+ );
184
+ assert.deepStrictEqual(result, { acknowledged: true, insertedId: 'subject-explicit-1' });
185
+ });
186
+
187
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
188
+ await drainSdk();
189
+
190
+ const [change] = dbChanges(flattenEvents(capturedBodies));
191
+ assert.strictEqual(change.type, 'mongo');
192
+ assert.strictEqual(change.collection, 'subjects');
193
+ assert.strictEqual(change.op, 'insertOne');
194
+ assert.strictEqual(change.pk._id, 'subject-explicit-1');
195
+ assert.strictEqual(change.query.driver, 'mongodb');
196
+ assert.strictEqual(change.query.db, 'clinical');
197
+ assert.strictEqual(change.query.doc.subjectNumber, '10013038');
198
+ assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
199
+ assert.strictEqual(change.query.options.session.id, 'native-explicit-session-1');
200
+ assert.strictEqual(change.after.subjectNumber, '10013038');
201
+ });
202
+ }
203
+
135
204
  async function testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne() {
136
205
  await withCapturedFetch(async (capturedBodies) => {
137
206
  __reproTestHooks.resetSdkBackgroundQueuesForTest();
@@ -452,6 +521,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
452
521
  }
453
522
 
454
523
  (async () => {
524
+ await testMongooseSuppressionDoesNotHideExplicitNativeInsertOne();
455
525
  await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
456
526
  await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
457
527
  await testNativeInsertOneIsCaptured();
@@ -0,0 +1,158 @@
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 mongoose = require('mongoose');
11
+ const { GenericContainer, Wait } = require('testcontainers');
12
+
13
+ const { flushIngestQueue } = require('../dist/ingest/client');
14
+ const { reproMiddleware, reproMongoosePlugin, __reproTestHooks } = require('../dist');
15
+
16
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
17
+
18
+ async function waitFor(predicate, timeoutMs = 5000) {
19
+ const deadline = Date.now() + timeoutMs;
20
+ while (Date.now() < deadline) {
21
+ if (predicate()) return;
22
+ await sleep(25);
23
+ }
24
+ assert(predicate(), 'timed out waiting for expected ingest payload');
25
+ }
26
+
27
+ function flattenEvents(posts) {
28
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
29
+ }
30
+
31
+ function dbChanges(events) {
32
+ return events
33
+ .filter((event) => event.event_type === 'db_change')
34
+ .flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
35
+ }
36
+
37
+ function makeReqRes(sessionId, actionId) {
38
+ const req = new EventEmitter();
39
+ req.method = 'POST';
40
+ req.url = '/subjects';
41
+ req.originalUrl = '/subjects';
42
+ req.headers = {
43
+ 'x-bug-session-id': sessionId,
44
+ 'x-bug-action-id': actionId,
45
+ };
46
+ req.query = {};
47
+ req.params = {};
48
+
49
+ const res = new EventEmitter();
50
+ res.statusCode = 200;
51
+ res.getHeader = () => undefined;
52
+ res.getHeaders = () => ({});
53
+ res.write = () => true;
54
+ res.end = function end() {
55
+ this.emit('finish');
56
+ return this;
57
+ };
58
+
59
+ return { req, res };
60
+ }
61
+
62
+ async function runCapturedRequest(cfg, sessionId, actionId, fn) {
63
+ const middleware = reproMiddleware(cfg);
64
+ const { req, res } = makeReqRes(sessionId, actionId);
65
+ await new Promise((resolve, reject) => {
66
+ middleware(req, res, (err) => {
67
+ if (err) {
68
+ reject(err);
69
+ return;
70
+ }
71
+ Promise.resolve().then(fn).then(resolve, reject);
72
+ });
73
+ });
74
+ res.end();
75
+ }
76
+
77
+ async function startMongo() {
78
+ if (process.env.REPRO_MONGO_TEST_URI) {
79
+ return {
80
+ uri: process.env.REPRO_MONGO_TEST_URI,
81
+ stop: async () => {},
82
+ };
83
+ }
84
+
85
+ const container = await new GenericContainer('mongo:7')
86
+ .withExposedPorts(27017)
87
+ .withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
88
+ .withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
89
+ .withWaitStrategy(Wait.forListeningPorts())
90
+ .start();
91
+ return {
92
+ uri: `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_mongoose_native_suppression`,
93
+ stop: () => container.stop(),
94
+ };
95
+ }
96
+
97
+ async function main() {
98
+ const capturedBodies = [];
99
+ const originalFetch = global.fetch;
100
+ global.fetch = async (_url, init) => {
101
+ capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
102
+ return {
103
+ ok: true,
104
+ status: 202,
105
+ json: async () => ({ ok: true }),
106
+ text: async () => '{"ok":true}',
107
+ };
108
+ };
109
+
110
+ let container;
111
+ let connection;
112
+ try {
113
+ __reproTestHooks.resetSdkBackgroundQueuesForTest();
114
+ container = await startMongo();
115
+ connection = await mongoose.createConnection(container.uri).asPromise();
116
+
117
+ const cfg = {
118
+ tenantId: 'TENANT_test',
119
+ appId: 'APP_test',
120
+ appSecret: 'secret',
121
+ captureHeaders: false,
122
+ privacy: { environment: 'dev', captureRaw: true },
123
+ ingestBase: 'http://127.0.0.1:65535',
124
+ };
125
+
126
+ const schema = new mongoose.Schema({ subjectNumber: String, status: String });
127
+ reproMongoosePlugin(cfg)(schema);
128
+ const Subject = connection.model('Subject', schema, 'subjects');
129
+
130
+ await runCapturedRequest(cfg, 'S_mongoose_native_suppression', 'A_mongoose_native_suppression', async () => {
131
+ await Subject.create({ subjectNumber: '10013038', status: 'screened' });
132
+ });
133
+
134
+ await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
135
+ await __reproTestHooks.drainSdkBackgroundQueueForTest();
136
+ await flushIngestQueue();
137
+
138
+ const changes = dbChanges(flattenEvents(capturedBodies));
139
+ assert.strictEqual(changes.length, 1);
140
+ assert.strictEqual(changes[0].type, 'mongo');
141
+ assert.strictEqual(changes[0].collection, 'subjects');
142
+ assert.strictEqual(changes[0].op, 'insert');
143
+ assert.strictEqual(changes[0].after.subjectNumber, '10013038');
144
+
145
+ console.log('mongodb mongoose native suppression testcontainers OK');
146
+ } finally {
147
+ await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
148
+ await flushIngestQueue().catch(() => {});
149
+ global.fetch = originalFetch;
150
+ if (connection) await connection.close().catch(() => {});
151
+ if (container) await container.stop().catch(() => {});
152
+ }
153
+ }
154
+
155
+ main().catch((error) => {
156
+ console.error(error);
157
+ process.exit(1);
158
+ });