@reproapp/node-sdk 0.0.44 → 0.0.46

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.d.ts CHANGED
@@ -52,6 +52,7 @@ export type TraceEventForFilter = {
52
52
  fn?: string;
53
53
  wrapperClass?: string | null;
54
54
  file?: string | null;
55
+ sourceFile?: string | null;
55
56
  line?: number | null;
56
57
  depth?: number;
57
58
  library?: string | null;
package/dist/index.js CHANGED
@@ -332,6 +332,7 @@ function refreshDisabledFunctionTraceRules() {
332
332
  let disabledFunctionTraceRules = computeDisabledFunctionTraceRules();
333
333
  let disabledFunctionTypePatterns = [];
334
334
  let disabledTraceFilePatterns = [];
335
+ let excludedInstrumentationFilePatterns = [];
335
336
  let __TRACE_LOG_PREF = null;
336
337
  let __FULL_VALUE_CAPTURE_ENABLED = true;
337
338
  function setInterceptorTracingEnabled(enabled) {
@@ -415,7 +416,7 @@ function matchesRule(rule, event) {
415
416
  const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
416
417
  if (!matchesPattern(event.wrapperClass, wrapperPattern))
417
418
  return false;
418
- if (!matchesPattern(event.file, rule.file))
419
+ if (!matchesTraceFile(event, rule.file))
419
420
  return false;
420
421
  if (!matchesPattern(event.line == null ? null : String(event.line), rule.line))
421
422
  return false;
@@ -430,9 +431,23 @@ function matchesRule(rule, event) {
430
431
  return false;
431
432
  return true;
432
433
  }
434
+ function matchesTraceFile(event, pattern, defaultWhenEmpty = true) {
435
+ if (pattern === undefined || pattern === null)
436
+ return defaultWhenEmpty;
437
+ const candidates = [event.file, event.sourceFile].filter((value) => typeof value === 'string' && value.length > 0);
438
+ if (!candidates.length) {
439
+ return matchesPattern(null, pattern, defaultWhenEmpty);
440
+ }
441
+ return candidates.some((value) => matchesPattern(value, pattern, false));
442
+ }
433
443
  function shouldDropTraceEvent(event) {
434
444
  if (disabledTraceFilePatterns.length) {
435
- if (matchesPattern(event.file, disabledTraceFilePatterns, false)) {
445
+ if (matchesTraceFile(event, disabledTraceFilePatterns, false)) {
446
+ return true;
447
+ }
448
+ }
449
+ if (excludedInstrumentationFilePatterns.length) {
450
+ if (matchesTraceFile(event, excludedInstrumentationFilePatterns, false)) {
436
451
  return true;
437
452
  }
438
453
  }
@@ -599,6 +614,9 @@ function initReproTracing(opts) {
599
614
  if (hasOwn(options, 'disableTraceFiles')) {
600
615
  setDisabledTraceFiles(options.disableTraceFiles ?? null);
601
616
  }
617
+ if (hasOwn(options, 'exclude')) {
618
+ excludedInstrumentationFilePatterns = normalizePatternArray(options.exclude);
619
+ }
602
620
  if (hasOwn(options, 'logFunctionCalls') && typeof options.logFunctionCalls === 'boolean') {
603
621
  setReproTraceLogsEnabled(options.logFunctionCalls);
604
622
  }
@@ -611,7 +629,17 @@ function initReproTracing(opts) {
611
629
  __TRACER__ = tracerPkg;
612
630
  applyTraceLogPreference(tracerPkg);
613
631
  const { disableFunctionTraces: _disableFunctionTraces, disableFunctionTypes: _disableFunctionTypes, disableTraceFiles: _disableTraceFiles, fullValueCapture: _fullValueCapture, logFunctionCalls: _logFunctionCalls, traceInterceptors: _traceInterceptors, ...rest } = options;
614
- const initOpts = { ...defaultTracerInitOpts(), ...rest };
632
+ const defaults = defaultTracerInitOpts();
633
+ const overrides = rest;
634
+ const initOpts = {
635
+ ...defaults,
636
+ ...overrides,
637
+ include: overrides.include ?? defaults.include,
638
+ exclude: [
639
+ ...(defaults.exclude ?? []),
640
+ ...(overrides.exclude ?? []),
641
+ ],
642
+ };
615
643
  tracerPkg.init?.(initOpts);
616
644
  tracerPkg.patchHttp?.();
617
645
  applyTraceLogPreference(tracerPkg);
@@ -736,6 +764,7 @@ function traceEventCandidateFromRecord(event) {
736
764
  fn: event.fn,
737
765
  wrapperClass: inferWrapperClassFromFn(event.fn),
738
766
  file: event.file ?? null,
767
+ sourceFile: sourceFileForLibrary,
739
768
  line: event.line ?? null,
740
769
  depth: event.depth,
741
770
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? event.file),
@@ -4875,11 +4904,11 @@ function reproMiddleware(cfg) {
4875
4904
  ?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), captureCfg, maskReq, endpointTraceCtx, masking, activePrivacy);
4876
4905
  const responseBody = responseBodyMaterialization.value;
4877
4906
  const requestValueEntries = [];
4878
- const bodyValueCapture = requestBodyMaterialization.skipped
4907
+ const bodyValueCapture = requestBodyRaw === undefined
4879
4908
  ? undefined
4880
4909
  : await maybeCaptureRequestValueAsync({
4881
4910
  target: 'request.body',
4882
- rawValue: req.body,
4911
+ rawValue: requestBodyRaw,
4883
4912
  previewValue: requestBody,
4884
4913
  capture: {
4885
4914
  runtimeConfig: captureCfg,
@@ -4929,7 +4958,7 @@ function reproMiddleware(cfg) {
4929
4958
  privacy: activePrivacy,
4930
4959
  },
4931
4960
  }, requestValueEntries);
4932
- const respBodyValueCapture = responseBodyMaterialization.skipped
4961
+ const respBodyValueCapture = capturedBody === undefined
4933
4962
  ? undefined
4934
4963
  : await maybeCaptureRequestValueAsync({
4935
4964
  target: 'response.body',
@@ -6336,6 +6365,7 @@ function recordKafkaTraceEvent(raw, sink, cfg, maskReq) {
6336
6365
  fn: raw.fn,
6337
6366
  wrapperClass: inferWrapperClassFromFn(raw.fn),
6338
6367
  file: raw.file ?? null,
6368
+ sourceFile: sourceFileForLibrary,
6339
6369
  line: typeof raw.line === 'number' ? raw.line : null,
6340
6370
  depth: typeof raw.depth === 'number' ? raw.depth : undefined,
6341
6371
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
@@ -6441,6 +6471,7 @@ function buildPendingKafkaTraceEvent(raw) {
6441
6471
  fn: raw.fn,
6442
6472
  wrapperClass: inferWrapperClassFromFn(raw.fn),
6443
6473
  file: raw.file ?? null,
6474
+ sourceFile: sourceFileForLibrary,
6444
6475
  line: typeof raw.line === 'number' ? raw.line : null,
6445
6476
  depth: typeof raw.depth === 'number' ? raw.depth : undefined,
6446
6477
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
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/init-auto-http-server.test.js && node test/backend-live-mode.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/runtime-service-registration.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-batching.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-service-scope.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-integration.test.js",
15
+ "test": "npm run build && node test/trace-source-exclude.test.js && 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/backend-live-mode.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/runtime-service-registration.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-batching.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-service-scope.test.js && node test/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js && node test/amqplib-integration.test.js && node test/mongodb-integration.test.js && node test/mongodb-esm-import-hook.test.js && node test/mongodb-suppression-cleanup.test.js && node test/postgresql-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 && node test/mongodb-mongoose-native-suppression.testcontainers.test.js && node test/postgresql-integrations.testcontainers.test.js"
17
17
  },
18
18
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -341,6 +341,7 @@ export type TraceEventForFilter = {
341
341
  fn?: string;
342
342
  wrapperClass?: string | null;
343
343
  file?: string | null;
344
+ sourceFile?: string | null;
344
345
  line?: number | null;
345
346
  depth?: number;
346
347
  library?: string | null;
@@ -606,6 +607,7 @@ function refreshDisabledFunctionTraceRules() {
606
607
  let disabledFunctionTraceRules: DisableFunctionTraceConfig[] = computeDisabledFunctionTraceRules();
607
608
  let disabledFunctionTypePatterns: Array<string | number | RegExp> = [];
608
609
  let disabledTraceFilePatterns: Array<string | number | RegExp> = [];
610
+ let excludedInstrumentationFilePatterns: Array<string | number | RegExp> = [];
609
611
  let __TRACE_LOG_PREF: boolean | null = null;
610
612
  let __FULL_VALUE_CAPTURE_ENABLED = true;
611
613
 
@@ -684,7 +686,7 @@ function matchesRule(rule: DisableFunctionTraceRule, event: TraceEventForFilter)
684
686
  const wrapperPattern = rule.wrapper ?? rule.wrapperClass ?? rule.className ?? rule.owner;
685
687
  if (!matchesPattern(event.wrapperClass, wrapperPattern)) return false;
686
688
 
687
- if (!matchesPattern(event.file, rule.file)) return false;
689
+ if (!matchesTraceFile(event, rule.file)) return false;
688
690
  if (!matchesPattern(event.line == null ? null : String(event.line), rule.line)) return false;
689
691
 
690
692
  const libPattern = rule.lib ?? rule.library;
@@ -699,9 +701,29 @@ function matchesRule(rule: DisableFunctionTraceRule, event: TraceEventForFilter)
699
701
  return true;
700
702
  }
701
703
 
704
+ function matchesTraceFile(
705
+ event: Pick<TraceEventForFilter, 'file' | 'sourceFile'>,
706
+ pattern?: TraceRulePattern,
707
+ defaultWhenEmpty: boolean = true,
708
+ ): boolean {
709
+ if (pattern === undefined || pattern === null) return defaultWhenEmpty;
710
+ const candidates = [event.file, event.sourceFile].filter(
711
+ (value): value is string => typeof value === 'string' && value.length > 0,
712
+ );
713
+ if (!candidates.length) {
714
+ return matchesPattern(null, pattern, defaultWhenEmpty);
715
+ }
716
+ return candidates.some((value) => matchesPattern(value, pattern, false));
717
+ }
718
+
702
719
  function shouldDropTraceEvent(event: TraceEventForFilter): boolean {
703
720
  if (disabledTraceFilePatterns.length) {
704
- if (matchesPattern(event.file, disabledTraceFilePatterns, false)) {
721
+ if (matchesTraceFile(event, disabledTraceFilePatterns, false)) {
722
+ return true;
723
+ }
724
+ }
725
+ if (excludedInstrumentationFilePatterns.length) {
726
+ if (matchesTraceFile(event, excludedInstrumentationFilePatterns, false)) {
705
727
  return true;
706
728
  }
707
729
  }
@@ -948,6 +970,9 @@ export function initReproTracing(opts?: ReproTracingInitOptions) {
948
970
  if (hasOwn(options, 'disableTraceFiles')) {
949
971
  setDisabledTraceFiles(options.disableTraceFiles ?? null);
950
972
  }
973
+ if (hasOwn(options, 'exclude')) {
974
+ excludedInstrumentationFilePatterns = normalizePatternArray(options.exclude);
975
+ }
951
976
  if (hasOwn(options, 'logFunctionCalls') && typeof options.logFunctionCalls === 'boolean') {
952
977
  setReproTraceLogsEnabled(options.logFunctionCalls);
953
978
  }
@@ -971,7 +996,17 @@ export function initReproTracing(opts?: ReproTracingInitOptions) {
971
996
  traceInterceptors: _traceInterceptors,
972
997
  ...rest
973
998
  } = options;
974
- const initOpts = { ...defaultTracerInitOpts(), ...(rest as TracerInitOpts) };
999
+ const defaults = defaultTracerInitOpts();
1000
+ const overrides = rest as TracerInitOpts;
1001
+ const initOpts = {
1002
+ ...defaults,
1003
+ ...overrides,
1004
+ include: overrides.include ?? defaults.include,
1005
+ exclude: [
1006
+ ...(defaults.exclude ?? []),
1007
+ ...(overrides.exclude ?? []),
1008
+ ],
1009
+ };
975
1010
  tracerPkg.init?.(initOpts);
976
1011
  tracerPkg.patchHttp?.();
977
1012
  applyTraceLogPreference(tracerPkg);
@@ -1094,6 +1129,7 @@ function traceEventCandidateFromRecord(event: PendingTraceEventRecord): TraceEve
1094
1129
  fn: event.fn,
1095
1130
  wrapperClass: inferWrapperClassFromFn(event.fn),
1096
1131
  file: event.file ?? null,
1132
+ sourceFile: sourceFileForLibrary,
1097
1133
  line: event.line ?? null,
1098
1134
  depth: event.depth,
1099
1135
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? event.file),
@@ -6036,11 +6072,11 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6036
6072
  );
6037
6073
  const responseBody = responseBodyMaterialization.value;
6038
6074
  const requestValueEntries: TraceValueBatchEntry[] = [];
6039
- const bodyValueCapture = requestBodyMaterialization.skipped
6075
+ const bodyValueCapture = requestBodyRaw === undefined
6040
6076
  ? undefined
6041
6077
  : await maybeCaptureRequestValueAsync({
6042
6078
  target: 'request.body',
6043
- rawValue: (req as any).body,
6079
+ rawValue: requestBodyRaw,
6044
6080
  previewValue: requestBody,
6045
6081
  capture: {
6046
6082
  runtimeConfig: captureCfg,
@@ -6090,7 +6126,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
6090
6126
  privacy: activePrivacy,
6091
6127
  },
6092
6128
  }, requestValueEntries);
6093
- const respBodyValueCapture = responseBodyMaterialization.skipped
6129
+ const respBodyValueCapture = capturedBody === undefined
6094
6130
  ? undefined
6095
6131
  : await maybeCaptureRequestValueAsync({
6096
6132
  target: 'response.body',
@@ -7595,6 +7631,7 @@ function recordKafkaTraceEvent(
7595
7631
  fn: raw.fn,
7596
7632
  wrapperClass: inferWrapperClassFromFn(raw.fn),
7597
7633
  file: raw.file ?? null,
7634
+ sourceFile: sourceFileForLibrary,
7598
7635
  line: typeof raw.line === 'number' ? raw.line : null,
7599
7636
  depth: typeof raw.depth === 'number' ? raw.depth : undefined,
7600
7637
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
@@ -7727,6 +7764,7 @@ function buildPendingKafkaTraceEvent(raw: any): PendingTraceEventRecord | null {
7727
7764
  fn: raw.fn,
7728
7765
  wrapperClass: inferWrapperClassFromFn(raw.fn),
7729
7766
  file: raw.file ?? null,
7767
+ sourceFile: sourceFileForLibrary,
7730
7768
  line: typeof raw.line === 'number' ? raw.line : null,
7731
7769
  depth: typeof raw.depth === 'number' ? raw.depth : undefined,
7732
7770
  library: inferLibraryNameFromFile(sourceFileForLibrary ?? raw.file),
@@ -142,6 +142,8 @@ async function main() {
142
142
  }
143
143
 
144
144
  const large = makeReqRes('S_perf_large_payload', 'A_perf_large_payload', '/large-payload');
145
+ large.req.method = 'POST';
146
+ large.req.body = largePayload();
145
147
  await invokeMiddleware(middleware, large.req, large.res, async () => {
146
148
  const traced = global.__repro_call(
147
149
  largeTraceReturn,
@@ -160,12 +162,48 @@ async function main() {
160
162
 
161
163
  const backendRequest = eventsOf(capturedBodies, 'backend_request')[0]?.payload?.request;
162
164
  assert(backendRequest, JSON.stringify(capturedBodies));
165
+ assert.equal(backendRequest.body, undefined, JSON.stringify(backendRequest));
166
+ assert.equal(
167
+ backendRequest.bodyMaterialization?.reason,
168
+ 'inline-size-limit',
169
+ JSON.stringify(backendRequest),
170
+ );
171
+ assert.equal(
172
+ backendRequest.bodyValueCapture?.status,
173
+ 'stored',
174
+ JSON.stringify(backendRequest),
175
+ );
163
176
  assert.equal(backendRequest.respBody, undefined, JSON.stringify(backendRequest));
164
177
  assert.equal(
165
178
  backendRequest.respBodyMaterialization?.reason,
166
179
  'inline-size-limit',
167
180
  JSON.stringify(backendRequest),
168
181
  );
182
+ assert.equal(
183
+ backendRequest.respBodyValueCapture?.status,
184
+ 'stored',
185
+ JSON.stringify(backendRequest),
186
+ );
187
+
188
+ const requestValues = eventsOf(capturedBodies, 'backend_request')
189
+ .flatMap((event) => Array.isArray(event?.payload?.requestValues) ? event.payload.requestValues : []);
190
+ const capturedRequestBody = requestValues.find((entry) =>
191
+ entry?.id === backendRequest.bodyValueCapture.valueId &&
192
+ entry?.target === 'request.body'
193
+ );
194
+ const capturedResponseBody = requestValues.find((entry) =>
195
+ entry?.id === backendRequest.respBodyValueCapture.valueId &&
196
+ entry?.target === 'response.body'
197
+ );
198
+ assert(capturedRequestBody, JSON.stringify(requestValues));
199
+ assert(capturedResponseBody, JSON.stringify(requestValues));
200
+ assert.equal(capturedRequestBody.value?.rows?.length, 400, JSON.stringify(capturedRequestBody));
201
+ assert.equal(capturedResponseBody.value?.rows?.length, 400, JSON.stringify(capturedResponseBody));
202
+ assert.equal(
203
+ capturedResponseBody.value.rows[399].depot,
204
+ 'depot-399',
205
+ JSON.stringify(capturedResponseBody),
206
+ );
169
207
 
170
208
  const traceEvents = eventsOf(capturedBodies, 'trace_batch')
171
209
  .flatMap((event) => Array.isArray(event?.payload?.trace) ? event.payload.trace : []);
@@ -307,7 +307,7 @@ async function testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest() {
307
307
  }
308
308
  }
309
309
 
310
- async function testLargeHttpResponseBodyKeepsRequestMetadataWithoutInlineBody() {
310
+ async function testLargeHttpResponseBodyKeepsFullValueOutOfBand() {
311
311
  const marker = 'OVERSIZED_HTTP_RESPONSE_MARKER';
312
312
  const capturedBodies = [];
313
313
  const originalFetch = global.fetch;
@@ -344,13 +344,28 @@ async function testLargeHttpResponseBodyKeepsRequestMetadataWithoutInlineBody()
344
344
  return text.includes('"event_type":"backend_request"') &&
345
345
  text.includes('"reason":"inline-size-limit"');
346
346
  }));
347
- const text = JSON.stringify(capturedBodies);
348
- assert(!text.includes(marker), text);
349
- assert(text.includes('respBodyMaterialization'), text);
350
- assert(text.includes('"target":"response.body"'), text);
351
- assert(!text.includes('__type":"json-string'), text);
352
- assert(!text.includes('__truncatedKeys'), text);
353
- assert(!text.includes('more chars'), text);
347
+ const backendEvent = capturedBodies
348
+ .flatMap((body) => Array.isArray(body?.events) ? body.events : [])
349
+ .find((event) => event?.event_type === 'backend_request');
350
+ const request = backendEvent?.payload?.request;
351
+ const requestValues = backendEvent?.payload?.requestValues;
352
+ assert(request, JSON.stringify(capturedBodies));
353
+ assert.strictEqual(request.respBody, undefined, JSON.stringify(request));
354
+ assert.strictEqual(request.respBodyMaterialization?.reason, 'inline-size-limit');
355
+ assert.strictEqual(request.respBodyValueCapture?.status, 'stored');
356
+ assert(!JSON.stringify(request).includes(marker), JSON.stringify(request));
357
+ const capturedResponse = requestValues?.find((entry) =>
358
+ entry?.id === request.respBodyValueCapture.valueId &&
359
+ entry?.target === 'response.body'
360
+ );
361
+ assert(capturedResponse, JSON.stringify(requestValues));
362
+ assert.strictEqual(
363
+ capturedResponse.value?.payload,
364
+ `${marker}:${'x'.repeat(12 * 1024)}`,
365
+ );
366
+ assert(!JSON.stringify(request).includes('__type":"json-string'), JSON.stringify(request));
367
+ assert(!JSON.stringify(request).includes('__truncatedKeys'), JSON.stringify(request));
368
+ assert(!JSON.stringify(request).includes('more chars'), JSON.stringify(request));
354
369
  } finally {
355
370
  await flushIngestQueue();
356
371
  global.fetch = originalFetch;
@@ -746,7 +761,7 @@ async function main() {
746
761
  await testKafkaConsumerTraceUsesConsumeTraceId();
747
762
  await testKafkaProducerCapturesFullPublishedMessageValue();
748
763
  await testKafkaTraceFlushIsNotBlockedForeverByActiveHttpRequest();
749
- await testLargeHttpResponseBodyKeepsRequestMetadataWithoutInlineBody();
764
+ await testLargeHttpResponseBodyKeepsFullValueOutOfBand();
750
765
  await testJsonBuiltinTracePayloadsAreOmitted();
751
766
  await testLargeStringTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
752
767
  await testLargeStructuredTraceValuesStayIngestibleAndCaptureFullValueOutOfBand();
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+
3
+ process.env.TRACE_LINGER_AFTER_FINISH_MS = '0';
4
+ process.env.TRACE_IDLE_FLUSH_MS = '25';
5
+ process.env.SESSION_DRAIN_TIMEOUT_MS = '0';
6
+ process.env.REPRO_SDK_BACKGROUND_IDLE_DELAY_MS = '25';
7
+
8
+ const assert = require('node:assert');
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const { EventEmitter } = require('node:events');
12
+ const {
13
+ initReproTracing,
14
+ reproMiddleware,
15
+ } = require('../dist');
16
+
17
+ const excludeTenantModule =
18
+ /src[\\/]modules[\\/]core[\\/]database[\\/]modules[\\/]tenant[\\/]tenant-database\.module\.ts$/;
19
+
20
+ function writeFixture(root) {
21
+ const distRoot = path.join(root, 'dist');
22
+ const tenantDistDir = path.join(
23
+ distRoot,
24
+ 'modules/core/database/modules/tenant',
25
+ );
26
+ const tenantSourceFile = path.join(
27
+ root,
28
+ 'src/modules/core/database/modules/tenant/tenant-database.module.ts',
29
+ );
30
+ const tenantDistFile = path.join(tenantDistDir, 'tenant-database.module.js');
31
+ const tenantMapFile = `${tenantDistFile}.map`;
32
+ const callerFile = path.join(distRoot, 'caller.js');
33
+
34
+ fs.mkdirSync(tenantDistDir, { recursive: true });
35
+ fs.writeFileSync(
36
+ tenantDistFile,
37
+ [
38
+ "'use strict';",
39
+ 'function connectTenantDatabase() {',
40
+ " return 'connected';",
41
+ '}',
42
+ 'module.exports = { connectTenantDatabase };',
43
+ '//# sourceMappingURL=tenant-database.module.js.map',
44
+ '',
45
+ ].join('\n'),
46
+ );
47
+ fs.writeFileSync(
48
+ tenantMapFile,
49
+ JSON.stringify({
50
+ version: 3,
51
+ file: path.basename(tenantDistFile),
52
+ sourceRoot: '',
53
+ sources: [tenantSourceFile],
54
+ names: [],
55
+ mappings: '',
56
+ }),
57
+ );
58
+ fs.writeFileSync(
59
+ callerFile,
60
+ [
61
+ "'use strict';",
62
+ "const tenant = require('./modules/core/database/modules/tenant/tenant-database.module');",
63
+ 'function invokeTenantDatabase() {',
64
+ ' return tenant.connectTenantDatabase();',
65
+ '}',
66
+ 'module.exports = { invokeTenantDatabase, tenant };',
67
+ '',
68
+ ].join('\n'),
69
+ );
70
+
71
+ return { callerFile, tenantSourceFile };
72
+ }
73
+
74
+ function makeReqRes() {
75
+ const req = new EventEmitter();
76
+ req.method = 'GET';
77
+ req.url = '/source-exclude';
78
+ req.headers = {
79
+ 'x-bug-session-id': 'S_trace_source_exclude',
80
+ 'x-bug-action-id': 'A_trace_source_exclude',
81
+ 'x-bug-request-start': String(Date.now()),
82
+ };
83
+ req.params = {};
84
+ req.query = {};
85
+
86
+ const res = new EventEmitter();
87
+ res.statusCode = 200;
88
+ res.getHeader = () => undefined;
89
+ res.setHeader = () => {};
90
+ res.json = function json(body) {
91
+ this.body = body;
92
+ this.emit('finish');
93
+ return body;
94
+ };
95
+ res.send = res.json;
96
+ res.write = () => true;
97
+ res.end = () => {
98
+ res.emit('finish');
99
+ return true;
100
+ };
101
+
102
+ return { req, res };
103
+ }
104
+
105
+ function traceEventsFromPosts(posts) {
106
+ const traces = [];
107
+ const append = (value) => {
108
+ if (Array.isArray(value)) {
109
+ traces.push(...value);
110
+ return;
111
+ }
112
+ if (typeof value !== 'string') return;
113
+ try {
114
+ const parsed = JSON.parse(value);
115
+ if (Array.isArray(parsed)) traces.push(...parsed);
116
+ } catch {}
117
+ };
118
+
119
+ for (const post of posts) {
120
+ let payload;
121
+ try {
122
+ payload = JSON.parse(String(post.body || ''));
123
+ } catch {
124
+ continue;
125
+ }
126
+ for (const entry of Array.isArray(payload.entries) ? payload.entries : []) {
127
+ append(entry?.trace);
128
+ append(entry?.request?.trace);
129
+ }
130
+ for (const event of Array.isArray(payload.events) ? payload.events : []) {
131
+ append(event?.payload?.trace);
132
+ append(event?.payload?.request?.trace);
133
+ }
134
+ }
135
+
136
+ return traces;
137
+ }
138
+
139
+ async function main() {
140
+ const fixtureRoot = fs.mkdtempSync(path.join(__dirname, '.tmp-source-exclude-'));
141
+ const posts = [];
142
+ const originalFetch = global.fetch;
143
+
144
+ try {
145
+ const fixture = writeFixture(fixtureRoot);
146
+ const includeFixture = new RegExp(
147
+ `^${fixtureRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
148
+ );
149
+
150
+ global.fetch = async (url, options = {}) => {
151
+ posts.push({ url: String(url), body: options.body });
152
+ return {
153
+ ok: true,
154
+ status: 200,
155
+ json: async () => ({ ok: true }),
156
+ text: async () => '{"ok":true}',
157
+ };
158
+ };
159
+
160
+ initReproTracing({
161
+ include: [includeFixture],
162
+ exclude: [excludeTenantModule],
163
+ logFunctionCalls: false,
164
+ });
165
+
166
+ const caller = require(fixture.callerFile);
167
+ assert.strictEqual(
168
+ caller.tenant.connectTenantDatabase.__repro_instrumented,
169
+ undefined,
170
+ 'source-mapped excluded module must not be instrumented',
171
+ );
172
+ assert.strictEqual(
173
+ caller.invokeTenantDatabase.__repro_instrumented,
174
+ true,
175
+ 'non-excluded caller must remain instrumented',
176
+ );
177
+
178
+ const middleware = reproMiddleware({
179
+ appId: 'APP_trace_source_exclude',
180
+ tenantId: 'TENANT_trace_source_exclude',
181
+ appSecret: 'secret',
182
+ captureHeaders: false,
183
+ });
184
+ const { req, res } = makeReqRes();
185
+ await new Promise((resolve, reject) => {
186
+ middleware(req, res, () => {
187
+ try {
188
+ assert.strictEqual(caller.invokeTenantDatabase(), 'connected');
189
+ res.json({ ok: true });
190
+ resolve();
191
+ } catch (error) {
192
+ reject(error);
193
+ }
194
+ });
195
+ });
196
+ await new Promise((resolve) => setTimeout(resolve, 500));
197
+
198
+ const traces = traceEventsFromPosts(posts);
199
+ assert(
200
+ traces.some((event) => event?.fn === 'invokeTenantDatabase'),
201
+ `expected caller trace, received ${JSON.stringify(traces)}`,
202
+ );
203
+ assert(
204
+ !traces.some(
205
+ (event) =>
206
+ event?.fn === 'connectTenantDatabase' ||
207
+ event?.sourceFile === fixture.tenantSourceFile,
208
+ ),
209
+ `excluded source leaked into trace payload: ${JSON.stringify(traces)}`,
210
+ );
211
+
212
+ console.log('source-mapped tracing exclusion OK');
213
+ } finally {
214
+ global.fetch = originalFetch;
215
+ fs.rmSync(fixtureRoot, { recursive: true, force: true });
216
+ }
217
+ }
218
+
219
+ main().catch((error) => {
220
+ console.error(error);
221
+ process.exitCode = 1;
222
+ });
@@ -31,16 +31,19 @@ function tagExports(value, filename, seen = new WeakSet(), depth = 0, instrument
31
31
 
32
32
  if (typeof value === 'function') {
33
33
  try {
34
- if (!value[SYM_SRC_FILE]) {
34
+ const existingSourceFile = value[SYM_SRC_FILE];
35
+ const belongsToCurrentModule =
36
+ !existingSourceFile || toPosix(existingSourceFile) === toPosix(filename);
37
+ if (!existingSourceFile) {
35
38
  Object.defineProperty(value, SYM_SRC_FILE, { value: filename, configurable: true });
36
39
  }
37
- if (value[SYM_IS_APP] !== isApp) {
40
+ if (belongsToCurrentModule && value[SYM_IS_APP] !== isApp) {
38
41
  Object.defineProperty(value, SYM_IS_APP, { value: isApp, configurable: true });
39
42
  }
40
- if (instrumented && value.__repro_instrumented !== true) {
43
+ if (instrumented && belongsToCurrentModule && value.__repro_instrumented !== true) {
41
44
  Object.defineProperty(value, '__repro_instrumented', { value: true, configurable: true });
42
45
  }
43
- if (instrumented && value[SYM_BODY_TRACED] !== true) {
46
+ if (instrumented && belongsToCurrentModule && value[SYM_BODY_TRACED] !== true) {
44
47
  Object.defineProperty(value, SYM_BODY_TRACED, { value: true, configurable: true });
45
48
  }
46
49
  } catch {}
@@ -82,10 +85,24 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
82
85
  : [ new RegExp('^' + escapeRx(CWD + '/')) ];
83
86
  const exc = Array.isArray(exclude) ? exclude : [];
84
87
 
85
- const shouldHandle = (f) => {
86
- const s = String(f || '').replace(/\\/g, '/');
87
- if (exc.some(rx => rx.test(s))) return false;
88
- return inc.some(rx => rx.test(s));
88
+ const matchesAny = (patterns, file) => {
89
+ const normalized = toPosix(file);
90
+ return patterns.some((rx) => {
91
+ try {
92
+ rx.lastIndex = 0;
93
+ const matched = rx.test(normalized);
94
+ rx.lastIndex = 0;
95
+ return matched;
96
+ } catch {
97
+ return false;
98
+ }
99
+ });
100
+ };
101
+
102
+ const shouldHandle = (physicalFile, sourceFile) => {
103
+ const candidates = [physicalFile, sourceFile].filter(Boolean);
104
+ if (candidates.some(file => matchesAny(exc, file))) return false;
105
+ return candidates.some(file => matchesAny(inc, file));
89
106
  };
90
107
 
91
108
  // ---- Global hook: intercept *all* compiles, regardless of how .ts was compiled ----
@@ -100,8 +117,7 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
100
117
  if (transformedFiles.has(filename)) {
101
118
  // Already transformed once; avoid double-transforming code that may already include __repro_call wrappers.
102
119
  out = code;
103
- } else if (shouldHandle(filename) && isAppFile(filename)) {
104
- try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
120
+ } else if (isAppFile(filename)) {
105
121
  const sourceInfo = getSourceInfo(code, filename);
106
122
  if (sourceInfo?.metaFilename) {
107
123
  metaFilename = sourceInfo.metaFilename;
@@ -109,35 +125,38 @@ function installCJS({ include, exclude, parserPlugins } = {}) {
109
125
  if (typeof sourceInfo?.mapOriginalPosition === 'function') {
110
126
  mapOriginalPosition = sourceInfo.mapOriginalPosition;
111
127
  }
112
- // Transform the already-compiled JS (keeps Nest’s decorator metadata intact)
113
- const res = babel.transformSync(code, {
114
- filename,
115
- sourceType: 'unambiguous',
116
- retainLines: true,
117
- sourceMaps: 'inline',
118
- parserOpts: {
128
+ if (shouldHandle(filename, metaFilename)) {
129
+ try { process.stderr.write(`[trace-debug] compile: ${filename}\n`); } catch {}
130
+ // Transform the already-compiled JS (keeps Nest’s decorator metadata intact).
131
+ const res = babel.transformSync(code, {
132
+ filename,
119
133
  sourceType: 'unambiguous',
120
- // We’re parsing JS here; TS was compiled by ts-node/Nest.
121
- plugins: parserPlugins || [
122
- 'jsx', 'classProperties', 'classPrivateProperties',
123
- 'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
124
- 'optionalChaining', 'nullishCoalescingOperator',
134
+ retainLines: true,
135
+ sourceMaps: 'inline',
136
+ parserOpts: {
137
+ sourceType: 'unambiguous',
138
+ // We’re parsing JS here; TS was compiled by ts-node/Nest.
139
+ plugins: parserPlugins || [
140
+ 'jsx', 'classProperties', 'classPrivateProperties',
141
+ 'classPrivateMethods', 'dynamicImport', 'topLevelAwait',
142
+ 'optionalChaining', 'nullishCoalescingOperator',
143
+ ],
144
+ },
145
+ // only the wrap plugin; do NOT run TS transform here
146
+ plugins: [
147
+ [ makeWrap(metaFilename, {
148
+ mode: 'all',
149
+ wrapGettersSetters: false,
150
+ skipAnonymous: false,
151
+ mapOriginalPosition,
152
+ }) ],
125
153
  ],
126
- },
127
- // only the wrap plugin; do NOT run TS transform here
128
- plugins: [
129
- [ makeWrap(metaFilename, {
130
- mode: 'all',
131
- wrapGettersSetters: false,
132
- skipAnonymous: false,
133
- mapOriginalPosition,
134
- }) ],
135
- ],
136
- compact: false,
137
- comments: true,
138
- });
139
- out = res?.code || code;
140
- wasInstrumented = !!res?.code;
154
+ compact: false,
155
+ comments: true,
156
+ });
157
+ out = res?.code || code;
158
+ wasInstrumented = !!res?.code;
159
+ }
141
160
  transformedFiles.add(filename);
142
161
  }
143
162
  } catch (err) {