@reproapp/node-sdk 0.0.28 → 0.0.30
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 +238 -26
- package/package.json +2 -2
- package/src/index.ts +221 -18
- package/test/init-auto-http-server.test.js +170 -0
- package/test/mongodb-integration.test.js +78 -0
package/dist/index.js
CHANGED
|
@@ -295,6 +295,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
|
|
|
295
295
|
const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
|
|
296
296
|
const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
|
|
297
297
|
const REPRO_QUERY_START_PARAM = '__repro_start';
|
|
298
|
+
const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
|
|
298
299
|
const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
|
|
299
300
|
const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
|
|
300
301
|
const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
@@ -1028,10 +1029,6 @@ function scheduleTraceMaterializerIdleShutdown() {
|
|
|
1028
1029
|
catch { }
|
|
1029
1030
|
}
|
|
1030
1031
|
}, TRACE_MATERIALIZE_WORKER_IDLE_SHUTDOWN_MS);
|
|
1031
|
-
try {
|
|
1032
|
-
traceMaterializerIdleTimer.unref?.();
|
|
1033
|
-
}
|
|
1034
|
-
catch { }
|
|
1035
1032
|
}
|
|
1036
1033
|
function rejectTraceMaterializerPending(error) {
|
|
1037
1034
|
for (const [taskId, pending] of traceMaterializerPending.entries()) {
|
|
@@ -1373,11 +1370,43 @@ function waitForSessionDrain(sessionId) {
|
|
|
1373
1370
|
if (remaining <= 0)
|
|
1374
1371
|
return Promise.resolve();
|
|
1375
1372
|
return new Promise(resolve => {
|
|
1373
|
+
let settled = false;
|
|
1374
|
+
let timeout = null;
|
|
1375
|
+
const removeWaiter = (waiter) => {
|
|
1376
|
+
const list = sessionWaiters.get(sessionId);
|
|
1377
|
+
if (!list)
|
|
1378
|
+
return;
|
|
1379
|
+
const next = list.filter(fn => fn !== waiter);
|
|
1380
|
+
if (next.length) {
|
|
1381
|
+
sessionWaiters.set(sessionId, next);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
sessionWaiters.delete(sessionId);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
const done = () => {
|
|
1388
|
+
if (settled)
|
|
1389
|
+
return;
|
|
1390
|
+
settled = true;
|
|
1391
|
+
if (timeout) {
|
|
1392
|
+
try {
|
|
1393
|
+
clearTimeout(timeout);
|
|
1394
|
+
}
|
|
1395
|
+
catch { }
|
|
1396
|
+
timeout = null;
|
|
1397
|
+
}
|
|
1398
|
+
removeWaiter(done);
|
|
1399
|
+
resolve();
|
|
1400
|
+
};
|
|
1376
1401
|
const list = sessionWaiters.get(sessionId) || [];
|
|
1377
|
-
list.push(
|
|
1402
|
+
list.push(done);
|
|
1378
1403
|
sessionWaiters.set(sessionId, list);
|
|
1379
1404
|
if (SESSION_DRAIN_TIMEOUT_MS > 0 && Number.isFinite(SESSION_DRAIN_TIMEOUT_MS)) {
|
|
1380
|
-
setTimeout(
|
|
1405
|
+
timeout = setTimeout(done, SESSION_DRAIN_TIMEOUT_MS);
|
|
1406
|
+
try {
|
|
1407
|
+
timeout.unref?.();
|
|
1408
|
+
}
|
|
1409
|
+
catch { }
|
|
1381
1410
|
}
|
|
1382
1411
|
});
|
|
1383
1412
|
}
|
|
@@ -3999,6 +4028,22 @@ function reproMiddleware(cfg) {
|
|
|
3999
4028
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4000
4029
|
patchMongoDb(cfg);
|
|
4001
4030
|
return async function (req, res, next) {
|
|
4031
|
+
const reqAny = req;
|
|
4032
|
+
if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
|
|
4033
|
+
return next();
|
|
4034
|
+
}
|
|
4035
|
+
try {
|
|
4036
|
+
Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
|
|
4037
|
+
value: true,
|
|
4038
|
+
configurable: true,
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
catch {
|
|
4042
|
+
try {
|
|
4043
|
+
reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true;
|
|
4044
|
+
}
|
|
4045
|
+
catch { }
|
|
4046
|
+
}
|
|
4002
4047
|
const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
|
|
4003
4048
|
const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
|
|
4004
4049
|
const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
|
|
@@ -4035,30 +4080,50 @@ function reproMiddleware(cfg) {
|
|
|
4035
4080
|
const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
4036
4081
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
4037
4082
|
beginSessionRequest(sid);
|
|
4038
|
-
// ---- response body capture
|
|
4083
|
+
// ---- response body capture ----
|
|
4039
4084
|
let capturedBody = undefined;
|
|
4040
|
-
const
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4085
|
+
const originalJson = res.json;
|
|
4086
|
+
if (typeof originalJson === 'function') {
|
|
4087
|
+
const origJson = originalJson.bind(res);
|
|
4088
|
+
res.json = (body) => { capturedBody = body; return origJson(body); };
|
|
4089
|
+
}
|
|
4090
|
+
const originalSend = res.send;
|
|
4091
|
+
if (typeof originalSend === 'function') {
|
|
4092
|
+
const origSend = originalSend.bind(res);
|
|
4093
|
+
res.send = (body) => {
|
|
4094
|
+
if (capturedBody === undefined) {
|
|
4095
|
+
capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
|
|
4096
|
+
}
|
|
4097
|
+
return origSend(body);
|
|
4098
|
+
};
|
|
4099
|
+
}
|
|
4100
|
+
const origWrite = typeof res.write === 'function'
|
|
4101
|
+
? res.write.bind(res)
|
|
4102
|
+
: null;
|
|
4103
|
+
const origEnd = typeof res.end === 'function'
|
|
4104
|
+
? res.end.bind(res)
|
|
4105
|
+
: null;
|
|
4051
4106
|
const chunks = [];
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4107
|
+
if (origWrite) {
|
|
4108
|
+
res.write = (chunk, ...args) => {
|
|
4109
|
+
try {
|
|
4110
|
+
if (chunk != null)
|
|
4111
|
+
chunks.push(chunk);
|
|
4112
|
+
}
|
|
4113
|
+
catch { }
|
|
4114
|
+
return origWrite(chunk, ...args);
|
|
4115
|
+
};
|
|
4055
4116
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4117
|
+
if (origEnd) {
|
|
4118
|
+
res.end = (chunk, ...args) => {
|
|
4119
|
+
try {
|
|
4120
|
+
if (chunk != null)
|
|
4121
|
+
chunks.push(chunk);
|
|
4122
|
+
}
|
|
4123
|
+
catch { }
|
|
4124
|
+
return origEnd(chunk, ...args);
|
|
4125
|
+
};
|
|
4060
4126
|
}
|
|
4061
|
-
catch { } return origEnd(chunk, ...args); };
|
|
4062
4127
|
// ---- our ALS (unchanged) ----
|
|
4063
4128
|
const tracerApiWithTrace = __TRACER__;
|
|
4064
4129
|
const runInTrace = (fn) => {
|
|
@@ -4546,6 +4611,7 @@ function nativeMongoOpsForMongooseOp(op) {
|
|
|
4546
4611
|
return [op];
|
|
4547
4612
|
}
|
|
4548
4613
|
function reproMongoosePlugin(cfg) {
|
|
4614
|
+
patchMongoDb(cfg);
|
|
4549
4615
|
return function (schema) {
|
|
4550
4616
|
// -------- pre/post save (unchanged) --------
|
|
4551
4617
|
schema.pre('save', { document: true }, function (next) {
|
|
@@ -7942,6 +8008,50 @@ function patchMongoCollectionPrototype(proto, cfg) {
|
|
|
7942
8008
|
patchMongoCollectionMethod(proto, op, true, cfg);
|
|
7943
8009
|
}
|
|
7944
8010
|
}
|
|
8011
|
+
function patchMongoCollectionInstance(collection, cfg) {
|
|
8012
|
+
if (!collection || typeof collection !== 'object')
|
|
8013
|
+
return collection;
|
|
8014
|
+
patchMongoCollectionPrototype(Object.getPrototypeOf(collection), cfg);
|
|
8015
|
+
patchMongoCollectionPrototype(collection, cfg);
|
|
8016
|
+
return collection;
|
|
8017
|
+
}
|
|
8018
|
+
function patchMongoDbPrototype(proto, cfg) {
|
|
8019
|
+
if (!proto)
|
|
8020
|
+
return;
|
|
8021
|
+
const marker = '__repro_mongodb_db_collection_patched';
|
|
8022
|
+
if (proto[marker])
|
|
8023
|
+
return;
|
|
8024
|
+
const original = proto.collection;
|
|
8025
|
+
if (typeof original !== 'function')
|
|
8026
|
+
return;
|
|
8027
|
+
proto[marker] = true;
|
|
8028
|
+
proto.collection = function reproPatchedMongoDbCollection(...args) {
|
|
8029
|
+
const collection = original.apply(this, args);
|
|
8030
|
+
return patchMongoCollectionInstance(collection, cfg);
|
|
8031
|
+
};
|
|
8032
|
+
}
|
|
8033
|
+
function patchMongoDbInstance(db, cfg) {
|
|
8034
|
+
if (!db || typeof db !== 'object')
|
|
8035
|
+
return db;
|
|
8036
|
+
patchMongoDbPrototype(Object.getPrototypeOf(db), cfg);
|
|
8037
|
+
patchMongoDbPrototype(db, cfg);
|
|
8038
|
+
return db;
|
|
8039
|
+
}
|
|
8040
|
+
function patchMongoClientPrototype(proto, cfg) {
|
|
8041
|
+
if (!proto)
|
|
8042
|
+
return;
|
|
8043
|
+
const marker = '__repro_mongodb_client_db_patched';
|
|
8044
|
+
if (proto[marker])
|
|
8045
|
+
return;
|
|
8046
|
+
const original = proto.db;
|
|
8047
|
+
if (typeof original !== 'function')
|
|
8048
|
+
return;
|
|
8049
|
+
proto[marker] = true;
|
|
8050
|
+
proto.db = function reproPatchedMongoClientDb(...args) {
|
|
8051
|
+
const db = original.apply(this, args);
|
|
8052
|
+
return patchMongoDbInstance(db, cfg);
|
|
8053
|
+
};
|
|
8054
|
+
}
|
|
7945
8055
|
function patchMongoDbFromModuleExports(mongoModule, cfg) {
|
|
7946
8056
|
if (!mongoModule)
|
|
7947
8057
|
return;
|
|
@@ -7956,6 +8066,28 @@ function patchMongoDbFromModuleExports(mongoModule, cfg) {
|
|
|
7956
8066
|
for (const ctor of constructors) {
|
|
7957
8067
|
patchMongoCollectionPrototype(ctor.prototype, cfg);
|
|
7958
8068
|
}
|
|
8069
|
+
const dbConstructors = new Set();
|
|
8070
|
+
const clientConstructors = new Set();
|
|
8071
|
+
const addDbCtor = (candidate) => {
|
|
8072
|
+
if (typeof candidate === 'function' && candidate.prototype) {
|
|
8073
|
+
dbConstructors.add(candidate);
|
|
8074
|
+
}
|
|
8075
|
+
};
|
|
8076
|
+
const addClientCtor = (candidate) => {
|
|
8077
|
+
if (typeof candidate === 'function' && candidate.prototype) {
|
|
8078
|
+
clientConstructors.add(candidate);
|
|
8079
|
+
}
|
|
8080
|
+
};
|
|
8081
|
+
addDbCtor(mongoModule?.Db);
|
|
8082
|
+
addDbCtor(mongoModule?.default?.Db);
|
|
8083
|
+
addClientCtor(mongoModule?.MongoClient);
|
|
8084
|
+
addClientCtor(mongoModule?.default?.MongoClient);
|
|
8085
|
+
for (const ctor of dbConstructors) {
|
|
8086
|
+
patchMongoDbPrototype(ctor.prototype, cfg);
|
|
8087
|
+
}
|
|
8088
|
+
for (const ctor of clientConstructors) {
|
|
8089
|
+
patchMongoClientPrototype(ctor.prototype, cfg);
|
|
8090
|
+
}
|
|
7959
8091
|
}
|
|
7960
8092
|
function installMongoDbRequireHook() {
|
|
7961
8093
|
if (mongoDbRequireHookInstalled)
|
|
@@ -7997,9 +8129,13 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
|
|
|
7997
8129
|
const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
|
|
7998
8130
|
const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
|
|
7999
8131
|
const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
|
|
8132
|
+
const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
|
|
8133
|
+
const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
|
|
8134
|
+
const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
|
|
8000
8135
|
const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
|
|
8001
8136
|
let reproAutoMiddlewareConfig = null;
|
|
8002
8137
|
const mongoosePluginConfigs = [];
|
|
8138
|
+
const httpRequestListenerWrappers = new WeakMap();
|
|
8003
8139
|
let frameworkRequireHookInstalled = false;
|
|
8004
8140
|
let mongooseRequireHookInstalled = false;
|
|
8005
8141
|
function hasMarker(target, marker) {
|
|
@@ -8104,6 +8240,81 @@ function patchExpressFromModuleExports(expressModule) {
|
|
|
8104
8240
|
setMarker(wrappedListen, REPRO_AUTO_EXPRESS_LISTEN_PATCHED);
|
|
8105
8241
|
application.listen = wrappedListen;
|
|
8106
8242
|
}
|
|
8243
|
+
function wrapHttpRequestListener(listener, cfg) {
|
|
8244
|
+
if (typeof listener !== 'function')
|
|
8245
|
+
return listener;
|
|
8246
|
+
if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED))
|
|
8247
|
+
return listener;
|
|
8248
|
+
const existing = httpRequestListenerWrappers.get(listener);
|
|
8249
|
+
if (existing)
|
|
8250
|
+
return existing;
|
|
8251
|
+
const middleware = reproMiddleware(cfg);
|
|
8252
|
+
const wrapped = function reproPatchedHttpRequestListener(req, res) {
|
|
8253
|
+
return middleware(req, res, (err) => {
|
|
8254
|
+
if (err) {
|
|
8255
|
+
try {
|
|
8256
|
+
if (!res.headersSent) {
|
|
8257
|
+
res.statusCode = 500;
|
|
8258
|
+
res.end('Internal Server Error');
|
|
8259
|
+
return undefined;
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
catch { }
|
|
8263
|
+
throw err;
|
|
8264
|
+
}
|
|
8265
|
+
return listener.call(this, req, res);
|
|
8266
|
+
});
|
|
8267
|
+
};
|
|
8268
|
+
setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
|
|
8269
|
+
httpRequestListenerWrappers.set(listener, wrapped);
|
|
8270
|
+
return wrapped;
|
|
8271
|
+
}
|
|
8272
|
+
function patchHttpServerRequestListenerMethods(server) {
|
|
8273
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
8274
|
+
if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED))
|
|
8275
|
+
return;
|
|
8276
|
+
for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
|
|
8277
|
+
const original = server[methodName];
|
|
8278
|
+
if (typeof original !== 'function')
|
|
8279
|
+
continue;
|
|
8280
|
+
server[methodName] = function reproPatchedHttpServerListenerMethod(...args) {
|
|
8281
|
+
if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
|
|
8282
|
+
args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
|
|
8283
|
+
}
|
|
8284
|
+
return original.apply(this, args);
|
|
8285
|
+
};
|
|
8286
|
+
}
|
|
8287
|
+
setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
|
|
8288
|
+
}
|
|
8289
|
+
function patchHttpCreateServerFromModuleExports(httpModule) {
|
|
8290
|
+
const original = httpModule?.createServer;
|
|
8291
|
+
if (!httpModule || typeof original !== 'function')
|
|
8292
|
+
return;
|
|
8293
|
+
if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED))
|
|
8294
|
+
return;
|
|
8295
|
+
const wrappedCreateServer = function reproPatchedHttpCreateServer(...args) {
|
|
8296
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
8297
|
+
if (cfg) {
|
|
8298
|
+
const listenerIndex = typeof args[0] === 'function'
|
|
8299
|
+
? 0
|
|
8300
|
+
: typeof args[1] === 'function'
|
|
8301
|
+
? 1
|
|
8302
|
+
: -1;
|
|
8303
|
+
if (listenerIndex >= 0) {
|
|
8304
|
+
args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8307
|
+
const server = original.apply(this, args);
|
|
8308
|
+
patchHttpServerRequestListenerMethods(server);
|
|
8309
|
+
return server;
|
|
8310
|
+
};
|
|
8311
|
+
setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
|
|
8312
|
+
httpModule.createServer = wrappedCreateServer;
|
|
8313
|
+
}
|
|
8314
|
+
function patchHttpServerAutoAttach() {
|
|
8315
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
|
|
8316
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
|
|
8317
|
+
}
|
|
8107
8318
|
function patchKnownFrameworkModules() {
|
|
8108
8319
|
const nestModules = new Set();
|
|
8109
8320
|
const resolvedNest = requireOptionalHostModule('@nestjs/core');
|
|
@@ -8407,6 +8618,7 @@ async function initRepro(cfg) {
|
|
|
8407
8618
|
};
|
|
8408
8619
|
shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
|
|
8409
8620
|
installFrameworkRequireHook();
|
|
8621
|
+
patchHttpServerAutoAttach();
|
|
8410
8622
|
patchKnownFrameworkModules();
|
|
8411
8623
|
patchHttpClients({
|
|
8412
8624
|
resolveContext: cfg.resolveContext,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "Repro Nest SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
13
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
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/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",
|
|
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",
|
|
16
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"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -313,6 +313,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
|
|
|
313
313
|
const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
|
|
314
314
|
const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
|
|
315
315
|
const REPRO_QUERY_START_PARAM = '__repro_start';
|
|
316
|
+
const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
|
|
316
317
|
const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
|
|
317
318
|
const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
|
|
318
319
|
const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
@@ -1429,7 +1430,6 @@ function scheduleTraceMaterializerIdleShutdown(): void {
|
|
|
1429
1430
|
try { void worker.terminate?.(); } catch {}
|
|
1430
1431
|
}
|
|
1431
1432
|
}, TRACE_MATERIALIZE_WORKER_IDLE_SHUTDOWN_MS);
|
|
1432
|
-
try { (traceMaterializerIdleTimer as any).unref?.(); } catch {}
|
|
1433
1433
|
}
|
|
1434
1434
|
|
|
1435
1435
|
function rejectTraceMaterializerPending(error: Error): void {
|
|
@@ -1745,11 +1745,34 @@ function waitForSessionDrain(sessionId: string): Promise<void> {
|
|
|
1745
1745
|
if (remaining <= 0) return Promise.resolve();
|
|
1746
1746
|
|
|
1747
1747
|
return new Promise(resolve => {
|
|
1748
|
+
let settled = false;
|
|
1749
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
1750
|
+
const removeWaiter = (waiter: () => void) => {
|
|
1751
|
+
const list = sessionWaiters.get(sessionId);
|
|
1752
|
+
if (!list) return;
|
|
1753
|
+
const next = list.filter(fn => fn !== waiter);
|
|
1754
|
+
if (next.length) {
|
|
1755
|
+
sessionWaiters.set(sessionId, next);
|
|
1756
|
+
} else {
|
|
1757
|
+
sessionWaiters.delete(sessionId);
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
const done = () => {
|
|
1761
|
+
if (settled) return;
|
|
1762
|
+
settled = true;
|
|
1763
|
+
if (timeout) {
|
|
1764
|
+
try { clearTimeout(timeout); } catch {}
|
|
1765
|
+
timeout = null;
|
|
1766
|
+
}
|
|
1767
|
+
removeWaiter(done);
|
|
1768
|
+
resolve();
|
|
1769
|
+
};
|
|
1748
1770
|
const list = sessionWaiters.get(sessionId) || [];
|
|
1749
|
-
list.push(
|
|
1771
|
+
list.push(done);
|
|
1750
1772
|
sessionWaiters.set(sessionId, list);
|
|
1751
1773
|
if (SESSION_DRAIN_TIMEOUT_MS > 0 && Number.isFinite(SESSION_DRAIN_TIMEOUT_MS)) {
|
|
1752
|
-
setTimeout(
|
|
1774
|
+
timeout = setTimeout(done, SESSION_DRAIN_TIMEOUT_MS);
|
|
1775
|
+
try { (timeout as any).unref?.(); } catch {}
|
|
1753
1776
|
}
|
|
1754
1777
|
});
|
|
1755
1778
|
}
|
|
@@ -4894,6 +4917,19 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4894
4917
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4895
4918
|
patchMongoDb(cfg);
|
|
4896
4919
|
return async function (req: Request, res: Response, next: NextFunction) {
|
|
4920
|
+
const reqAny = req as any;
|
|
4921
|
+
if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
|
|
4922
|
+
return next();
|
|
4923
|
+
}
|
|
4924
|
+
try {
|
|
4925
|
+
Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
|
|
4926
|
+
value: true,
|
|
4927
|
+
configurable: true,
|
|
4928
|
+
});
|
|
4929
|
+
} catch {
|
|
4930
|
+
try { reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true; } catch {}
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4897
4933
|
const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
|
|
4898
4934
|
const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
|
|
4899
4935
|
const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
|
|
@@ -4933,24 +4969,44 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4933
4969
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
4934
4970
|
beginSessionRequest(sid);
|
|
4935
4971
|
|
|
4936
|
-
// ---- response body capture
|
|
4972
|
+
// ---- response body capture ----
|
|
4937
4973
|
let capturedBody: any = undefined;
|
|
4938
|
-
const
|
|
4939
|
-
(
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4974
|
+
const originalJson = (res as any).json;
|
|
4975
|
+
if (typeof originalJson === 'function') {
|
|
4976
|
+
const origJson = originalJson.bind(res as any);
|
|
4977
|
+
(res as any).json = (body: any) => { capturedBody = body; return origJson(body); };
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
const originalSend = (res as any).send;
|
|
4981
|
+
if (typeof originalSend === 'function') {
|
|
4982
|
+
const origSend = originalSend.bind(res as any);
|
|
4983
|
+
(res as any).send = (body: any) => {
|
|
4984
|
+
if (capturedBody === undefined) {
|
|
4985
|
+
capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
|
|
4986
|
+
}
|
|
4987
|
+
return origSend(body);
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4948
4990
|
|
|
4949
|
-
const origWrite = (res as any).write
|
|
4950
|
-
|
|
4991
|
+
const origWrite = typeof (res as any).write === 'function'
|
|
4992
|
+
? (res as any).write.bind(res as any)
|
|
4993
|
+
: null;
|
|
4994
|
+
const origEnd = typeof (res as any).end === 'function'
|
|
4995
|
+
? (res as any).end.bind(res as any)
|
|
4996
|
+
: null;
|
|
4951
4997
|
const chunks: Array<Buffer | string> = [];
|
|
4952
|
-
|
|
4953
|
-
|
|
4998
|
+
if (origWrite) {
|
|
4999
|
+
(res as any).write = (chunk: any, ...args: any[]) => {
|
|
5000
|
+
try { if (chunk != null) chunks.push(chunk); } catch {}
|
|
5001
|
+
return origWrite(chunk, ...args);
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
if (origEnd) {
|
|
5005
|
+
(res as any).end = (chunk?: any, ...args: any[]) => {
|
|
5006
|
+
try { if (chunk != null) chunks.push(chunk); } catch {}
|
|
5007
|
+
return origEnd(chunk, ...args);
|
|
5008
|
+
};
|
|
5009
|
+
}
|
|
4954
5010
|
|
|
4955
5011
|
// ---- our ALS (unchanged) ----
|
|
4956
5012
|
const tracerApiWithTrace = __TRACER__ as (TracerApi & { withTrace?: (id: string, fn: () => void) => void }) | null;
|
|
@@ -5490,6 +5546,7 @@ function nativeMongoOpsForMongooseOp(op: string): string[] {
|
|
|
5490
5546
|
}
|
|
5491
5547
|
|
|
5492
5548
|
export function reproMongoosePlugin(cfg: ReproMiddlewareConfig) {
|
|
5549
|
+
patchMongoDb(cfg);
|
|
5493
5550
|
return function (schema: Schema) {
|
|
5494
5551
|
// -------- pre/post save (unchanged) --------
|
|
5495
5552
|
schema.pre('save', { document: true }, function (next) {
|
|
@@ -9185,6 +9242,46 @@ function patchMongoCollectionPrototype(proto: any, cfg: MongoDbPatchConfig): voi
|
|
|
9185
9242
|
}
|
|
9186
9243
|
}
|
|
9187
9244
|
|
|
9245
|
+
function patchMongoCollectionInstance(collection: any, cfg: MongoDbPatchConfig): any {
|
|
9246
|
+
if (!collection || typeof collection !== 'object') return collection;
|
|
9247
|
+
patchMongoCollectionPrototype(Object.getPrototypeOf(collection), cfg);
|
|
9248
|
+
patchMongoCollectionPrototype(collection, cfg);
|
|
9249
|
+
return collection;
|
|
9250
|
+
}
|
|
9251
|
+
|
|
9252
|
+
function patchMongoDbPrototype(proto: any, cfg: MongoDbPatchConfig): void {
|
|
9253
|
+
if (!proto) return;
|
|
9254
|
+
const marker = '__repro_mongodb_db_collection_patched';
|
|
9255
|
+
if (proto[marker]) return;
|
|
9256
|
+
const original = proto.collection;
|
|
9257
|
+
if (typeof original !== 'function') return;
|
|
9258
|
+
proto[marker] = true;
|
|
9259
|
+
proto.collection = function reproPatchedMongoDbCollection(this: any, ...args: any[]) {
|
|
9260
|
+
const collection = original.apply(this, args);
|
|
9261
|
+
return patchMongoCollectionInstance(collection, cfg);
|
|
9262
|
+
};
|
|
9263
|
+
}
|
|
9264
|
+
|
|
9265
|
+
function patchMongoDbInstance(db: any, cfg: MongoDbPatchConfig): any {
|
|
9266
|
+
if (!db || typeof db !== 'object') return db;
|
|
9267
|
+
patchMongoDbPrototype(Object.getPrototypeOf(db), cfg);
|
|
9268
|
+
patchMongoDbPrototype(db, cfg);
|
|
9269
|
+
return db;
|
|
9270
|
+
}
|
|
9271
|
+
|
|
9272
|
+
function patchMongoClientPrototype(proto: any, cfg: MongoDbPatchConfig): void {
|
|
9273
|
+
if (!proto) return;
|
|
9274
|
+
const marker = '__repro_mongodb_client_db_patched';
|
|
9275
|
+
if (proto[marker]) return;
|
|
9276
|
+
const original = proto.db;
|
|
9277
|
+
if (typeof original !== 'function') return;
|
|
9278
|
+
proto[marker] = true;
|
|
9279
|
+
proto.db = function reproPatchedMongoClientDb(this: any, ...args: any[]) {
|
|
9280
|
+
const db = original.apply(this, args);
|
|
9281
|
+
return patchMongoDbInstance(db, cfg);
|
|
9282
|
+
};
|
|
9283
|
+
}
|
|
9284
|
+
|
|
9188
9285
|
function patchMongoDbFromModuleExports(mongoModule: any, cfg: MongoDbPatchConfig): void {
|
|
9189
9286
|
if (!mongoModule) return;
|
|
9190
9287
|
const constructors = new Set<any>();
|
|
@@ -9200,6 +9297,31 @@ function patchMongoDbFromModuleExports(mongoModule: any, cfg: MongoDbPatchConfig
|
|
|
9200
9297
|
for (const ctor of constructors) {
|
|
9201
9298
|
patchMongoCollectionPrototype(ctor.prototype, cfg);
|
|
9202
9299
|
}
|
|
9300
|
+
|
|
9301
|
+
const dbConstructors = new Set<any>();
|
|
9302
|
+
const clientConstructors = new Set<any>();
|
|
9303
|
+
const addDbCtor = (candidate: any) => {
|
|
9304
|
+
if (typeof candidate === 'function' && candidate.prototype) {
|
|
9305
|
+
dbConstructors.add(candidate);
|
|
9306
|
+
}
|
|
9307
|
+
};
|
|
9308
|
+
const addClientCtor = (candidate: any) => {
|
|
9309
|
+
if (typeof candidate === 'function' && candidate.prototype) {
|
|
9310
|
+
clientConstructors.add(candidate);
|
|
9311
|
+
}
|
|
9312
|
+
};
|
|
9313
|
+
|
|
9314
|
+
addDbCtor(mongoModule?.Db);
|
|
9315
|
+
addDbCtor(mongoModule?.default?.Db);
|
|
9316
|
+
addClientCtor(mongoModule?.MongoClient);
|
|
9317
|
+
addClientCtor(mongoModule?.default?.MongoClient);
|
|
9318
|
+
|
|
9319
|
+
for (const ctor of dbConstructors) {
|
|
9320
|
+
patchMongoDbPrototype(ctor.prototype, cfg);
|
|
9321
|
+
}
|
|
9322
|
+
for (const ctor of clientConstructors) {
|
|
9323
|
+
patchMongoClientPrototype(ctor.prototype, cfg);
|
|
9324
|
+
}
|
|
9203
9325
|
}
|
|
9204
9326
|
|
|
9205
9327
|
function installMongoDbRequireHook(): void {
|
|
@@ -9243,10 +9365,14 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
|
|
|
9243
9365
|
const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
|
|
9244
9366
|
const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
|
|
9245
9367
|
const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
|
|
9368
|
+
const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
|
|
9369
|
+
const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
|
|
9370
|
+
const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
|
|
9246
9371
|
const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
|
|
9247
9372
|
|
|
9248
9373
|
let reproAutoMiddlewareConfig: ReproMiddlewareConfig | null = null;
|
|
9249
9374
|
const mongoosePluginConfigs: ReproMiddlewareConfig[] = [];
|
|
9375
|
+
const httpRequestListenerWrappers = new WeakMap<Function, Function>();
|
|
9250
9376
|
let frameworkRequireHookInstalled = false;
|
|
9251
9377
|
let mongooseRequireHookInstalled = false;
|
|
9252
9378
|
|
|
@@ -9347,6 +9473,82 @@ function patchExpressFromModuleExports(expressModule: any): void {
|
|
|
9347
9473
|
application.listen = wrappedListen;
|
|
9348
9474
|
}
|
|
9349
9475
|
|
|
9476
|
+
function wrapHttpRequestListener(listener: any, cfg: ReproMiddlewareConfig): any {
|
|
9477
|
+
if (typeof listener !== 'function') return listener;
|
|
9478
|
+
if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED)) return listener;
|
|
9479
|
+
|
|
9480
|
+
const existing = httpRequestListenerWrappers.get(listener);
|
|
9481
|
+
if (existing) return existing;
|
|
9482
|
+
|
|
9483
|
+
const middleware = reproMiddleware(cfg);
|
|
9484
|
+
const wrapped = function reproPatchedHttpRequestListener(this: any, req: any, res: any) {
|
|
9485
|
+
return middleware(req, res, (err?: any) => {
|
|
9486
|
+
if (err) {
|
|
9487
|
+
try {
|
|
9488
|
+
if (!res.headersSent) {
|
|
9489
|
+
res.statusCode = 500;
|
|
9490
|
+
res.end('Internal Server Error');
|
|
9491
|
+
return undefined;
|
|
9492
|
+
}
|
|
9493
|
+
} catch {}
|
|
9494
|
+
throw err;
|
|
9495
|
+
}
|
|
9496
|
+
return listener.call(this, req, res);
|
|
9497
|
+
});
|
|
9498
|
+
};
|
|
9499
|
+
setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
|
|
9500
|
+
httpRequestListenerWrappers.set(listener, wrapped);
|
|
9501
|
+
return wrapped;
|
|
9502
|
+
}
|
|
9503
|
+
|
|
9504
|
+
function patchHttpServerRequestListenerMethods(server: any): void {
|
|
9505
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
9506
|
+
if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED)) return;
|
|
9507
|
+
|
|
9508
|
+
for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
|
|
9509
|
+
const original = server[methodName];
|
|
9510
|
+
if (typeof original !== 'function') continue;
|
|
9511
|
+
server[methodName] = function reproPatchedHttpServerListenerMethod(this: any, ...args: any[]) {
|
|
9512
|
+
if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
|
|
9513
|
+
args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
|
|
9514
|
+
}
|
|
9515
|
+
return original.apply(this, args);
|
|
9516
|
+
};
|
|
9517
|
+
}
|
|
9518
|
+
setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
|
|
9519
|
+
}
|
|
9520
|
+
|
|
9521
|
+
function patchHttpCreateServerFromModuleExports(httpModule: any): void {
|
|
9522
|
+
const original = httpModule?.createServer;
|
|
9523
|
+
if (!httpModule || typeof original !== 'function') return;
|
|
9524
|
+
if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED)) return;
|
|
9525
|
+
|
|
9526
|
+
const wrappedCreateServer = function reproPatchedHttpCreateServer(this: any, ...args: any[]) {
|
|
9527
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
9528
|
+
if (cfg) {
|
|
9529
|
+
const listenerIndex =
|
|
9530
|
+
typeof args[0] === 'function'
|
|
9531
|
+
? 0
|
|
9532
|
+
: typeof args[1] === 'function'
|
|
9533
|
+
? 1
|
|
9534
|
+
: -1;
|
|
9535
|
+
if (listenerIndex >= 0) {
|
|
9536
|
+
args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
const server = original.apply(this, args);
|
|
9540
|
+
patchHttpServerRequestListenerMethods(server);
|
|
9541
|
+
return server;
|
|
9542
|
+
};
|
|
9543
|
+
setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
|
|
9544
|
+
httpModule.createServer = wrappedCreateServer;
|
|
9545
|
+
}
|
|
9546
|
+
|
|
9547
|
+
function patchHttpServerAutoAttach(): void {
|
|
9548
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
|
|
9549
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
|
|
9550
|
+
}
|
|
9551
|
+
|
|
9350
9552
|
function patchKnownFrameworkModules(): void {
|
|
9351
9553
|
const nestModules = new Set<any>();
|
|
9352
9554
|
const resolvedNest = requireOptionalHostModule('@nestjs/core');
|
|
@@ -9654,6 +9856,7 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
9654
9856
|
shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
|
|
9655
9857
|
|
|
9656
9858
|
installFrameworkRequireHook();
|
|
9859
|
+
patchHttpServerAutoAttach();
|
|
9657
9860
|
patchKnownFrameworkModules();
|
|
9658
9861
|
|
|
9659
9862
|
patchHttpClients({
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
|
|
2
|
+
process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
|
|
3
|
+
process.env.TRACE_IDLE_FLUSH_MS = '10';
|
|
4
|
+
process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
|
|
5
|
+
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const http = require('node:http');
|
|
8
|
+
const mongodb = require('mongodb');
|
|
9
|
+
|
|
10
|
+
const originalNativeMongoInsertOne = mongodb.Collection.prototype.insertOne;
|
|
11
|
+
mongodb.Collection.prototype.insertOne = async function reproFakeNativeMongoInsertOne(doc, options) {
|
|
12
|
+
assert.equal(options.session.id, 'auto-http-native-session-1');
|
|
13
|
+
doc._id = doc._id ?? 'subject-auto-http-1';
|
|
14
|
+
return { acknowledged: true, insertedId: doc._id };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const { initRepro, reproMiddleware } = require('../dist');
|
|
18
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
19
|
+
|
|
20
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
|
|
22
|
+
async function waitFor(predicate, timeoutMs = 3000) {
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
while (Date.now() < deadline) {
|
|
25
|
+
if (predicate()) return;
|
|
26
|
+
await sleep(25);
|
|
27
|
+
}
|
|
28
|
+
assert(predicate(), 'timed out waiting for expected condition');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function flattenEvents(posts) {
|
|
32
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function backendRequests(posts, sessionId) {
|
|
36
|
+
return flattenEvents(posts).filter((event) => {
|
|
37
|
+
return event?.event_type === 'backend_request' && event?.session_id === sessionId;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dbChanges(posts, sessionId) {
|
|
42
|
+
return flattenEvents(posts)
|
|
43
|
+
.filter((event) => event?.event_type === 'db_change' && event?.session_id === sessionId)
|
|
44
|
+
.flatMap((event) => Array.isArray(event?.payload?.db) ? event.payload.db : []);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listen(server) {
|
|
48
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
49
|
+
return server.address().port;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function close(server) {
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function request(port, sessionId, actionId) {
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
const req = http.request({
|
|
61
|
+
host: '127.0.0.1',
|
|
62
|
+
port,
|
|
63
|
+
path: '/subjects',
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'content-type': 'application/json',
|
|
67
|
+
'x-bug-session-id': sessionId,
|
|
68
|
+
'x-bug-action-id': actionId,
|
|
69
|
+
'x-bug-request-start': String(Date.now()),
|
|
70
|
+
},
|
|
71
|
+
}, (res) => {
|
|
72
|
+
res.resume();
|
|
73
|
+
res.on('end', resolve);
|
|
74
|
+
});
|
|
75
|
+
req.on('error', reject);
|
|
76
|
+
req.end(JSON.stringify({ ok: true }));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function main() {
|
|
81
|
+
const capturedBodies = [];
|
|
82
|
+
const originalFetch = global.fetch;
|
|
83
|
+
global.fetch = async (_url, init = {}) => {
|
|
84
|
+
capturedBodies.push(JSON.parse(String(init.body || '{}')));
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
status: 202,
|
|
88
|
+
json: async () => ({ ok: true }),
|
|
89
|
+
text: async () => '{"ok":true}',
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const cfg = {
|
|
94
|
+
tenantId: 'TENANT_test',
|
|
95
|
+
appId: 'APP_test',
|
|
96
|
+
appSecret: 'secret',
|
|
97
|
+
appName: 'auto-http-test',
|
|
98
|
+
serviceName: 'Auto HTTP Test',
|
|
99
|
+
captureHeaders: false,
|
|
100
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
101
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let autoServer;
|
|
105
|
+
let doubleServer;
|
|
106
|
+
try {
|
|
107
|
+
await initRepro(cfg);
|
|
108
|
+
|
|
109
|
+
const subjectsCollection = new mongodb.MongoClient('mongodb://127.0.0.1:27017/clinical')
|
|
110
|
+
.db('clinical')
|
|
111
|
+
.collection('subjects');
|
|
112
|
+
|
|
113
|
+
autoServer = http.createServer(async (_req, res) => {
|
|
114
|
+
await subjectsCollection.insertOne(
|
|
115
|
+
{ subjectNumber: '10013038', status: 'screened' },
|
|
116
|
+
{
|
|
117
|
+
session: {
|
|
118
|
+
id: 'auto-http-native-session-1',
|
|
119
|
+
hasEnded: false,
|
|
120
|
+
inTransaction: () => true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
res.setHeader('content-type', 'application/json');
|
|
125
|
+
res.end(JSON.stringify({ ok: true, mode: 'auto' }));
|
|
126
|
+
});
|
|
127
|
+
const autoPort = await listen(autoServer);
|
|
128
|
+
await request(autoPort, 'S_auto_http_only', 'A_auto_http_only');
|
|
129
|
+
await waitFor(() => backendRequests(capturedBodies, 'S_auto_http_only').length === 1);
|
|
130
|
+
await waitFor(() => dbChanges(capturedBodies, 'S_auto_http_only').length === 1);
|
|
131
|
+
assert.equal(backendRequests(capturedBodies, 'S_auto_http_only').length, 1);
|
|
132
|
+
const [nativeMongoChange] = dbChanges(capturedBodies, 'S_auto_http_only');
|
|
133
|
+
assert.equal(nativeMongoChange.type, 'mongo');
|
|
134
|
+
assert.equal(nativeMongoChange.collection, 'subjects');
|
|
135
|
+
assert.equal(nativeMongoChange.op, 'insertOne');
|
|
136
|
+
assert.equal(nativeMongoChange.pk._id, 'subject-auto-http-1');
|
|
137
|
+
assert.equal(nativeMongoChange.query.driver, 'mongodb');
|
|
138
|
+
assert.equal(nativeMongoChange.query.db, 'clinical');
|
|
139
|
+
assert.equal(nativeMongoChange.query.doc.subjectNumber, '10013038');
|
|
140
|
+
assert.equal(nativeMongoChange.query.options.session.__kind, 'mongodb_client_session');
|
|
141
|
+
assert.equal(nativeMongoChange.query.options.session.id, 'auto-http-native-session-1');
|
|
142
|
+
assert.equal(nativeMongoChange.after.subjectNumber, '10013038');
|
|
143
|
+
assert.equal(nativeMongoChange.resultMeta.insertedId, 'subject-auto-http-1');
|
|
144
|
+
|
|
145
|
+
const manual = reproMiddleware(cfg);
|
|
146
|
+
doubleServer = http.createServer((req, res) => {
|
|
147
|
+
manual(req, res, () => {
|
|
148
|
+
res.setHeader('content-type', 'application/json');
|
|
149
|
+
res.end(JSON.stringify({ ok: true, mode: 'manual-too' }));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
const doublePort = await listen(doubleServer);
|
|
153
|
+
await request(doublePort, 'S_auto_http_no_duplicate', 'A_auto_http_no_duplicate');
|
|
154
|
+
await waitFor(() => backendRequests(capturedBodies, 'S_auto_http_no_duplicate').length === 1);
|
|
155
|
+
assert.equal(backendRequests(capturedBodies, 'S_auto_http_no_duplicate').length, 1);
|
|
156
|
+
|
|
157
|
+
console.log('initRepro auto HTTP server middleware OK');
|
|
158
|
+
} finally {
|
|
159
|
+
if (autoServer?.listening) await close(autoServer);
|
|
160
|
+
if (doubleServer?.listening) await close(doubleServer);
|
|
161
|
+
await flushIngestQueue();
|
|
162
|
+
mongodb.Collection.prototype.insertOne = originalNativeMongoInsertOne;
|
|
163
|
+
global.fetch = originalFetch;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main().catch((error) => {
|
|
168
|
+
console.error(error);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
@@ -176,6 +176,83 @@ async function testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne() {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
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
|
+
|
|
179
256
|
async function testNativeInsertOneIsCaptured() {
|
|
180
257
|
await withCapturedFetch(async (capturedBodies) => {
|
|
181
258
|
__reproTestHooks.resetSdkBackgroundQueuesForTest();
|
|
@@ -376,6 +453,7 @@ async function testNativeOperationFailureIsCapturedAndRethrown() {
|
|
|
376
453
|
|
|
377
454
|
(async () => {
|
|
378
455
|
await testReproMiddlewarePatchesPreloadedNativeMongoClientInsertOne();
|
|
456
|
+
await testMongoClientDbCollectionChainIsPatchedWithoutCollectionExport();
|
|
379
457
|
await testNativeInsertOneIsCaptured();
|
|
380
458
|
await testNativeUpdateOneUpsertIsCaptured();
|
|
381
459
|
await testNativeFindCursorToArrayIsCaptured();
|