@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 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 (unchanged) ----
4084
+ // ---- response body capture ----
4067
4085
  let capturedBody = undefined;
4068
- const origJson = res.json.bind(res);
4069
- res.json = (body) => { capturedBody = body; return origJson(body); };
4070
- const origSend = res.send.bind(res);
4071
- res.send = (body) => {
4072
- if (capturedBody === undefined) {
4073
- capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
4074
- }
4075
- return origSend(body);
4076
- };
4077
- const origWrite = res.write.bind(res);
4078
- const origEnd = res.end.bind(res);
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
- res.write = (chunk, ...args) => { try {
4081
- if (chunk != null)
4082
- chunks.push(chunk);
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
- catch { } return origWrite(chunk, ...args); };
4085
- res.end = (chunk, ...args) => { try {
4086
- if (chunk != null)
4087
- chunks.push(chunk);
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 { sid, aid } = resolveMongoDbRuntimeContext(cfg);
7878
- if (!sid) {
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(cfg, sid, aid ?? undefined, {
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(cfg, sid, aid ?? undefined, {
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
- mongoDbPatchConfigs.push(cfg);
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.29",
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 (unchanged) ----
4973
+ // ---- response body capture ----
4959
4974
  let capturedBody: any = undefined;
4960
- const origJson = res.json.bind(res as any);
4961
- (res as any).json = (body: any) => { capturedBody = body; return origJson(body); };
4962
-
4963
- const origSend = res.send.bind(res as any);
4964
- (res as any).send = (body: any) => {
4965
- if (capturedBody === undefined) {
4966
- capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
4967
- }
4968
- return origSend(body);
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.bind(res as any);
4972
- const origEnd = (res as any).end.bind(res as any);
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
- (res as any).write = (chunk: any, ...args: any[]) => { try { if (chunk != null) chunks.push(chunk); } catch {} return origWrite(chunk, ...args); };
4975
- (res as any).end = (chunk?: any, ...args: any[]) => { try { if (chunk != null) chunks.push(chunk); } catch {} return origEnd(chunk, ...args); };
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 { sid, aid } = resolveMongoDbRuntimeContext(cfg);
9112
- if (!sid) {
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(cfg, sid, aid ?? undefined, {
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(cfg, sid, aid ?? undefined, {
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
- mongoDbPatchConfigs.push(cfg);
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
+ });