@reproapp/node-sdk 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -295,6 +295,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
295
295
  const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
296
296
  const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
297
297
  const REPRO_QUERY_START_PARAM = '__repro_start';
298
+ const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
298
299
  const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
299
300
  const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
300
301
  const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
@@ -4027,6 +4028,22 @@ function reproMiddleware(cfg) {
4027
4028
  const masking = normalizeMaskingConfig(cfg.masking);
4028
4029
  patchMongoDb(cfg);
4029
4030
  return async function (req, res, next) {
4031
+ const reqAny = req;
4032
+ if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
4033
+ return next();
4034
+ }
4035
+ try {
4036
+ Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
4037
+ value: true,
4038
+ configurable: true,
4039
+ });
4040
+ }
4041
+ catch {
4042
+ try {
4043
+ reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true;
4044
+ }
4045
+ catch { }
4046
+ }
4030
4047
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4031
4048
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
4032
4049
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
@@ -4063,30 +4080,50 @@ function reproMiddleware(cfg) {
4063
4080
  const maskReq = { method: String(req.method || 'GET').toUpperCase(), path: urlPathOnly, key };
4064
4081
  const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
4065
4082
  beginSessionRequest(sid);
4066
- // ---- response body capture (unchanged) ----
4083
+ // ---- response body capture ----
4067
4084
  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);
4085
+ const originalJson = res.json;
4086
+ if (typeof originalJson === 'function') {
4087
+ const origJson = originalJson.bind(res);
4088
+ res.json = (body) => { capturedBody = body; return origJson(body); };
4089
+ }
4090
+ const originalSend = res.send;
4091
+ if (typeof originalSend === 'function') {
4092
+ const origSend = originalSend.bind(res);
4093
+ res.send = (body) => {
4094
+ if (capturedBody === undefined) {
4095
+ capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
4096
+ }
4097
+ return origSend(body);
4098
+ };
4099
+ }
4100
+ const origWrite = typeof res.write === 'function'
4101
+ ? res.write.bind(res)
4102
+ : null;
4103
+ const origEnd = typeof res.end === 'function'
4104
+ ? res.end.bind(res)
4105
+ : null;
4079
4106
  const chunks = [];
4080
- res.write = (chunk, ...args) => { try {
4081
- if (chunk != null)
4082
- chunks.push(chunk);
4107
+ if (origWrite) {
4108
+ res.write = (chunk, ...args) => {
4109
+ try {
4110
+ if (chunk != null)
4111
+ chunks.push(chunk);
4112
+ }
4113
+ catch { }
4114
+ return origWrite(chunk, ...args);
4115
+ };
4083
4116
  }
4084
- catch { } return origWrite(chunk, ...args); };
4085
- res.end = (chunk, ...args) => { try {
4086
- if (chunk != null)
4087
- chunks.push(chunk);
4117
+ if (origEnd) {
4118
+ res.end = (chunk, ...args) => {
4119
+ try {
4120
+ if (chunk != null)
4121
+ chunks.push(chunk);
4122
+ }
4123
+ catch { }
4124
+ return origEnd(chunk, ...args);
4125
+ };
4088
4126
  }
4089
- catch { } return origEnd(chunk, ...args); };
4090
4127
  // ---- our ALS (unchanged) ----
4091
4128
  const tracerApiWithTrace = __TRACER__;
4092
4129
  const runInTrace = (fn) => {
@@ -8092,9 +8129,13 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
8092
8129
  const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
8093
8130
  const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
8094
8131
  const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
8132
+ const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
8133
+ const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
8134
+ const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
8095
8135
  const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
8096
8136
  let reproAutoMiddlewareConfig = null;
8097
8137
  const mongoosePluginConfigs = [];
8138
+ const httpRequestListenerWrappers = new WeakMap();
8098
8139
  let frameworkRequireHookInstalled = false;
8099
8140
  let mongooseRequireHookInstalled = false;
8100
8141
  function hasMarker(target, marker) {
@@ -8199,6 +8240,81 @@ function patchExpressFromModuleExports(expressModule) {
8199
8240
  setMarker(wrappedListen, REPRO_AUTO_EXPRESS_LISTEN_PATCHED);
8200
8241
  application.listen = wrappedListen;
8201
8242
  }
8243
+ function wrapHttpRequestListener(listener, cfg) {
8244
+ if (typeof listener !== 'function')
8245
+ return listener;
8246
+ if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED))
8247
+ return listener;
8248
+ const existing = httpRequestListenerWrappers.get(listener);
8249
+ if (existing)
8250
+ return existing;
8251
+ const middleware = reproMiddleware(cfg);
8252
+ const wrapped = function reproPatchedHttpRequestListener(req, res) {
8253
+ return middleware(req, res, (err) => {
8254
+ if (err) {
8255
+ try {
8256
+ if (!res.headersSent) {
8257
+ res.statusCode = 500;
8258
+ res.end('Internal Server Error');
8259
+ return undefined;
8260
+ }
8261
+ }
8262
+ catch { }
8263
+ throw err;
8264
+ }
8265
+ return listener.call(this, req, res);
8266
+ });
8267
+ };
8268
+ setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
8269
+ httpRequestListenerWrappers.set(listener, wrapped);
8270
+ return wrapped;
8271
+ }
8272
+ function patchHttpServerRequestListenerMethods(server) {
8273
+ const cfg = reproAutoMiddlewareConfig;
8274
+ if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED))
8275
+ return;
8276
+ for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
8277
+ const original = server[methodName];
8278
+ if (typeof original !== 'function')
8279
+ continue;
8280
+ server[methodName] = function reproPatchedHttpServerListenerMethod(...args) {
8281
+ if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
8282
+ args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
8283
+ }
8284
+ return original.apply(this, args);
8285
+ };
8286
+ }
8287
+ setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
8288
+ }
8289
+ function patchHttpCreateServerFromModuleExports(httpModule) {
8290
+ const original = httpModule?.createServer;
8291
+ if (!httpModule || typeof original !== 'function')
8292
+ return;
8293
+ if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED))
8294
+ return;
8295
+ const wrappedCreateServer = function reproPatchedHttpCreateServer(...args) {
8296
+ const cfg = reproAutoMiddlewareConfig;
8297
+ if (cfg) {
8298
+ const listenerIndex = typeof args[0] === 'function'
8299
+ ? 0
8300
+ : typeof args[1] === 'function'
8301
+ ? 1
8302
+ : -1;
8303
+ if (listenerIndex >= 0) {
8304
+ args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
8305
+ }
8306
+ }
8307
+ const server = original.apply(this, args);
8308
+ patchHttpServerRequestListenerMethods(server);
8309
+ return server;
8310
+ };
8311
+ setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
8312
+ httpModule.createServer = wrappedCreateServer;
8313
+ }
8314
+ function patchHttpServerAutoAttach() {
8315
+ patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
8316
+ patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
8317
+ }
8202
8318
  function patchKnownFrameworkModules() {
8203
8319
  const nestModules = new Set();
8204
8320
  const resolvedNest = requireOptionalHostModule('@nestjs/core');
@@ -8502,6 +8618,7 @@ async function initRepro(cfg) {
8502
8618
  };
8503
8619
  shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
8504
8620
  installFrameworkRequireHook();
8621
+ patchHttpServerAutoAttach();
8505
8622
  patchKnownFrameworkModules();
8506
8623
  patchHttpClients({
8507
8624
  resolveContext: cfg.resolveContext,
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.30",
4
4
  "description": "Repro Nest SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,7 @@
12
12
  "build": "tsc -p tsconfig.json",
13
13
  "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
14
14
  "prepublishOnly": "npm run build",
15
- "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js",
15
+ "test": "npm run build && node test/unawaited.test.js && node test/integration-unawaited.js && node test/request-flush-timing.test.js && node test/request-capture-background.test.js && node test/init-auto-http-server.test.js && node test/client-performance-regression.test.js && node test/request-manifest-priority.test.js && node test/runtime-no-double-invoke.test.js && node test/express-trace-http-args.test.js && node test/trace-batch-size.test.js && node test/sdk-background-priority.test.js && node test/ingest-queue-retention.test.js && node test/ingest-high-priority-retention.test.js && node test/ingest-critical-retention.test.js && node test/ingest-critical-retry-retention.test.js && node test/ingest-timeout.test.js && node -r ./tracer/register test/promise-map.test.js && node test/disable-subtree.test.js && node test/circular-capture.test.js && node test/wrap-plugin-arrow-args.test.js && node test/wrap-plugin-super-call.test.js && node test/function-arg-identity.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js",
16
16
  "test:containers": "npm run build && node test/runtime-no-double-invoke.testcontainers.test.js && node test/wrap-plugin-super-call.testcontainers.test.js && node test/function-arg-identity.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -313,6 +313,7 @@ const REQUEST_START_HEADER = 'x-bug-request-start';
313
313
  const REPRO_QUERY_SESSION_PARAM = '__repro_sid';
314
314
  const REPRO_QUERY_ACTION_PARAM = '__repro_aid';
315
315
  const REPRO_QUERY_START_PARAM = '__repro_start';
316
+ const REPRO_REQUEST_MIDDLEWARE_RAN = Symbol.for('__repro_request_middleware_ran');
316
317
  const KAFKA_HEADER_TRACE_ID = 'x-repro-trace-id';
317
318
  const KAFKA_HEADER_SPAN_ID = 'x-repro-span-id';
318
319
  const KAFKA_HEADER_PARENT_SPAN_ID = 'x-repro-parent-span-id';
@@ -4916,6 +4917,19 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4916
4917
  const masking = normalizeMaskingConfig(cfg.masking);
4917
4918
  patchMongoDb(cfg);
4918
4919
  return async function (req: Request, res: Response, next: NextFunction) {
4920
+ const reqAny = req as any;
4921
+ if (reqAny?.[REPRO_REQUEST_MIDDLEWARE_RAN]) {
4922
+ return next();
4923
+ }
4924
+ try {
4925
+ Object.defineProperty(reqAny, REPRO_REQUEST_MIDDLEWARE_RAN, {
4926
+ value: true,
4927
+ configurable: true,
4928
+ });
4929
+ } catch {
4930
+ try { reqAny[REPRO_REQUEST_MIDDLEWARE_RAN] = true; } catch {}
4931
+ }
4932
+
4919
4933
  const querySid = readRequestQueryParam(req, REPRO_QUERY_SESSION_PARAM);
4920
4934
  const queryAid = readRequestQueryParam(req, REPRO_QUERY_ACTION_PARAM);
4921
4935
  const queryTs = readRequestQueryNumber(req, REPRO_QUERY_START_PARAM);
@@ -4955,24 +4969,44 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4955
4969
  const requestHeaders = sanitizeHeaders(req.headers, cfg.captureHeaders);
4956
4970
  beginSessionRequest(sid);
4957
4971
 
4958
- // ---- response body capture (unchanged) ----
4972
+ // ---- response body capture ----
4959
4973
  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
- };
4974
+ const originalJson = (res as any).json;
4975
+ if (typeof originalJson === 'function') {
4976
+ const origJson = originalJson.bind(res as any);
4977
+ (res as any).json = (body: any) => { capturedBody = body; return origJson(body); };
4978
+ }
4979
+
4980
+ const originalSend = (res as any).send;
4981
+ if (typeof originalSend === 'function') {
4982
+ const origSend = originalSend.bind(res as any);
4983
+ (res as any).send = (body: any) => {
4984
+ if (capturedBody === undefined) {
4985
+ capturedBody = coerceBodyToStorable(body, res.getHeader?.('content-type'));
4986
+ }
4987
+ return origSend(body);
4988
+ };
4989
+ }
4970
4990
 
4971
- const origWrite = (res as any).write.bind(res as any);
4972
- const origEnd = (res as any).end.bind(res as any);
4991
+ const origWrite = typeof (res as any).write === 'function'
4992
+ ? (res as any).write.bind(res as any)
4993
+ : null;
4994
+ const origEnd = typeof (res as any).end === 'function'
4995
+ ? (res as any).end.bind(res as any)
4996
+ : null;
4973
4997
  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); };
4998
+ if (origWrite) {
4999
+ (res as any).write = (chunk: any, ...args: any[]) => {
5000
+ try { if (chunk != null) chunks.push(chunk); } catch {}
5001
+ return origWrite(chunk, ...args);
5002
+ };
5003
+ }
5004
+ if (origEnd) {
5005
+ (res as any).end = (chunk?: any, ...args: any[]) => {
5006
+ try { if (chunk != null) chunks.push(chunk); } catch {}
5007
+ return origEnd(chunk, ...args);
5008
+ };
5009
+ }
4976
5010
 
4977
5011
  // ---- our ALS (unchanged) ----
4978
5012
  const tracerApiWithTrace = __TRACER__ as (TracerApi & { withTrace?: (id: string, fn: () => void) => void }) | null;
@@ -9331,10 +9365,14 @@ const REPRO_AUTO_MIDDLEWARE_INSTALLED = Symbol.for('__repro_auto_middleware_inst
9331
9365
  const REPRO_AUTO_NEST_FACTORY_PATCHED = Symbol.for('__repro_auto_nest_factory_patched');
9332
9366
  const REPRO_AUTO_EXPRESS_LISTEN_PATCHED = Symbol.for('__repro_auto_express_listen_patched');
9333
9367
  const REPRO_AUTO_NEST_LISTEN_PATCHED = Symbol.for('__repro_auto_nest_listen_patched');
9368
+ const REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED = Symbol.for('__repro_auto_http_create_server_patched');
9369
+ const REPRO_AUTO_HTTP_SERVER_PATCHED = Symbol.for('__repro_auto_http_server_patched');
9370
+ const REPRO_AUTO_HTTP_LISTENER_WRAPPED = Symbol.for('__repro_auto_http_listener_wrapped');
9334
9371
  const REPRO_AUTO_MONGOOSE_PLUGIN_PATCHED = Symbol.for('__repro_auto_mongoose_plugin_patched');
9335
9372
 
9336
9373
  let reproAutoMiddlewareConfig: ReproMiddlewareConfig | null = null;
9337
9374
  const mongoosePluginConfigs: ReproMiddlewareConfig[] = [];
9375
+ const httpRequestListenerWrappers = new WeakMap<Function, Function>();
9338
9376
  let frameworkRequireHookInstalled = false;
9339
9377
  let mongooseRequireHookInstalled = false;
9340
9378
 
@@ -9435,6 +9473,82 @@ function patchExpressFromModuleExports(expressModule: any): void {
9435
9473
  application.listen = wrappedListen;
9436
9474
  }
9437
9475
 
9476
+ function wrapHttpRequestListener(listener: any, cfg: ReproMiddlewareConfig): any {
9477
+ if (typeof listener !== 'function') return listener;
9478
+ if (hasMarker(listener, REPRO_AUTO_HTTP_LISTENER_WRAPPED)) return listener;
9479
+
9480
+ const existing = httpRequestListenerWrappers.get(listener);
9481
+ if (existing) return existing;
9482
+
9483
+ const middleware = reproMiddleware(cfg);
9484
+ const wrapped = function reproPatchedHttpRequestListener(this: any, req: any, res: any) {
9485
+ return middleware(req, res, (err?: any) => {
9486
+ if (err) {
9487
+ try {
9488
+ if (!res.headersSent) {
9489
+ res.statusCode = 500;
9490
+ res.end('Internal Server Error');
9491
+ return undefined;
9492
+ }
9493
+ } catch {}
9494
+ throw err;
9495
+ }
9496
+ return listener.call(this, req, res);
9497
+ });
9498
+ };
9499
+ setMarker(wrapped, REPRO_AUTO_HTTP_LISTENER_WRAPPED);
9500
+ httpRequestListenerWrappers.set(listener, wrapped);
9501
+ return wrapped;
9502
+ }
9503
+
9504
+ function patchHttpServerRequestListenerMethods(server: any): void {
9505
+ const cfg = reproAutoMiddlewareConfig;
9506
+ if (!cfg || !server || hasMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED)) return;
9507
+
9508
+ for (const methodName of ['on', 'addListener', 'prependListener', 'once']) {
9509
+ const original = server[methodName];
9510
+ if (typeof original !== 'function') continue;
9511
+ server[methodName] = function reproPatchedHttpServerListenerMethod(this: any, ...args: any[]) {
9512
+ if (args[0] === 'request' && typeof args[1] === 'function' && reproAutoMiddlewareConfig) {
9513
+ args[1] = wrapHttpRequestListener(args[1], reproAutoMiddlewareConfig);
9514
+ }
9515
+ return original.apply(this, args);
9516
+ };
9517
+ }
9518
+ setMarker(server, REPRO_AUTO_HTTP_SERVER_PATCHED);
9519
+ }
9520
+
9521
+ function patchHttpCreateServerFromModuleExports(httpModule: any): void {
9522
+ const original = httpModule?.createServer;
9523
+ if (!httpModule || typeof original !== 'function') return;
9524
+ if (hasMarker(original, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED)) return;
9525
+
9526
+ const wrappedCreateServer = function reproPatchedHttpCreateServer(this: any, ...args: any[]) {
9527
+ const cfg = reproAutoMiddlewareConfig;
9528
+ if (cfg) {
9529
+ const listenerIndex =
9530
+ typeof args[0] === 'function'
9531
+ ? 0
9532
+ : typeof args[1] === 'function'
9533
+ ? 1
9534
+ : -1;
9535
+ if (listenerIndex >= 0) {
9536
+ args[listenerIndex] = wrapHttpRequestListener(args[listenerIndex], cfg);
9537
+ }
9538
+ }
9539
+ const server = original.apply(this, args);
9540
+ patchHttpServerRequestListenerMethods(server);
9541
+ return server;
9542
+ };
9543
+ setMarker(wrappedCreateServer, REPRO_AUTO_HTTP_CREATE_SERVER_PATCHED);
9544
+ httpModule.createServer = wrappedCreateServer;
9545
+ }
9546
+
9547
+ function patchHttpServerAutoAttach(): void {
9548
+ patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:http') ?? requireOptionalHostModule('http'));
9549
+ patchHttpCreateServerFromModuleExports(requireOptionalHostModule('node:https') ?? requireOptionalHostModule('https'));
9550
+ }
9551
+
9438
9552
  function patchKnownFrameworkModules(): void {
9439
9553
  const nestModules = new Set<any>();
9440
9554
  const resolvedNest = requireOptionalHostModule('@nestjs/core');
@@ -9742,6 +9856,7 @@ export async function initRepro(cfg: ReproInitConfig): Promise<void> {
9742
9856
  shareRuntimePrivacyState(reproAutoMiddlewareConfig, integrationCfg);
9743
9857
 
9744
9858
  installFrameworkRequireHook();
9859
+ patchHttpServerAutoAttach();
9745
9860
  patchKnownFrameworkModules();
9746
9861
 
9747
9862
  patchHttpClients({
@@ -0,0 +1,170 @@
1
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
2
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
3
+ process.env.TRACE_IDLE_FLUSH_MS = '10';
4
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
5
+
6
+ const assert = require('node:assert/strict');
7
+ const http = require('node:http');
8
+ const mongodb = require('mongodb');
9
+
10
+ const originalNativeMongoInsertOne = mongodb.Collection.prototype.insertOne;
11
+ mongodb.Collection.prototype.insertOne = async function reproFakeNativeMongoInsertOne(doc, options) {
12
+ assert.equal(options.session.id, 'auto-http-native-session-1');
13
+ doc._id = doc._id ?? 'subject-auto-http-1';
14
+ return { acknowledged: true, insertedId: doc._id };
15
+ };
16
+
17
+ const { initRepro, reproMiddleware } = require('../dist');
18
+ const { flushIngestQueue } = require('../dist/ingest/client');
19
+
20
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
+
22
+ async function waitFor(predicate, timeoutMs = 3000) {
23
+ const deadline = Date.now() + timeoutMs;
24
+ while (Date.now() < deadline) {
25
+ if (predicate()) return;
26
+ await sleep(25);
27
+ }
28
+ assert(predicate(), 'timed out waiting for expected condition');
29
+ }
30
+
31
+ function flattenEvents(posts) {
32
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
33
+ }
34
+
35
+ function backendRequests(posts, sessionId) {
36
+ return flattenEvents(posts).filter((event) => {
37
+ return event?.event_type === 'backend_request' && event?.session_id === sessionId;
38
+ });
39
+ }
40
+
41
+ function dbChanges(posts, sessionId) {
42
+ return flattenEvents(posts)
43
+ .filter((event) => event?.event_type === 'db_change' && event?.session_id === sessionId)
44
+ .flatMap((event) => Array.isArray(event?.payload?.db) ? event.payload.db : []);
45
+ }
46
+
47
+ async function listen(server) {
48
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
49
+ return server.address().port;
50
+ }
51
+
52
+ async function close(server) {
53
+ await new Promise((resolve, reject) => {
54
+ server.close((err) => (err ? reject(err) : resolve()));
55
+ });
56
+ }
57
+
58
+ async function request(port, sessionId, actionId) {
59
+ await new Promise((resolve, reject) => {
60
+ const req = http.request({
61
+ host: '127.0.0.1',
62
+ port,
63
+ path: '/subjects',
64
+ method: 'POST',
65
+ headers: {
66
+ 'content-type': 'application/json',
67
+ 'x-bug-session-id': sessionId,
68
+ 'x-bug-action-id': actionId,
69
+ 'x-bug-request-start': String(Date.now()),
70
+ },
71
+ }, (res) => {
72
+ res.resume();
73
+ res.on('end', resolve);
74
+ });
75
+ req.on('error', reject);
76
+ req.end(JSON.stringify({ ok: true }));
77
+ });
78
+ }
79
+
80
+ async function main() {
81
+ const capturedBodies = [];
82
+ const originalFetch = global.fetch;
83
+ global.fetch = async (_url, init = {}) => {
84
+ capturedBodies.push(JSON.parse(String(init.body || '{}')));
85
+ return {
86
+ ok: true,
87
+ status: 202,
88
+ json: async () => ({ ok: true }),
89
+ text: async () => '{"ok":true}',
90
+ };
91
+ };
92
+
93
+ const cfg = {
94
+ tenantId: 'TENANT_test',
95
+ appId: 'APP_test',
96
+ appSecret: 'secret',
97
+ appName: 'auto-http-test',
98
+ serviceName: 'Auto HTTP Test',
99
+ captureHeaders: false,
100
+ privacy: { environment: 'dev', captureRaw: true },
101
+ ingestBase: 'http://127.0.0.1:65535',
102
+ };
103
+
104
+ let autoServer;
105
+ let doubleServer;
106
+ try {
107
+ await initRepro(cfg);
108
+
109
+ const subjectsCollection = new mongodb.MongoClient('mongodb://127.0.0.1:27017/clinical')
110
+ .db('clinical')
111
+ .collection('subjects');
112
+
113
+ autoServer = http.createServer(async (_req, res) => {
114
+ await subjectsCollection.insertOne(
115
+ { subjectNumber: '10013038', status: 'screened' },
116
+ {
117
+ session: {
118
+ id: 'auto-http-native-session-1',
119
+ hasEnded: false,
120
+ inTransaction: () => true,
121
+ },
122
+ },
123
+ );
124
+ res.setHeader('content-type', 'application/json');
125
+ res.end(JSON.stringify({ ok: true, mode: 'auto' }));
126
+ });
127
+ const autoPort = await listen(autoServer);
128
+ await request(autoPort, 'S_auto_http_only', 'A_auto_http_only');
129
+ await waitFor(() => backendRequests(capturedBodies, 'S_auto_http_only').length === 1);
130
+ await waitFor(() => dbChanges(capturedBodies, 'S_auto_http_only').length === 1);
131
+ assert.equal(backendRequests(capturedBodies, 'S_auto_http_only').length, 1);
132
+ const [nativeMongoChange] = dbChanges(capturedBodies, 'S_auto_http_only');
133
+ assert.equal(nativeMongoChange.type, 'mongo');
134
+ assert.equal(nativeMongoChange.collection, 'subjects');
135
+ assert.equal(nativeMongoChange.op, 'insertOne');
136
+ assert.equal(nativeMongoChange.pk._id, 'subject-auto-http-1');
137
+ assert.equal(nativeMongoChange.query.driver, 'mongodb');
138
+ assert.equal(nativeMongoChange.query.db, 'clinical');
139
+ assert.equal(nativeMongoChange.query.doc.subjectNumber, '10013038');
140
+ assert.equal(nativeMongoChange.query.options.session.__kind, 'mongodb_client_session');
141
+ assert.equal(nativeMongoChange.query.options.session.id, 'auto-http-native-session-1');
142
+ assert.equal(nativeMongoChange.after.subjectNumber, '10013038');
143
+ assert.equal(nativeMongoChange.resultMeta.insertedId, 'subject-auto-http-1');
144
+
145
+ const manual = reproMiddleware(cfg);
146
+ doubleServer = http.createServer((req, res) => {
147
+ manual(req, res, () => {
148
+ res.setHeader('content-type', 'application/json');
149
+ res.end(JSON.stringify({ ok: true, mode: 'manual-too' }));
150
+ });
151
+ });
152
+ const doublePort = await listen(doubleServer);
153
+ await request(doublePort, 'S_auto_http_no_duplicate', 'A_auto_http_no_duplicate');
154
+ await waitFor(() => backendRequests(capturedBodies, 'S_auto_http_no_duplicate').length === 1);
155
+ assert.equal(backendRequests(capturedBodies, 'S_auto_http_no_duplicate').length, 1);
156
+
157
+ console.log('initRepro auto HTTP server middleware OK');
158
+ } finally {
159
+ if (autoServer?.listening) await close(autoServer);
160
+ if (doubleServer?.listening) await close(doubleServer);
161
+ await flushIngestQueue();
162
+ mongodb.Collection.prototype.insertOne = originalNativeMongoInsertOne;
163
+ global.fetch = originalFetch;
164
+ }
165
+ }
166
+
167
+ main().catch((error) => {
168
+ console.error(error);
169
+ process.exit(1);
170
+ });