@reproapp/node-sdk 0.0.29 → 0.0.31
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 +203 -26
- package/package.json +2 -2
- package/src/index.ts +201 -21
- package/test/init-auto-http-server.test.js +170 -0
- package/test/mongodb-esm-import-hook.test.js +207 -0
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const async_hooks_1 = require("async_hooks");
|
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
7
7
|
const module_1 = require("module");
|
|
8
8
|
const path_1 = require("path");
|
|
9
|
+
const url_1 = require("url");
|
|
9
10
|
const client_1 = require("./ingest/client");
|
|
10
11
|
const privacy_1 = require("./privacy");
|
|
11
12
|
/**
|
|
@@ -295,6 +296,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
|
|
|
295
296
|
const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
|
|
296
297
|
const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
|
|
297
298
|
const REPRO_QUERY_START_PARAM = '__repro_start';
|
|
299
|
+
const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
|
|
298
300
|
const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
|
|
299
301
|
const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
|
|
300
302
|
const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
@@ -4027,6 +4029,22 @@ function reproMiddleware(cfg) {
|
|
|
4027
4029
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4028
4030
|
patchMongoDb(cfg);
|
|
4029
4031
|
return async function (req, res, next) {
|
|
4032
|
+
const reqAny = req;
|
|
4033
|
+
if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
|
|
4034
|
+
return next();
|
|
4035
|
+
}
|
|
4036
|
+
try {
|
|
4037
|
+
Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
|
|
4038
|
+
value: true,
|
|
4039
|
+
configurable: true,
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
catch {
|
|
4043
|
+
try {
|
|
4044
|
+
reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true;
|
|
4045
|
+
}
|
|
4046
|
+
catch { }
|
|
4047
|
+
}
|
|
4030
4048
|
const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
|
|
4031
4049
|
const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
|
|
4032
4050
|
const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
|
|
@@ -4063,30 +4081,50 @@ function reproMiddleware(cfg) {
|
|
|
4063
4081
|
const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
|
|
4064
4082
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
4065
4083
|
beginSessionRequest(sid);
|
|
4066
|
-
// ---- response body capture
|
|
4084
|
+
// ---- response body capture ----
|
|
4067
4085
|
let capturedBody = undefined;
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4086
|
+
const originalJson = res.json;
|
|
4087
|
+
if (typeof originalJson === 'function') {
|
|
4088
|
+
const origJson = originalJson.bind(res);
|
|
4089
|
+
res.json = (body) => { capturedBody = body; return origJson(body); };
|
|
4090
|
+
}
|
|
4091
|
+
const originalSend = res.send;
|
|
4092
|
+
if (typeof originalSend === 'function') {
|
|
4093
|
+
const origSend = originalSend.bind(res);
|
|
4094
|
+
res.send = (body) => {
|
|
4095
|
+
if (capturedBody === undefined) {
|
|
4096
|
+
capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
|
|
4097
|
+
}
|
|
4098
|
+
return origSend(body);
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
const origWrite = typeof res.write === 'function'
|
|
4102
|
+
? res.write.bind(res)
|
|
4103
|
+
: null;
|
|
4104
|
+
const origEnd = typeof res.end === 'function'
|
|
4105
|
+
? res.end.bind(res)
|
|
4106
|
+
: null;
|
|
4079
4107
|
const chunks = [];
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4108
|
+
if (origWrite) {
|
|
4109
|
+
res.write = (chunk, ...args) => {
|
|
4110
|
+
try {
|
|
4111
|
+
if (chunk != null)
|
|
4112
|
+
chunks.push(chunk);
|
|
4113
|
+
}
|
|
4114
|
+
catch { }
|
|
4115
|
+
return origWrite(chunk, ...args);
|
|
4116
|
+
};
|
|
4083
4117
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4118
|
+
if (origEnd) {
|
|
4119
|
+
res.end = (chunk, ...args) => {
|
|
4120
|
+
try {
|
|
4121
|
+
if (chunk != null)
|
|
4122
|
+
chunks.push(chunk);
|
|
4123
|
+
}
|
|
4124
|
+
catch { }
|
|
4125
|
+
return origEnd(chunk, ...args);
|
|
4126
|
+
};
|
|
4088
4127
|
}
|
|
4089
|
-
catch { } return origEnd(chunk, ...args); };
|
|
4090
4128
|
// ---- our ALS (unchanged) ----
|
|
4091
4129
|
const tracerApiWithTrace = __TRACER__;
|
|
4092
4130
|
const runInTrace = (fn) => {
|
|
@@ -7562,6 +7600,8 @@ function patchRedis(cfg) {
|
|
|
7562
7600
|
exports.patchRedis = patchRedis;
|
|
7563
7601
|
const mongoDbPatchConfigs = [];
|
|
7564
7602
|
let mongoDbRequireHookInstalled = false;
|
|
7603
|
+
let mongoDbImportHookInstalled = false;
|
|
7604
|
+
let mongoDbImportHookReady = null;
|
|
7565
7605
|
const MONGODB_PROMISE_OPS = [
|
|
7566
7606
|
'insertOne',
|
|
7567
7607
|
'insertMany',
|
|
@@ -7581,6 +7621,11 @@ const MONGODB_PROMISE_OPS = [
|
|
|
7581
7621
|
];
|
|
7582
7622
|
const MONGODB_CURSOR_OPS = ['find', 'aggregate'];
|
|
7583
7623
|
const MONGODB_CURSOR_TERMINALS = ['toArray', 'next', 'forEach'];
|
|
7624
|
+
function registerMongoDbPatchConfig(cfg) {
|
|
7625
|
+
if (!mongoDbPatchConfigs.includes(cfg)) {
|
|
7626
|
+
mongoDbPatchConfigs.push(cfg);
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7584
7629
|
function resolveMongoDbRuntimeContext(cfg) {
|
|
7585
7630
|
const ctx = getCtx();
|
|
7586
7631
|
const fallback = cfg.resolveContext?.() ?? {};
|
|
@@ -7590,6 +7635,18 @@ function resolveMongoDbRuntimeContext(cfg) {
|
|
|
7590
7635
|
aid: ctx.aid ?? fallback.aid ?? null,
|
|
7591
7636
|
};
|
|
7592
7637
|
}
|
|
7638
|
+
function resolveMongoDbCaptureContext() {
|
|
7639
|
+
for (let i = mongoDbPatchConfigs.length - 1; i >= 0; i -= 1) {
|
|
7640
|
+
const cfg = mongoDbPatchConfigs[i];
|
|
7641
|
+
if (!cfg)
|
|
7642
|
+
continue;
|
|
7643
|
+
const { sid, aid } = resolveMongoDbRuntimeContext(cfg);
|
|
7644
|
+
if (sid) {
|
|
7645
|
+
return { cfg, sid, aid };
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
return null;
|
|
7649
|
+
}
|
|
7593
7650
|
function resolveMongoCollectionName(collection) {
|
|
7594
7651
|
const candidates = [
|
|
7595
7652
|
collection?.collectionName,
|
|
@@ -7874,10 +7931,11 @@ function patchMongoCollectionMethod(proto, op, cursorOp, cfg) {
|
|
|
7874
7931
|
if (isNativeMongoSuppressed(collection, op)) {
|
|
7875
7932
|
return original.apply(this, args);
|
|
7876
7933
|
}
|
|
7877
|
-
const
|
|
7878
|
-
if (!
|
|
7934
|
+
const captureContext = resolveMongoDbCaptureContext();
|
|
7935
|
+
if (!captureContext) {
|
|
7879
7936
|
return original.apply(this, args);
|
|
7880
7937
|
}
|
|
7938
|
+
const { cfg: activeCfg, sid, aid } = captureContext;
|
|
7881
7939
|
const spanContext = captureSpanContextFromTracer(this);
|
|
7882
7940
|
const t0 = Date.now();
|
|
7883
7941
|
const dbName = resolveMongoDbName(this);
|
|
@@ -7888,7 +7946,7 @@ function patchMongoCollectionMethod(proto, op, cursorOp, cfg) {
|
|
|
7888
7946
|
if (emitted)
|
|
7889
7947
|
return;
|
|
7890
7948
|
emitted = true;
|
|
7891
|
-
emitDbQuery(
|
|
7949
|
+
emitDbQuery(activeCfg, sid, aid ?? undefined, {
|
|
7892
7950
|
type: 'mongo',
|
|
7893
7951
|
collection,
|
|
7894
7952
|
op,
|
|
@@ -7906,7 +7964,7 @@ function patchMongoCollectionMethod(proto, op, cursorOp, cfg) {
|
|
|
7906
7964
|
if (emitted)
|
|
7907
7965
|
return;
|
|
7908
7966
|
emitted = true;
|
|
7909
|
-
emitDbQuery(
|
|
7967
|
+
emitDbQuery(activeCfg, sid, aid ?? undefined, {
|
|
7910
7968
|
type: 'mongo',
|
|
7911
7969
|
collection,
|
|
7912
7970
|
op,
|
|
@@ -7931,7 +7989,7 @@ function patchMongoCollectionMethod(proto, op, cursorOp, cfg) {
|
|
|
7931
7989
|
const result = original.apply(this, actualArgs);
|
|
7932
7990
|
if (cursorOp) {
|
|
7933
7991
|
return patchMongoCursorInstance(result, {
|
|
7934
|
-
cfg,
|
|
7992
|
+
cfg: activeCfg,
|
|
7935
7993
|
sid,
|
|
7936
7994
|
aid,
|
|
7937
7995
|
collection,
|
|
@@ -8016,6 +8074,7 @@ function patchMongoClientPrototype(proto, cfg) {
|
|
|
8016
8074
|
};
|
|
8017
8075
|
}
|
|
8018
8076
|
function patchMongoDbFromModuleExports(mongoModule, cfg) {
|
|
8077
|
+
registerMongoDbPatchConfig(cfg);
|
|
8019
8078
|
if (!mongoModule)
|
|
8020
8079
|
return;
|
|
8021
8080
|
const constructors = new Set();
|
|
@@ -8071,9 +8130,46 @@ function installMongoDbRequireHook() {
|
|
|
8071
8130
|
}
|
|
8072
8131
|
catch { }
|
|
8073
8132
|
}
|
|
8133
|
+
function installMongoDbImportHook() {
|
|
8134
|
+
if (mongoDbImportHookInstalled)
|
|
8135
|
+
return mongoDbImportHookReady ?? Promise.resolve();
|
|
8136
|
+
mongoDbImportHookInstalled = true;
|
|
8137
|
+
mongoDbImportHookReady = (async () => {
|
|
8138
|
+
try {
|
|
8139
|
+
const nodeModule = requireOptionalHostModule('node:module') ?? requireOptionalHostModule('module');
|
|
8140
|
+
if (typeof nodeModule?.register !== 'function')
|
|
8141
|
+
return;
|
|
8142
|
+
const iitm = requireOptionalHostModule('import-in-the-middle');
|
|
8143
|
+
const Hook = iitm?.Hook ?? iitm;
|
|
8144
|
+
if (typeof Hook !== 'function')
|
|
8145
|
+
return;
|
|
8146
|
+
let waitForAllMessagesAcknowledged;
|
|
8147
|
+
let registerOptions = { data: { include: ['mongodb'] } };
|
|
8148
|
+
if (typeof iitm?.createAddHookMessageChannel === 'function') {
|
|
8149
|
+
const channel = iitm.createAddHookMessageChannel();
|
|
8150
|
+
registerOptions = channel.registerOptions;
|
|
8151
|
+
waitForAllMessagesAcknowledged = channel.waitForAllMessagesAcknowledged;
|
|
8152
|
+
}
|
|
8153
|
+
nodeModule.register('import-in-the-middle/hook.mjs', (0, url_1.pathToFileURL)(__filename), registerOptions);
|
|
8154
|
+
// eslint-disable-next-line no-new
|
|
8155
|
+
new Hook(['mongodb'], (exports) => {
|
|
8156
|
+
for (const cfg of mongoDbPatchConfigs) {
|
|
8157
|
+
patchMongoDbFromModuleExports(exports, cfg);
|
|
8158
|
+
}
|
|
8159
|
+
return exports;
|
|
8160
|
+
});
|
|
8161
|
+
if (waitForAllMessagesAcknowledged) {
|
|
8162
|
+
await waitForAllMessagesAcknowledged();
|
|
8163
|
+
}
|
|
8164
|
+
}
|
|
8165
|
+
catch { }
|
|
8166
|
+
})();
|
|
8167
|
+
return mongoDbImportHookReady;
|
|
8168
|
+
}
|
|
8074
8169
|
function patchMongoDb(cfg) {
|
|
8075
|
-
|
|
8170
|
+
registerMongoDbPatchConfig(cfg);
|
|
8076
8171
|
installMongoDbRequireHook();
|
|
8172
|
+
void installMongoDbImportHook();
|
|
8077
8173
|
const mongoModules = new Set();
|
|
8078
8174
|
for (const loadedMongoModule of getLoadedModuleExportsByPackage('mongodb')) {
|
|
8079
8175
|
mongoModules.add(loadedMongoModule);
|
|
@@ -8092,9 +8188,13 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
|
|
|
8092
8188
|
const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
|
|
8093
8189
|
const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
|
|
8094
8190
|
const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
|
|
8191
|
+
const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
|
|
8192
|
+
const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
|
|
8193
|
+
const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
|
|
8095
8194
|
const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
|
|
8096
8195
|
let reproAutoMiddlewareConfig = null;
|
|
8097
8196
|
const mongoosePluginConfigs = [];
|
|
8197
|
+
const httpRequestListenerWrappers = new WeakMap();
|
|
8098
8198
|
let frameworkRequireHookInstalled = false;
|
|
8099
8199
|
let mongooseRequireHookInstalled = false;
|
|
8100
8200
|
function hasMarker(target, marker) {
|
|
@@ -8199,6 +8299,81 @@ function patchExpressFromModuleExports(expressModule) {
|
|
|
8199
8299
|
setMarker(wrappedListen, REPRO_AUTO_EXPRESS_LISTEN_PATCHED);
|
|
8200
8300
|
application.listen = wrappedListen;
|
|
8201
8301
|
}
|
|
8302
|
+
function wrapHttpRequestListener(listener, cfg) {
|
|
8303
|
+
if (typeof listener !== 'function')
|
|
8304
|
+
return listener;
|
|
8305
|
+
if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED))
|
|
8306
|
+
return listener;
|
|
8307
|
+
const existing = httpRequestListenerWrappers.get(listener);
|
|
8308
|
+
if (existing)
|
|
8309
|
+
return existing;
|
|
8310
|
+
const middleware = reproMiddleware(cfg);
|
|
8311
|
+
const wrapped = function reproPatchedHttpRequestListener(req, res) {
|
|
8312
|
+
return middleware(req, res, (err) => {
|
|
8313
|
+
if (err) {
|
|
8314
|
+
try {
|
|
8315
|
+
if (!res.headersSent) {
|
|
8316
|
+
res.statusCode = 500;
|
|
8317
|
+
res.end('Internal Server Error');
|
|
8318
|
+
return undefined;
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
catch { }
|
|
8322
|
+
throw err;
|
|
8323
|
+
}
|
|
8324
|
+
return listener.call(this, req, res);
|
|
8325
|
+
});
|
|
8326
|
+
};
|
|
8327
|
+
setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
|
|
8328
|
+
httpRequestListenerWrappers.set(listener, wrapped);
|
|
8329
|
+
return wrapped;
|
|
8330
|
+
}
|
|
8331
|
+
function patchHttpServerRequestListenerMethods(server) {
|
|
8332
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
8333
|
+
if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED))
|
|
8334
|
+
return;
|
|
8335
|
+
for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
|
|
8336
|
+
const original = server[methodName];
|
|
8337
|
+
if (typeof original !== 'function')
|
|
8338
|
+
continue;
|
|
8339
|
+
server[methodName] = function reproPatchedHttpServerListenerMethod(...args) {
|
|
8340
|
+
if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
|
|
8341
|
+
args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
|
|
8342
|
+
}
|
|
8343
|
+
return original.apply(this, args);
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
|
|
8347
|
+
}
|
|
8348
|
+
function patchHttpCreateServerFromModuleExports(httpModule) {
|
|
8349
|
+
const original = httpModule?.createServer;
|
|
8350
|
+
if (!httpModule || typeof original !== 'function')
|
|
8351
|
+
return;
|
|
8352
|
+
if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED))
|
|
8353
|
+
return;
|
|
8354
|
+
const wrappedCreateServer = function reproPatchedHttpCreateServer(...args) {
|
|
8355
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
8356
|
+
if (cfg) {
|
|
8357
|
+
const listenerIndex = typeof args[0] === 'function'
|
|
8358
|
+
? 0
|
|
8359
|
+
: typeof args[1] === 'function'
|
|
8360
|
+
? 1
|
|
8361
|
+
: -1;
|
|
8362
|
+
if (listenerIndex >= 0) {
|
|
8363
|
+
args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
|
|
8364
|
+
}
|
|
8365
|
+
}
|
|
8366
|
+
const server = original.apply(this, args);
|
|
8367
|
+
patchHttpServerRequestListenerMethods(server);
|
|
8368
|
+
return server;
|
|
8369
|
+
};
|
|
8370
|
+
setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
|
|
8371
|
+
httpModule.createServer = wrappedCreateServer;
|
|
8372
|
+
}
|
|
8373
|
+
function patchHttpServerAutoAttach() {
|
|
8374
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
|
|
8375
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
|
|
8376
|
+
}
|
|
8202
8377
|
function patchKnownFrameworkModules() {
|
|
8203
8378
|
const nestModules = new Set();
|
|
8204
8379
|
const resolvedNest = requireOptionalHostModule('@nestjs/core');
|
|
@@ -8502,6 +8677,7 @@ async function initRepro(cfg) {
|
|
|
8502
8677
|
};
|
|
8503
8678
|
shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
|
|
8504
8679
|
installFrameworkRequireHook();
|
|
8680
|
+
patchHttpServerAutoAttach();
|
|
8505
8681
|
patchKnownFrameworkModules();
|
|
8506
8682
|
patchHttpClients({
|
|
8507
8683
|
resolveContext: cfg.resolveContext,
|
|
@@ -8511,6 +8687,7 @@ async function initRepro(cfg) {
|
|
|
8511
8687
|
patchAmqplib(integrationCfg);
|
|
8512
8688
|
patchRedis(integrationCfg);
|
|
8513
8689
|
patchMongoDb(integrationCfg);
|
|
8690
|
+
await installMongoDbImportHook();
|
|
8514
8691
|
patchSendgridMail(integrationCfg);
|
|
8515
8692
|
patchMongooseIntegration(reproAutoMiddlewareConfig);
|
|
8516
8693
|
await initializeRuntimePrivacyPolicyAtStartup(reproAutoMiddlewareConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reproapp/node-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
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 && node test/mongodb-esm-import-hook.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
|
@@ -5,6 +5,7 @@ import { AsyncLocalStorage } from 'async_hooks';
|
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import { join } from 'path';
|
|
8
|
+
import { pathToFileURL } from 'url';
|
|
8
9
|
import {
|
|
9
10
|
configureIngestQueueDrain,
|
|
10
11
|
enqueueIngestEntries,
|
|
@@ -313,6 +314,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
|
|
|
313
314
|
const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
|
|
314
315
|
const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
|
|
315
316
|
const REPRO_QUERY_START_PARAM = '__repro_start';
|
|
317
|
+
const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
|
|
316
318
|
const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
|
|
317
319
|
const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
|
|
318
320
|
const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
|
|
@@ -4916,6 +4918,19 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4916
4918
|
const masking = normalizeMaskingConfig(cfg.masking);
|
|
4917
4919
|
patchMongoDb(cfg);
|
|
4918
4920
|
return async function (req: Request, res: Response, next: NextFunction) {
|
|
4921
|
+
const reqAny = req as any;
|
|
4922
|
+
if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
|
|
4923
|
+
return next();
|
|
4924
|
+
}
|
|
4925
|
+
try {
|
|
4926
|
+
Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
|
|
4927
|
+
value: true,
|
|
4928
|
+
configurable: true,
|
|
4929
|
+
});
|
|
4930
|
+
} catch {
|
|
4931
|
+
try { reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true; } catch {}
|
|
4932
|
+
}
|
|
4933
|
+
|
|
4919
4934
|
const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
|
|
4920
4935
|
const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
|
|
4921
4936
|
const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
|
|
@@ -4955,24 +4970,44 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
|
|
|
4955
4970
|
const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
|
|
4956
4971
|
beginSessionRequest(sid);
|
|
4957
4972
|
|
|
4958
|
-
// ---- response body capture
|
|
4973
|
+
// ---- response body capture ----
|
|
4959
4974
|
let capturedBody: any = undefined;
|
|
4960
|
-
const
|
|
4961
|
-
(
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4975
|
+
const originalJson = (res as any).json;
|
|
4976
|
+
if (typeof originalJson === 'function') {
|
|
4977
|
+
const origJson = originalJson.bind(res as any);
|
|
4978
|
+
(res as any).json = (body: any) => { capturedBody = body; return origJson(body); };
|
|
4979
|
+
}
|
|
4980
|
+
|
|
4981
|
+
const originalSend = (res as any).send;
|
|
4982
|
+
if (typeof originalSend === 'function') {
|
|
4983
|
+
const origSend = originalSend.bind(res as any);
|
|
4984
|
+
(res as any).send = (body: any) => {
|
|
4985
|
+
if (capturedBody === undefined) {
|
|
4986
|
+
capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
|
|
4987
|
+
}
|
|
4988
|
+
return origSend(body);
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4970
4991
|
|
|
4971
|
-
const origWrite = (res as any).write
|
|
4972
|
-
|
|
4992
|
+
const origWrite = typeof (res as any).write === 'function'
|
|
4993
|
+
? (res as any).write.bind(res as any)
|
|
4994
|
+
: null;
|
|
4995
|
+
const origEnd = typeof (res as any).end === 'function'
|
|
4996
|
+
? (res as any).end.bind(res as any)
|
|
4997
|
+
: null;
|
|
4973
4998
|
const chunks: Array<Buffer | string> = [];
|
|
4974
|
-
|
|
4975
|
-
|
|
4999
|
+
if (origWrite) {
|
|
5000
|
+
(res as any).write = (chunk: any, ...args: any[]) => {
|
|
5001
|
+
try { if (chunk != null) chunks.push(chunk); } catch {}
|
|
5002
|
+
return origWrite(chunk, ...args);
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
if (origEnd) {
|
|
5006
|
+
(res as any).end = (chunk?: any, ...args: any[]) => {
|
|
5007
|
+
try { if (chunk != null) chunks.push(chunk); } catch {}
|
|
5008
|
+
return origEnd(chunk, ...args);
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
4976
5011
|
|
|
4977
5012
|
// ---- our ALS (unchanged) ----
|
|
4978
5013
|
const tracerApiWithTrace = __TRACER__ as (TracerApi & { withTrace?: (id: string, fn: () => void) => void }) | null;
|
|
@@ -8776,6 +8811,8 @@ export type MongoDbPatchConfig = ReproMiddlewareConfig & {
|
|
|
8776
8811
|
|
|
8777
8812
|
const mongoDbPatchConfigs: MongoDbPatchConfig[] = [];
|
|
8778
8813
|
let mongoDbRequireHookInstalled = false;
|
|
8814
|
+
let mongoDbImportHookInstalled = false;
|
|
8815
|
+
let mongoDbImportHookReady: Promise<void> | null = null;
|
|
8779
8816
|
|
|
8780
8817
|
const MONGODB_PROMISE_OPS = [
|
|
8781
8818
|
'insertOne',
|
|
@@ -8798,6 +8835,12 @@ const MONGODB_PROMISE_OPS = [
|
|
|
8798
8835
|
const MONGODB_CURSOR_OPS = ['find', 'aggregate'] as const;
|
|
8799
8836
|
const MONGODB_CURSOR_TERMINALS = ['toArray', 'next', 'forEach'] as const;
|
|
8800
8837
|
|
|
8838
|
+
function registerMongoDbPatchConfig(cfg: MongoDbPatchConfig): void {
|
|
8839
|
+
if (!mongoDbPatchConfigs.includes(cfg)) {
|
|
8840
|
+
mongoDbPatchConfigs.push(cfg);
|
|
8841
|
+
}
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8801
8844
|
function resolveMongoDbRuntimeContext(cfg: MongoDbPatchConfig) {
|
|
8802
8845
|
const ctx = getCtx();
|
|
8803
8846
|
const fallback = cfg.resolveContext?.() ?? {};
|
|
@@ -8808,6 +8851,18 @@ function resolveMongoDbRuntimeContext(cfg: MongoDbPatchConfig) {
|
|
|
8808
8851
|
};
|
|
8809
8852
|
}
|
|
8810
8853
|
|
|
8854
|
+
function resolveMongoDbCaptureContext(): { cfg: MongoDbPatchConfig; sid: string; aid: string | null } | null {
|
|
8855
|
+
for (let i = mongoDbPatchConfigs.length - 1; i >= 0; i -= 1) {
|
|
8856
|
+
const cfg = mongoDbPatchConfigs[i];
|
|
8857
|
+
if (!cfg) continue;
|
|
8858
|
+
const { sid, aid } = resolveMongoDbRuntimeContext(cfg);
|
|
8859
|
+
if (sid) {
|
|
8860
|
+
return { cfg, sid, aid };
|
|
8861
|
+
}
|
|
8862
|
+
}
|
|
8863
|
+
return null;
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8811
8866
|
function resolveMongoCollectionName(collection: any): string {
|
|
8812
8867
|
const candidates = [
|
|
8813
8868
|
collection?.collectionName,
|
|
@@ -9108,10 +9163,11 @@ function patchMongoCollectionMethod(proto: any, op: string, cursorOp: boolean, c
|
|
|
9108
9163
|
return original.apply(this, args);
|
|
9109
9164
|
}
|
|
9110
9165
|
|
|
9111
|
-
const
|
|
9112
|
-
if (!
|
|
9166
|
+
const captureContext = resolveMongoDbCaptureContext();
|
|
9167
|
+
if (!captureContext) {
|
|
9113
9168
|
return original.apply(this, args);
|
|
9114
9169
|
}
|
|
9170
|
+
const { cfg: activeCfg, sid, aid } = captureContext;
|
|
9115
9171
|
|
|
9116
9172
|
const spanContext = captureSpanContextFromTracer(this);
|
|
9117
9173
|
const t0 = Date.now();
|
|
@@ -9123,7 +9179,7 @@ function patchMongoCollectionMethod(proto: any, op: string, cursorOp: boolean, c
|
|
|
9123
9179
|
const emitSuccess = (result: any) => {
|
|
9124
9180
|
if (emitted) return;
|
|
9125
9181
|
emitted = true;
|
|
9126
|
-
emitDbQuery(
|
|
9182
|
+
emitDbQuery(activeCfg, sid, aid ?? undefined, {
|
|
9127
9183
|
type: 'mongo',
|
|
9128
9184
|
collection,
|
|
9129
9185
|
op,
|
|
@@ -9140,7 +9196,7 @@ function patchMongoCollectionMethod(proto: any, op: string, cursorOp: boolean, c
|
|
|
9140
9196
|
const emitFailure = (error: any) => {
|
|
9141
9197
|
if (emitted) return;
|
|
9142
9198
|
emitted = true;
|
|
9143
|
-
emitDbQuery(
|
|
9199
|
+
emitDbQuery(activeCfg, sid, aid ?? undefined, {
|
|
9144
9200
|
type: 'mongo',
|
|
9145
9201
|
collection,
|
|
9146
9202
|
op,
|
|
@@ -9165,7 +9221,7 @@ function patchMongoCollectionMethod(proto: any, op: string, cursorOp: boolean, c
|
|
|
9165
9221
|
const result = original.apply(this, actualArgs);
|
|
9166
9222
|
if (cursorOp) {
|
|
9167
9223
|
return patchMongoCursorInstance(result, {
|
|
9168
|
-
cfg,
|
|
9224
|
+
cfg: activeCfg,
|
|
9169
9225
|
sid,
|
|
9170
9226
|
aid,
|
|
9171
9227
|
collection,
|
|
@@ -9249,6 +9305,7 @@ function patchMongoClientPrototype(proto: any, cfg: MongoDbPatchConfig): void {
|
|
|
9249
9305
|
}
|
|
9250
9306
|
|
|
9251
9307
|
function patchMongoDbFromModuleExports(mongoModule: any, cfg: MongoDbPatchConfig): void {
|
|
9308
|
+
registerMongoDbPatchConfig(cfg);
|
|
9252
9309
|
if (!mongoModule) return;
|
|
9253
9310
|
const constructors = new Set<any>();
|
|
9254
9311
|
const addCtor = (candidate: any) => {
|
|
@@ -9309,9 +9366,50 @@ function installMongoDbRequireHook(): void {
|
|
|
9309
9366
|
} catch {}
|
|
9310
9367
|
}
|
|
9311
9368
|
|
|
9369
|
+
function installMongoDbImportHook(): Promise<void> {
|
|
9370
|
+
if (mongoDbImportHookInstalled) return mongoDbImportHookReady ?? Promise.resolve();
|
|
9371
|
+
mongoDbImportHookInstalled = true;
|
|
9372
|
+
|
|
9373
|
+
mongoDbImportHookReady = (async () => {
|
|
9374
|
+
try {
|
|
9375
|
+
const nodeModule = requireOptionalHostModule('node:module') ?? requireOptionalHostModule('module');
|
|
9376
|
+
if (typeof nodeModule?.register !== 'function') return;
|
|
9377
|
+
|
|
9378
|
+
const iitm = requireOptionalHostModule('import-in-the-middle');
|
|
9379
|
+
const Hook = iitm?.Hook ?? iitm;
|
|
9380
|
+
if (typeof Hook !== 'function') return;
|
|
9381
|
+
|
|
9382
|
+
let waitForAllMessagesAcknowledged: undefined | (() => Promise<void>);
|
|
9383
|
+
let registerOptions: any = { data: { include: ['mongodb'] } };
|
|
9384
|
+
if (typeof iitm?.createAddHookMessageChannel === 'function') {
|
|
9385
|
+
const channel = iitm.createAddHookMessageChannel();
|
|
9386
|
+
registerOptions = channel.registerOptions;
|
|
9387
|
+
waitForAllMessagesAcknowledged = channel.waitForAllMessagesAcknowledged;
|
|
9388
|
+
}
|
|
9389
|
+
|
|
9390
|
+
nodeModule.register('import-in-the-middle/hook.mjs', pathToFileURL(__filename), registerOptions);
|
|
9391
|
+
|
|
9392
|
+
// eslint-disable-next-line no-new
|
|
9393
|
+
new Hook(['mongodb'], (exports: any) => {
|
|
9394
|
+
for (const cfg of mongoDbPatchConfigs) {
|
|
9395
|
+
patchMongoDbFromModuleExports(exports, cfg);
|
|
9396
|
+
}
|
|
9397
|
+
return exports;
|
|
9398
|
+
});
|
|
9399
|
+
|
|
9400
|
+
if (waitForAllMessagesAcknowledged) {
|
|
9401
|
+
await waitForAllMessagesAcknowledged();
|
|
9402
|
+
}
|
|
9403
|
+
} catch {}
|
|
9404
|
+
})();
|
|
9405
|
+
|
|
9406
|
+
return mongoDbImportHookReady;
|
|
9407
|
+
}
|
|
9408
|
+
|
|
9312
9409
|
export function patchMongoDb(cfg: MongoDbPatchConfig): void {
|
|
9313
|
-
|
|
9410
|
+
registerMongoDbPatchConfig(cfg);
|
|
9314
9411
|
installMongoDbRequireHook();
|
|
9412
|
+
void installMongoDbImportHook();
|
|
9315
9413
|
|
|
9316
9414
|
const mongoModules = new Set<any>();
|
|
9317
9415
|
for (const loadedMongoModule of getLoadedModuleExportsByPackage('mongodb')) {
|
|
@@ -9331,10 +9429,14 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
|
|
|
9331
9429
|
const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
|
|
9332
9430
|
const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
|
|
9333
9431
|
const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
|
|
9432
|
+
const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
|
|
9433
|
+
const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
|
|
9434
|
+
const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
|
|
9334
9435
|
const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
|
|
9335
9436
|
|
|
9336
9437
|
let reproAutoMiddlewareConfig: ReproMiddlewareConfig | null = null;
|
|
9337
9438
|
const mongoosePluginConfigs: ReproMiddlewareConfig[] = [];
|
|
9439
|
+
const httpRequestListenerWrappers = new WeakMap<Function, Function>();
|
|
9338
9440
|
let frameworkRequireHookInstalled = false;
|
|
9339
9441
|
let mongooseRequireHookInstalled = false;
|
|
9340
9442
|
|
|
@@ -9435,6 +9537,82 @@ function patchExpressFromModuleExports(expressModule: any): void {
|
|
|
9435
9537
|
application.listen = wrappedListen;
|
|
9436
9538
|
}
|
|
9437
9539
|
|
|
9540
|
+
function wrapHttpRequestListener(listener: any, cfg: ReproMiddlewareConfig): any {
|
|
9541
|
+
if (typeof listener !== 'function') return listener;
|
|
9542
|
+
if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED)) return listener;
|
|
9543
|
+
|
|
9544
|
+
const existing = httpRequestListenerWrappers.get(listener);
|
|
9545
|
+
if (existing) return existing;
|
|
9546
|
+
|
|
9547
|
+
const middleware = reproMiddleware(cfg);
|
|
9548
|
+
const wrapped = function reproPatchedHttpRequestListener(this: any, req: any, res: any) {
|
|
9549
|
+
return middleware(req, res, (err?: any) => {
|
|
9550
|
+
if (err) {
|
|
9551
|
+
try {
|
|
9552
|
+
if (!res.headersSent) {
|
|
9553
|
+
res.statusCode = 500;
|
|
9554
|
+
res.end('Internal Server Error');
|
|
9555
|
+
return undefined;
|
|
9556
|
+
}
|
|
9557
|
+
} catch {}
|
|
9558
|
+
throw err;
|
|
9559
|
+
}
|
|
9560
|
+
return listener.call(this, req, res);
|
|
9561
|
+
});
|
|
9562
|
+
};
|
|
9563
|
+
setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
|
|
9564
|
+
httpRequestListenerWrappers.set(listener, wrapped);
|
|
9565
|
+
return wrapped;
|
|
9566
|
+
}
|
|
9567
|
+
|
|
9568
|
+
function patchHttpServerRequestListenerMethods(server: any): void {
|
|
9569
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
9570
|
+
if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED)) return;
|
|
9571
|
+
|
|
9572
|
+
for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
|
|
9573
|
+
const original = server[methodName];
|
|
9574
|
+
if (typeof original !== 'function') continue;
|
|
9575
|
+
server[methodName] = function reproPatchedHttpServerListenerMethod(this: any, ...args: any[]) {
|
|
9576
|
+
if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
|
|
9577
|
+
args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
|
|
9578
|
+
}
|
|
9579
|
+
return original.apply(this, args);
|
|
9580
|
+
};
|
|
9581
|
+
}
|
|
9582
|
+
setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
|
|
9583
|
+
}
|
|
9584
|
+
|
|
9585
|
+
function patchHttpCreateServerFromModuleExports(httpModule: any): void {
|
|
9586
|
+
const original = httpModule?.createServer;
|
|
9587
|
+
if (!httpModule || typeof original !== 'function') return;
|
|
9588
|
+
if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED)) return;
|
|
9589
|
+
|
|
9590
|
+
const wrappedCreateServer = function reproPatchedHttpCreateServer(this: any, ...args: any[]) {
|
|
9591
|
+
const cfg = reproAutoMiddlewareConfig;
|
|
9592
|
+
if (cfg) {
|
|
9593
|
+
const listenerIndex =
|
|
9594
|
+
typeof args[0] === 'function'
|
|
9595
|
+
? 0
|
|
9596
|
+
: typeof args[1] === 'function'
|
|
9597
|
+
? 1
|
|
9598
|
+
: -1;
|
|
9599
|
+
if (listenerIndex >= 0) {
|
|
9600
|
+
args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
|
|
9601
|
+
}
|
|
9602
|
+
}
|
|
9603
|
+
const server = original.apply(this, args);
|
|
9604
|
+
patchHttpServerRequestListenerMethods(server);
|
|
9605
|
+
return server;
|
|
9606
|
+
};
|
|
9607
|
+
setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
|
|
9608
|
+
httpModule.createServer = wrappedCreateServer;
|
|
9609
|
+
}
|
|
9610
|
+
|
|
9611
|
+
function patchHttpServerAutoAttach(): void {
|
|
9612
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
|
|
9613
|
+
patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
|
|
9614
|
+
}
|
|
9615
|
+
|
|
9438
9616
|
function patchKnownFrameworkModules(): void {
|
|
9439
9617
|
const nestModules = new Set<any>();
|
|
9440
9618
|
const resolvedNest = requireOptionalHostModule('@nestjs/core');
|
|
@@ -9742,6 +9920,7 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
9742
9920
|
shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
|
|
9743
9921
|
|
|
9744
9922
|
installFrameworkRequireHook();
|
|
9923
|
+
patchHttpServerAutoAttach();
|
|
9745
9924
|
patchKnownFrameworkModules();
|
|
9746
9925
|
|
|
9747
9926
|
patchHttpClients({
|
|
@@ -9752,6 +9931,7 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
|
|
|
9752
9931
|
patchAmqplib(integrationCfg);
|
|
9753
9932
|
patchRedis(integrationCfg);
|
|
9754
9933
|
patchMongoDb(integrationCfg);
|
|
9934
|
+
await installMongoDbImportHook();
|
|
9755
9935
|
patchSendgridMail(integrationCfg);
|
|
9756
9936
|
patchMongooseIntegration(reproAutoMiddlewareConfig);
|
|
9757
9937
|
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
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 { pathToFileURL } = require('url');
|
|
11
|
+
const { join } = require('path');
|
|
12
|
+
const { mkdtempSync, mkdirSync, rmSync, writeFileSync } = require('fs');
|
|
13
|
+
const { tmpdir } = require('os');
|
|
14
|
+
const { initRepro, reproMiddleware, __reproTestHooks } = require('../dist');
|
|
15
|
+
const { flushIngestQueue } = require('../dist/ingest/client');
|
|
16
|
+
|
|
17
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
async function waitFor(predicate, timeoutMs = 3000) {
|
|
20
|
+
const deadline = Date.now() + timeoutMs;
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
if (predicate()) return;
|
|
23
|
+
await sleep(20);
|
|
24
|
+
}
|
|
25
|
+
assert(predicate(), 'timed out waiting for expected ingest payload');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function flattenEvents(posts) {
|
|
29
|
+
return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function dbChanges(events) {
|
|
33
|
+
return events
|
|
34
|
+
.filter((event) => event.event_type === 'db_change')
|
|
35
|
+
.flatMap((event) => Array.isArray(event.payload?.db) ? event.payload.db : []);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeReqRes() {
|
|
39
|
+
const req = new EventEmitter();
|
|
40
|
+
req.method = 'POST';
|
|
41
|
+
req.url = '/native-mongodb-esm';
|
|
42
|
+
req.originalUrl = '/native-mongodb-esm';
|
|
43
|
+
req.headers = {
|
|
44
|
+
'x-bug-session-id': 'S_mongo_esm',
|
|
45
|
+
'x-bug-action-id': 'A_mongo_esm',
|
|
46
|
+
};
|
|
47
|
+
req.query = {};
|
|
48
|
+
req.params = {};
|
|
49
|
+
req.body = undefined;
|
|
50
|
+
|
|
51
|
+
const res = new EventEmitter();
|
|
52
|
+
res.statusCode = 200;
|
|
53
|
+
res.getHeader = () => undefined;
|
|
54
|
+
res.getHeaders = () => ({});
|
|
55
|
+
res.write = () => true;
|
|
56
|
+
res.end = function end() {
|
|
57
|
+
this.emit('finish');
|
|
58
|
+
return this;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return { req, res };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createEsmMongoFixture() {
|
|
65
|
+
const root = mkdtempSync(join(tmpdir(), 'repro-node-sdk-esm-mongodb-'));
|
|
66
|
+
const packageDir = join(root, 'node_modules', 'mongodb');
|
|
67
|
+
mkdirSync(packageDir, { recursive: true });
|
|
68
|
+
writeFileSync(join(packageDir, 'package.json'), JSON.stringify({
|
|
69
|
+
name: 'mongodb',
|
|
70
|
+
version: '99.0.0-repro-fixture',
|
|
71
|
+
type: 'module',
|
|
72
|
+
exports: './index.mjs',
|
|
73
|
+
}, null, 2));
|
|
74
|
+
writeFileSync(join(packageDir, 'index.mjs'), `
|
|
75
|
+
export class Collection {
|
|
76
|
+
constructor(dbName, collectionName) {
|
|
77
|
+
this.dbName = dbName;
|
|
78
|
+
this.collectionName = collectionName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async insertOne(doc, options) {
|
|
82
|
+
if (options?.session?.id !== 'esm-native-session-1') {
|
|
83
|
+
throw new Error(\`unexpected session \${options?.session?.id}\`);
|
|
84
|
+
}
|
|
85
|
+
doc._id = doc._id ?? 'subject-esm-1';
|
|
86
|
+
return { acknowledged: true, insertedId: doc._id };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class Db {
|
|
91
|
+
constructor(dbName) {
|
|
92
|
+
this.databaseName = dbName;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
collection(collectionName) {
|
|
96
|
+
return new Collection(this.databaseName, collectionName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class MongoClient {
|
|
101
|
+
constructor(uri) {
|
|
102
|
+
this.uri = uri;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
db(dbName) {
|
|
106
|
+
return new Db(dbName);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`);
|
|
110
|
+
const appFile = join(root, 'app.mjs');
|
|
111
|
+
writeFileSync(appFile, `
|
|
112
|
+
import { MongoClient } from 'mongodb';
|
|
113
|
+
|
|
114
|
+
const dbClient = new MongoClient('mongodb://127.0.0.1:27017/clinical');
|
|
115
|
+
|
|
116
|
+
export async function insertSubject() {
|
|
117
|
+
const subjectsCollection = dbClient.db('clinical').collection('subjects');
|
|
118
|
+
return subjectsCollection.insertOne(
|
|
119
|
+
{ subjectNumber: '10013038', status: 'screened' },
|
|
120
|
+
{
|
|
121
|
+
session: {
|
|
122
|
+
id: 'esm-native-session-1',
|
|
123
|
+
hasEnded: false,
|
|
124
|
+
inTransaction: () => true,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
`);
|
|
130
|
+
return { root, appFile };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
const capturedBodies = [];
|
|
135
|
+
const originalFetch = global.fetch;
|
|
136
|
+
let fixtureRoot = null;
|
|
137
|
+
global.fetch = async (_url, init) => {
|
|
138
|
+
capturedBodies.push(JSON.parse(String(init?.body ?? '{}')));
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
status: 202,
|
|
142
|
+
json: async () => ({ ok: true }),
|
|
143
|
+
text: async () => '{"ok":true}',
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const cfg = {
|
|
149
|
+
tenantId: 'TENANT_test',
|
|
150
|
+
appId: 'APP_test',
|
|
151
|
+
appSecret: 'secret',
|
|
152
|
+
captureHeaders: false,
|
|
153
|
+
privacy: { environment: 'dev', captureRaw: true },
|
|
154
|
+
ingestBase: 'http://127.0.0.1:65535',
|
|
155
|
+
};
|
|
156
|
+
await initRepro(cfg);
|
|
157
|
+
|
|
158
|
+
const fixture = createEsmMongoFixture();
|
|
159
|
+
fixtureRoot = fixture.root;
|
|
160
|
+
const fixtureUrl = pathToFileURL(fixture.appFile).href;
|
|
161
|
+
const { insertSubject } = await import(`${fixtureUrl}?run=${Date.now()}`);
|
|
162
|
+
|
|
163
|
+
const middleware = reproMiddleware(cfg);
|
|
164
|
+
const { req, res } = makeReqRes();
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
middleware(req, res, (err) => {
|
|
167
|
+
if (err) {
|
|
168
|
+
reject(err);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
Promise.resolve(insertSubject()).then(resolve, reject);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
res.end();
|
|
175
|
+
|
|
176
|
+
await waitFor(() => dbChanges(flattenEvents(capturedBodies)).length === 1);
|
|
177
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
178
|
+
await flushIngestQueue();
|
|
179
|
+
|
|
180
|
+
const [change] = dbChanges(flattenEvents(capturedBodies));
|
|
181
|
+
assert.strictEqual(change.type, 'mongo');
|
|
182
|
+
assert.strictEqual(change.collection, 'subjects');
|
|
183
|
+
assert.strictEqual(change.op, 'insertOne');
|
|
184
|
+
assert.strictEqual(change.pk._id, 'subject-esm-1');
|
|
185
|
+
assert.strictEqual(change.query.driver, 'mongodb');
|
|
186
|
+
assert.strictEqual(change.query.db, 'clinical');
|
|
187
|
+
assert.strictEqual(change.query.doc.subjectNumber, '10013038');
|
|
188
|
+
assert.strictEqual(change.query.options.session.__kind, 'mongodb_client_session');
|
|
189
|
+
assert.strictEqual(change.query.options.session.id, 'esm-native-session-1');
|
|
190
|
+
assert.strictEqual(change.after.subjectNumber, '10013038');
|
|
191
|
+
assert.strictEqual(change.resultMeta.insertedId, 'subject-esm-1');
|
|
192
|
+
|
|
193
|
+
console.log('mongodb esm import hook OK');
|
|
194
|
+
} finally {
|
|
195
|
+
await __reproTestHooks.drainSdkBackgroundQueueForTest();
|
|
196
|
+
await flushIngestQueue();
|
|
197
|
+
if (fixtureRoot) {
|
|
198
|
+
try { rmSync(fixtureRoot, { recursive: true, force: true }); } catch {}
|
|
199
|
+
}
|
|
200
|
+
global.fetch = originalFetch;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
main().catch((error) => {
|
|
205
|
+
console.error(error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|