@reproapp/node-sdk 0.0.32 → 0.0.34
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
|
@@ -824,6 +824,19 @@ const getCtx = () => als.getStore() || {};
|
|
|
824
824
|
function normalizeNativeMongoSuppressionOp(op) {
|
|
825
825
|
return String(op ?? '').trim();
|
|
826
826
|
}
|
|
827
|
+
const NATIVE_MONGO_WRITE_OPS = new Set([
|
|
828
|
+
'insertOne',
|
|
829
|
+
'insertMany',
|
|
830
|
+
'updateOne',
|
|
831
|
+
'updateMany',
|
|
832
|
+
'replaceOne',
|
|
833
|
+
'deleteOne',
|
|
834
|
+
'deleteMany',
|
|
835
|
+
'bulkWrite',
|
|
836
|
+
'findOneAndUpdate',
|
|
837
|
+
'findOneAndDelete',
|
|
838
|
+
'findOneAndReplace',
|
|
839
|
+
]);
|
|
827
840
|
function pushNativeMongoSuppression(collection, ops) {
|
|
828
841
|
const store = als.getStore();
|
|
829
842
|
if (!store)
|
|
@@ -869,13 +882,28 @@ function releaseNativeMongoSuppression(target) {
|
|
|
869
882
|
catch { }
|
|
870
883
|
}
|
|
871
884
|
}
|
|
885
|
+
function isMongooseInternalNativeMongoCall() {
|
|
886
|
+
let stack = '';
|
|
887
|
+
try {
|
|
888
|
+
stack = new Error().stack ?? '';
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
stack = '';
|
|
892
|
+
}
|
|
893
|
+
return stack.includes('/node_modules/mongoose/') ||
|
|
894
|
+
stack.includes('\\node_modules\\mongoose\\') ||
|
|
895
|
+
stack.includes('mongoose/lib/');
|
|
896
|
+
}
|
|
872
897
|
function isNativeMongoSuppressed(collection, op) {
|
|
873
898
|
const suppressions = getCtx().nativeMongoSuppressions;
|
|
874
899
|
if (!Array.isArray(suppressions) || suppressions.length === 0)
|
|
875
900
|
return false;
|
|
876
901
|
const normalizedCollection = collection == null ? null : String(collection);
|
|
877
902
|
const normalizedOp = normalizeNativeMongoSuppressionOp(op);
|
|
903
|
+
if (NATIVE_MONGO_WRITE_OPS.has(normalizedOp))
|
|
904
|
+
return false;
|
|
878
905
|
const now = Date.now();
|
|
906
|
+
const matchingTokens = [];
|
|
879
907
|
for (let i = suppressions.length - 1; i >= 0; i -= 1) {
|
|
880
908
|
const token = suppressions[i];
|
|
881
909
|
if (!token || now - token.startedAt > 60000) {
|
|
@@ -886,9 +914,17 @@ function isNativeMongoSuppressed(collection, op) {
|
|
|
886
914
|
continue;
|
|
887
915
|
if (token.ops && token.ops.size > 0 && !token.ops.has(normalizedOp))
|
|
888
916
|
continue;
|
|
889
|
-
|
|
917
|
+
matchingTokens.push(token);
|
|
890
918
|
}
|
|
891
|
-
|
|
919
|
+
if (matchingTokens.length === 0)
|
|
920
|
+
return false;
|
|
921
|
+
if (!isMongooseInternalNativeMongoCall()) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
for (const token of matchingTokens) {
|
|
925
|
+
popNativeMongoSuppression(token);
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
892
928
|
}
|
|
893
929
|
function currentClockSkewMs() {
|
|
894
930
|
const store = als.getStore();
|
|
@@ -3878,7 +3914,7 @@ function estimateTraceBatchSerializedBytes(batch, requestRid, actionId, batchInd
|
|
|
3878
3914
|
function chunkTraceEventsForTransport(events, requestRid, actionId) {
|
|
3879
3915
|
if (!Array.isArray(events) || events.length === 0)
|
|
3880
3916
|
return [];
|
|
3881
|
-
const boundedEvents = events
|
|
3917
|
+
const boundedEvents = boundTraceEventsForTransport(events);
|
|
3882
3918
|
const countBatches = chunkArray(boundedEvents, TRACE_BATCH_SIZE);
|
|
3883
3919
|
const sizedBatches = [];
|
|
3884
3920
|
let current = [];
|
|
@@ -3907,6 +3943,13 @@ function chunkTraceEventsForTransport(events, requestRid, actionId) {
|
|
|
3907
3943
|
}
|
|
3908
3944
|
return sizedBatches;
|
|
3909
3945
|
}
|
|
3946
|
+
function boundTraceEventsForTransport(events) {
|
|
3947
|
+
if (events.length <= TRACE_MAX_EVENTS_PER_REQUEST)
|
|
3948
|
+
return events;
|
|
3949
|
+
const headCount = Math.floor(TRACE_MAX_EVENTS_PER_REQUEST / 2);
|
|
3950
|
+
const tailCount = TRACE_MAX_EVENTS_PER_REQUEST - headCount;
|
|
3951
|
+
return events.slice(0, headCount).concat(events.slice(events.length - tailCount));
|
|
3952
|
+
}
|
|
3910
3953
|
function createCapturedValueEntry(params) {
|
|
3911
3954
|
if (!__FULL_VALUE_CAPTURE_ENABLED)
|
|
3912
3955
|
return undefined;
|
|
@@ -5154,6 +5197,9 @@ function emitDbQuery(cfg, sid, aid, payload) {
|
|
|
5154
5197
|
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
5155
5198
|
if (!shouldCaptureDbSpan(spanContext))
|
|
5156
5199
|
return;
|
|
5200
|
+
if (ctx.captureScope) {
|
|
5201
|
+
ctx.captureScope.dbChangeCount += 1;
|
|
5202
|
+
}
|
|
5157
5203
|
const runCtx = {
|
|
5158
5204
|
sid: ctx.sid,
|
|
5159
5205
|
aid: ctx.aid,
|
|
@@ -5281,9 +5327,6 @@ async function emitDbQueryAsync(cfg, sid, aid, payload) {
|
|
|
5281
5327
|
t: payload.t,
|
|
5282
5328
|
}]
|
|
5283
5329
|
});
|
|
5284
|
-
if (enqueued && ctx.captureScope) {
|
|
5285
|
-
ctx.captureScope.dbChangeCount += 1;
|
|
5286
|
-
}
|
|
5287
5330
|
}
|
|
5288
5331
|
function buildMinimalUpdate(before, after) {
|
|
5289
5332
|
const set = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
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
|
@@ -1227,6 +1227,20 @@ function normalizeNativeMongoSuppressionOp(op: string | null | undefined): strin
|
|
|
1227
1227
|
return String(op ?? '').trim();
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
const NATIVE_MONGO_WRITE_OPS = new Set([
|
|
1231
|
+
'insertOne',
|
|
1232
|
+
'insertMany',
|
|
1233
|
+
'updateOne',
|
|
1234
|
+
'updateMany',
|
|
1235
|
+
'replaceOne',
|
|
1236
|
+
'deleteOne',
|
|
1237
|
+
'deleteMany',
|
|
1238
|
+
'bulkWrite',
|
|
1239
|
+
'findOneAndUpdate',
|
|
1240
|
+
'findOneAndDelete',
|
|
1241
|
+
'findOneAndReplace',
|
|
1242
|
+
]);
|
|
1243
|
+
|
|
1230
1244
|
function pushNativeMongoSuppression(collection: any, ops?: string[] | readonly string[]): NativeMongoSuppression | null {
|
|
1231
1245
|
const store = als.getStore();
|
|
1232
1246
|
if (!store) return null;
|
|
@@ -1267,12 +1281,26 @@ function releaseNativeMongoSuppression(target: any): void {
|
|
|
1267
1281
|
}
|
|
1268
1282
|
}
|
|
1269
1283
|
|
|
1284
|
+
function isMongooseInternalNativeMongoCall(): boolean {
|
|
1285
|
+
let stack = '';
|
|
1286
|
+
try {
|
|
1287
|
+
stack = new Error().stack ?? '';
|
|
1288
|
+
} catch {
|
|
1289
|
+
stack = '';
|
|
1290
|
+
}
|
|
1291
|
+
return stack.includes('/node_modules/mongoose/') ||
|
|
1292
|
+
stack.includes('\\node_modules\\mongoose\\') ||
|
|
1293
|
+
stack.includes('mongoose/lib/');
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1270
1296
|
function isNativeMongoSuppressed(collection: any, op: string): boolean {
|
|
1271
1297
|
const suppressions = (getCtx() as Ctx).nativeMongoSuppressions;
|
|
1272
1298
|
if (!Array.isArray(suppressions) || suppressions.length === 0) return false;
|
|
1273
1299
|
const normalizedCollection = collection == null ? null : String(collection);
|
|
1274
1300
|
const normalizedOp = normalizeNativeMongoSuppressionOp(op);
|
|
1301
|
+
if (NATIVE_MONGO_WRITE_OPS.has(normalizedOp)) return false;
|
|
1275
1302
|
const now = Date.now();
|
|
1303
|
+
const matchingTokens: NativeMongoSuppression[] = [];
|
|
1276
1304
|
for (let i = suppressions.length - 1; i >= 0; i -= 1) {
|
|
1277
1305
|
const token = suppressions[i];
|
|
1278
1306
|
if (!token || now - token.startedAt > 60000) {
|
|
@@ -1281,9 +1309,18 @@ function isNativeMongoSuppressed(collection: any, op: string): boolean {
|
|
|
1281
1309
|
}
|
|
1282
1310
|
if (token.collection && normalizedCollection && token.collection !== normalizedCollection) continue;
|
|
1283
1311
|
if (token.ops && token.ops.size > 0 && !token.ops.has(normalizedOp)) continue;
|
|
1284
|
-
|
|
1312
|
+
matchingTokens.push(token);
|
|
1285
1313
|
}
|
|
1286
|
-
return false;
|
|
1314
|
+
if (matchingTokens.length === 0) return false;
|
|
1315
|
+
|
|
1316
|
+
if (!isMongooseInternalNativeMongoCall()) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
for (const token of matchingTokens) {
|
|
1321
|
+
popNativeMongoSuppression(token);
|
|
1322
|
+
}
|
|
1323
|
+
return true;
|
|
1287
1324
|
}
|
|
1288
1325
|
|
|
1289
1326
|
function currentClockSkewMs(): number {
|
|
@@ -4605,7 +4642,7 @@ function chunkTraceEventsForTransport(
|
|
|
4605
4642
|
): TraceEventRecord[][] {
|
|
4606
4643
|
if (!Array.isArray(events) || events.length === 0) return [];
|
|
4607
4644
|
|
|
4608
|
-
const boundedEvents = events
|
|
4645
|
+
const boundedEvents = boundTraceEventsForTransport(events);
|
|
4609
4646
|
const countBatches = chunkArray(boundedEvents, TRACE_BATCH_SIZE);
|
|
4610
4647
|
const sizedBatches: TraceEventRecord[][] = [];
|
|
4611
4648
|
let current: TraceEventRecord[] = [];
|
|
@@ -4649,6 +4686,13 @@ function chunkTraceEventsForTransport(
|
|
|
4649
4686
|
return sizedBatches;
|
|
4650
4687
|
}
|
|
4651
4688
|
|
|
4689
|
+
function boundTraceEventsForTransport(events: TraceEventRecord[]): TraceEventRecord[] {
|
|
4690
|
+
if (events.length <= TRACE_MAX_EVENTS_PER_REQUEST) return events;
|
|
4691
|
+
const headCount = Math.floor(TRACE_MAX_EVENTS_PER_REQUEST / 2);
|
|
4692
|
+
const tailCount = TRACE_MAX_EVENTS_PER_REQUEST - headCount;
|
|
4693
|
+
return events.slice(0, headCount).concat(events.slice(events.length - tailCount));
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4652
4696
|
function createCapturedValueEntry(
|
|
4653
4697
|
params: {
|
|
4654
4698
|
target:
|
|
@@ -6110,6 +6154,9 @@ function emitDbQuery(cfg: any, sid?: string, aid?: string, payload?: any) {
|
|
|
6110
6154
|
const ctx = getCtx() as Ctx;
|
|
6111
6155
|
const spanContext = payload?.spanContext ?? captureSpanContextFromTracer();
|
|
6112
6156
|
if (!shouldCaptureDbSpan(spanContext)) return;
|
|
6157
|
+
if (ctx.captureScope) {
|
|
6158
|
+
ctx.captureScope.dbChangeCount += 1;
|
|
6159
|
+
}
|
|
6113
6160
|
const runCtx: Ctx = {
|
|
6114
6161
|
sid: ctx.sid,
|
|
6115
6162
|
aid: ctx.aid,
|
|
@@ -6236,9 +6283,6 @@ async function emitDbQueryAsync(cfg: any, sid?: string, aid?: string, payload?:
|
|
|
6236
6283
|
t: payload.t,
|
|
6237
6284
|
}]
|
|
6238
6285
|
});
|
|
6239
|
-
if (enqueued && ctx.captureScope) {
|
|
6240
|
-
ctx.captureScope.dbChangeCount += 1;
|
|
6241
|
-
}
|
|
6242
6286
|
}
|
|
6243
6287
|
|
|
6244
6288
|
function buildMinimalUpdate(before: any, after: any) {
|
|
@@ -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,90 @@ 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
|
+
function requestCaptureManifests(events) {
|
|
143
|
+
return events
|
|
144
|
+
.filter((event) => event.event_type === 'request_capture_manifest')
|
|
145
|
+
.map((event) => event.payload?.requestCaptureManifest ?? event.payload)
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function testMongooseSuppressionDoesNotHideExplicitNativeInsertOne() {
|
|
150
|
+
await withCapturedFetch(async (capturedBodies) => {
|
|
151
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
152
|
+
const sessionId = 'S_mongo_mixed_driver_insert';
|
|
153
|
+
const actionId = 'A_mongo_mixed_driver_insert';
|
|
154
|
+
const cfg = makeConfig(sessionId, actionId);
|
|
155
|
+
|
|
156
|
+
const schema = new mongoose.Schema({ subjectNumber: String, status: String });
|
|
157
|
+
reproMongoosePlugin(cfg)(schema);
|
|
158
|
+
const Model = mongoose.model(`ReproMixedNativeMongo${Date.now()}`, schema, 'subjects');
|
|
159
|
+
const pendingMongooseDoc = new Model({ subjectNumber: 'pre-save', status: 'pending' });
|
|
160
|
+
|
|
161
|
+
class FakeCollection {
|
|
162
|
+
constructor() {
|
|
163
|
+
this.collectionName = 'subjects';
|
|
164
|
+
this.dbName = 'clinical';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async insertOne(doc, options) {
|
|
168
|
+
assert.strictEqual(options.session.id, 'native-explicit-session-1');
|
|
169
|
+
doc._id = doc._id ?? 'subject-explicit-1';
|
|
170
|
+
return { acknowledged: true, insertedId: doc._id };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
__reproTestHooks.patchMongoCollectionPrototypeForTest(FakeCollection.prototype, cfg);
|
|
175
|
+
const subjectsCollection = new FakeCollection();
|
|
176
|
+
|
|
177
|
+
await runCapturedRequest(cfg, sessionId, actionId, async () => {
|
|
178
|
+
await execPre(schema.s.hooks, 'save', pendingMongooseDoc);
|
|
179
|
+
assert.strictEqual(__reproTestHooks.getNativeMongoSuppressionCountForTest(), 1);
|
|
180
|
+
|
|
181
|
+
const result = await subjectsCollection.insertOne(
|
|
182
|
+
{ subjectNumber: '10013038', status: 'screened' },
|
|
183
|
+
{
|
|
184
|
+
session: {
|
|
185
|
+
id: 'native-explicit-session-1',
|
|
186
|
+
hasEnded: false,
|
|
187
|
+
inTransaction: () => true,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
assert.deepStrictEqual(result, { acknowledged: true, insertedId: 'subject-explicit-1' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
195
|
+
await waitFor(() => requestCaptureManifests(flattenEvents(capturedBodies)).length === 1);
|
|
196
|
+
await drainSdk();
|
|
197
|
+
|
|
198
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
199
|
+
const [manifest] = requestCaptureManifests(flattenEvents(capturedBodies));
|
|
200
|
+
assert.strictEqual(change.type, 'mongo');
|
|
201
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
202
|
+
assert.strictEqual(change.op, 'insertOne');
|
|
203
|
+
assert.strictEqual(change.pk._id, 'subject-explicit-1');
|
|
204
|
+
assert.strictEqual(change.query.driver, 'mongodb');
|
|
205
|
+
assert.strictEqual(change.query.db, 'clinical');
|
|
206
|
+
assert.strictEqual(change.query.doc.subjectNumber, '10013038');
|
|
207
|
+
assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
|
|
208
|
+
assert.strictEqual(change.query.options.session.id, 'native-explicit-session-1');
|
|
209
|
+
assert.strictEqual(change.after.subjectNumber, '10013038');
|
|
210
|
+
assert.strictEqual(manifest.expected.dbChange, 1);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
135
214
|
async function testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne() {
|
|
136
215
|
await withCapturedFetch(async (capturedBodies) => {
|
|
137
216
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -452,6 +531,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
|
|
|
452
531
|
}
|
|
453
532
|
|
|
454
533
|
(async () => {
|
|
534
|
+
await testMongooseSuppressionDoesNotHideExplicitNativeInsertOne();
|
|
455
535
|
await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
|
|
456
536
|
await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
|
|
457
537
|
await testNativeInsertOneIsCaptured();
|
|
@@ -0,0 +1,178 @@
|
|
|
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 requestCaptureManifests(events) {
|
|
38
|
+
return events
|
|
39
|
+
.filter((event) => event.event_type === 'request_capture_manifest')
|
|
40
|
+
.map((event) => event.payload?.requestCaptureManifest ?? event.payload)
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeReqRes(sessionId, actionId) {
|
|
45
|
+
const req = new EventEmitter();
|
|
46
|
+
req.method = 'POST';
|
|
47
|
+
req.url = '/subjects';
|
|
48
|
+
req.originalUrl = '/subjects';
|
|
49
|
+
req.headers = {
|
|
50
|
+
'x-bug-session-id': sessionId,
|
|
51
|
+
'x-bug-action-id': actionId,
|
|
52
|
+
};
|
|
53
|
+
req.query = {};
|
|
54
|
+
req.params = {};
|
|
55
|
+
|
|
56
|
+
const res = new EventEmitter();
|
|
57
|
+
res.statusCode = 200;
|
|
58
|
+
res.getHeader = () => undefined;
|
|
59
|
+
res.getHeaders = () => ({});
|
|
60
|
+
res.write = () => true;
|
|
61
|
+
res.end = function end() {
|
|
62
|
+
this.emit('finish');
|
|
63
|
+
return this;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { req, res };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runCapturedRequest(cfg, sessionId, actionId, fn) {
|
|
70
|
+
const middleware = reproMiddleware(cfg);
|
|
71
|
+
const { req, res } = makeReqRes(sessionId, actionId);
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
middleware(req, res, (err) => {
|
|
74
|
+
if (err) {
|
|
75
|
+
reject(err);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
Promise.resolve().then(fn).then(resolve, reject);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
res.end();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function startMongo() {
|
|
85
|
+
if (process.env.REPRO_MONGO_TEST_URI) {
|
|
86
|
+
return {
|
|
87
|
+
uri: process.env.REPRO_MONGO_TEST_URI,
|
|
88
|
+
stop: async () => {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const container = await new GenericContainer('mongo:7')
|
|
93
|
+
.withExposedPorts(27017)
|
|
94
|
+
.withTmpFs({ '/data/db': 'rw,noexec,nosuid,size=1g' })
|
|
95
|
+
.withCommand(['mongod', '--bind_ip_all', '--setParameter', 'diagnosticDataCollectionEnabled=false'])
|
|
96
|
+
.withWaitStrategy(Wait.forListeningPorts())
|
|
97
|
+
.start();
|
|
98
|
+
return {
|
|
99
|
+
uri: `mongodb://${container.getHost()}:${container.getMappedPort(27017)}/repro_mongoose_native_suppression`,
|
|
100
|
+
stop: () => container.stop(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
const capturedBodies = [];
|
|
106
|
+
const originalFetch = global.fetch;
|
|
107
|
+
global.fetch = async (_url, init) => {
|
|
108
|
+
capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
status: 202,
|
|
112
|
+
json: async () => ({ ok: true }),
|
|
113
|
+
text: async () => '{"ok":true}',
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
let container;
|
|
118
|
+
let connection;
|
|
119
|
+
try {
|
|
120
|
+
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
121
|
+
container = await startMongo();
|
|
122
|
+
connection = await mongoose.createConnection(container.uri).asPromise();
|
|
123
|
+
|
|
124
|
+
const cfg = {
|
|
125
|
+
tenantId: 'TENANT_test',
|
|
126
|
+
appId: 'APP_test',
|
|
127
|
+
appSecret: 'secret',
|
|
128
|
+
captureHeaders: false,
|
|
129
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
130
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const schema = new mongoose.Schema({ subjectNumber: String, status: String });
|
|
134
|
+
reproMongoosePlugin(cfg)(schema);
|
|
135
|
+
const Subject = connection.model('Subject', schema, 'subjects');
|
|
136
|
+
|
|
137
|
+
await runCapturedRequest(cfg, 'S_mongoose_native_suppression', 'A_mongoose_native_suppression', async () => {
|
|
138
|
+
await Subject.create({ subjectNumber: '10013038', status: 'screened' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length >= 2);
|
|
142
|
+
await waitFor(() => requestCaptureManifests(flattenEvents(capturedBodies)).length === 1);
|
|
143
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
144
|
+
await flushIngestQueue();
|
|
145
|
+
|
|
146
|
+
const changes = dbChanges(flattenEvents(capturedBodies));
|
|
147
|
+
const manifest = requestCaptureManifests(flattenEvents(capturedBodies))[0];
|
|
148
|
+
const mongooseWrite = changes.find((change) => (
|
|
149
|
+
change.type === 'mongo' &&
|
|
150
|
+
change.collection === 'subjects' &&
|
|
151
|
+
change.op === 'insert' &&
|
|
152
|
+
change.after?.subjectNumber === '10013038'
|
|
153
|
+
));
|
|
154
|
+
const nativeWrite = changes.find((change) => (
|
|
155
|
+
change.type === 'mongo' &&
|
|
156
|
+
change.collection === 'subjects' &&
|
|
157
|
+
change.op === 'insertOne' &&
|
|
158
|
+
change.query?.driver === 'mongodb' &&
|
|
159
|
+
change.after?.subjectNumber === '10013038'
|
|
160
|
+
));
|
|
161
|
+
assert.ok(mongooseWrite, 'expected mongoose-level insert evidence');
|
|
162
|
+
assert.ok(nativeWrite, 'expected native mongodb insertOne evidence');
|
|
163
|
+
assert.ok(manifest.expected.dbChange >= 2);
|
|
164
|
+
|
|
165
|
+
console.log('mongodb mongoose native suppression testcontainers OK');
|
|
166
|
+
} finally {
|
|
167
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest().catch(() => {});
|
|
168
|
+
await flushIngestQueue().catch(() => {});
|
|
169
|
+
global.fetch = originalFetch;
|
|
170
|
+
if (connection) await connection.close().catch(() => {});
|
|
171
|
+
if (container) await container.stop().catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main().catch((error) => {
|
|
176
|
+
console.error(error);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
|
@@ -66,6 +66,11 @@ function main() {
|
|
|
66
66
|
cappedEventCount <= 1000,
|
|
67
67
|
`expected trace transport to cap events per request, got ${cappedEventCount}`,
|
|
68
68
|
);
|
|
69
|
+
const cappedEvents = cappedBatches.flat();
|
|
70
|
+
assert.strictEqual(cappedEvents[0].fn, 'fn0');
|
|
71
|
+
assert.strictEqual(cappedEvents[499].fn, 'fn499');
|
|
72
|
+
assert.strictEqual(cappedEvents[500].fn, 'fn1000');
|
|
73
|
+
assert.strictEqual(cappedEvents[cappedEvents.length - 1].fn, 'fn1499');
|
|
69
74
|
|
|
70
75
|
// eslint-disable-next-line no-console
|
|
71
76
|
console.log('trace batch size chunking OK');
|