@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
|
-
|
|
902
|
+
matchingTokens.push(token);
|
|
890
903
|
}
|
|
891
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
});
|