@reproapp/node-sdk 0.0.6 → 0.0.8

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
@@ -1929,6 +1929,50 @@ function sanitizeTraceValue(value, depth = 0, seen = new WeakMap(), options = {}
1929
1929
  const mongoId = coerceMongoId(value);
1930
1930
  if (mongoId !== null)
1931
1931
  return mongoId;
1932
+ if (isHttpRequestLike(value)) {
1933
+ const projected = {
1934
+ __kind: 'http-request',
1935
+ };
1936
+ if (typeof value.method === 'string') {
1937
+ projected.method = value.method;
1938
+ }
1939
+ const url = typeof value.originalUrl === 'string'
1940
+ ? value.originalUrl
1941
+ : typeof value.url === 'string'
1942
+ ? value.url
1943
+ : undefined;
1944
+ if (url) {
1945
+ projected.url = url;
1946
+ }
1947
+ const headers = sanitizeHeaders(value.headers, true);
1948
+ if (headers !== undefined) {
1949
+ projected.headers = sanitizeTraceValue(headers, depth + 1, seen, options, childCapturePath(valuePath, 'headers'));
1950
+ }
1951
+ if (value.params !== undefined) {
1952
+ projected.params = sanitizeTraceValue(value.params, depth + 1, seen, options, childCapturePath(valuePath, 'params'));
1953
+ }
1954
+ if (value.query !== undefined) {
1955
+ projected.query = sanitizeTraceValue(value.query, depth + 1, seen, options, childCapturePath(valuePath, 'query'));
1956
+ }
1957
+ if (value.body !== undefined) {
1958
+ projected.body = sanitizeTraceValue(value.body, depth + 1, seen, options, childCapturePath(valuePath, 'body'));
1959
+ }
1960
+ return projected;
1961
+ }
1962
+ if (isHttpResponseLike(value)) {
1963
+ const projected = {
1964
+ __kind: 'http-response',
1965
+ statusCode: Number(value.statusCode) || 0,
1966
+ };
1967
+ const rawHeaders = typeof value.getHeaders === 'function'
1968
+ ? value.getHeaders()
1969
+ : value._headers;
1970
+ const headers = sanitizeHeaders(rawHeaders, true);
1971
+ if (headers !== undefined) {
1972
+ projected.headers = sanitizeTraceValue(headers, depth + 1, seen, options, childCapturePath(valuePath, 'headers'));
1973
+ }
1974
+ return projected;
1975
+ }
1932
1976
  if (isMongooseQueryLike(value)) {
1933
1977
  const captured = value.__repro_result;
1934
1978
  if (captured !== undefined) {
@@ -3695,6 +3739,7 @@ function reproMiddleware(cfg) {
3695
3739
  let idleTimer = null;
3696
3740
  let hardStopTimer = null;
3697
3741
  let flushPayload = null;
3742
+ let requestCaptureScheduled = false;
3698
3743
  let sessionDrainWait = null;
3699
3744
  const activeSpans = new Set();
3700
3745
  let anonymousSpanDepth = 0;
@@ -3784,6 +3829,179 @@ function reproMiddleware(cfg) {
3784
3829
  }
3785
3830
  scheduleIdleFlush();
3786
3831
  };
3832
+ const chooseRequestEndpoint = () => {
3833
+ const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
3834
+ const baseEvents = balanceTraceEvents(pendingEvents.slice());
3835
+ const orderedEvents = TRACE_ORDER_MODE === 'tree'
3836
+ ? reorderTraceEvents(baseEvents)
3837
+ : sortTraceEventsChronologically(baseEvents);
3838
+ const summary = summarizeEndpointFromEvents(orderedEvents);
3839
+ return {
3840
+ chosenEndpoint: summary.endpointTrace
3841
+ ?? summary.preferredAppTrace
3842
+ ?? summary.firstAppTrace
3843
+ ?? endpointTrace
3844
+ ?? preferredAppTrace
3845
+ ?? firstAppTrace
3846
+ ?? { fn: null, file: null, line: null, functionType: null },
3847
+ hasTraceEvents: orderedEvents.length > 0,
3848
+ };
3849
+ };
3850
+ const buildRequestCapturePayloadAsync = async (chosenEndpoint, hasTraceEvents) => {
3851
+ const endpointTraceCtx = (() => {
3852
+ if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
3853
+ return null;
3854
+ return {
3855
+ type: 'enter',
3856
+ eventType: 'enter',
3857
+ fn: chosenEndpoint.fn ?? undefined,
3858
+ wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
3859
+ file: chosenEndpoint.file ?? null,
3860
+ line: chosenEndpoint.line ?? null,
3861
+ functionType: chosenEndpoint.functionType ?? null,
3862
+ library: inferLibraryNameFromFile(chosenEndpoint.file),
3863
+ };
3864
+ })();
3865
+ const activePrivacy = resolvePrivacy();
3866
+ const requestBodyRaw = req.body;
3867
+ const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
3868
+ ?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3869
+ const requestBody = requestBodyMaterialization.value;
3870
+ const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3871
+ const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3872
+ const maskedHeaders = await applyPrivacyThenMaskAsync('request.headers', requestHeaders, cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3873
+ const responseBodyMaterialization = capturedBody === undefined
3874
+ ? { value: undefined }
3875
+ : limitRawInlinePrivacyValue('response.body', capturedBody)
3876
+ ?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3877
+ const responseBody = responseBodyMaterialization.value;
3878
+ const requestValueEntries = [];
3879
+ const bodyValueCapture = requestBodyMaterialization.skipped
3880
+ ? undefined
3881
+ : await maybeCaptureRequestValueAsync({
3882
+ target: 'request.body',
3883
+ rawValue: req.body,
3884
+ previewValue: requestBody,
3885
+ capture: {
3886
+ runtimeConfig: cfg,
3887
+ captureHeaders: cfg.captureHeaders,
3888
+ maskReq,
3889
+ trace: endpointTraceCtx,
3890
+ masking,
3891
+ privacy: activePrivacy,
3892
+ },
3893
+ }, requestValueEntries);
3894
+ const paramsValueCapture = await maybeCaptureRequestValueAsync({
3895
+ target: 'request.params',
3896
+ rawValue: req.params,
3897
+ previewValue: requestParams,
3898
+ capture: {
3899
+ runtimeConfig: cfg,
3900
+ captureHeaders: cfg.captureHeaders,
3901
+ maskReq,
3902
+ trace: endpointTraceCtx,
3903
+ masking,
3904
+ privacy: activePrivacy,
3905
+ },
3906
+ }, requestValueEntries);
3907
+ const queryValueCapture = await maybeCaptureRequestValueAsync({
3908
+ target: 'request.query',
3909
+ rawValue: req.query,
3910
+ previewValue: requestQuery,
3911
+ capture: {
3912
+ runtimeConfig: cfg,
3913
+ captureHeaders: cfg.captureHeaders,
3914
+ maskReq,
3915
+ trace: endpointTraceCtx,
3916
+ masking,
3917
+ privacy: activePrivacy,
3918
+ },
3919
+ }, requestValueEntries);
3920
+ const headersValueCapture = await maybeCaptureRequestValueAsync({
3921
+ target: 'request.headers',
3922
+ rawValue: req.headers,
3923
+ previewValue: maskedHeaders,
3924
+ capture: {
3925
+ runtimeConfig: cfg,
3926
+ captureHeaders: cfg.captureHeaders,
3927
+ maskReq,
3928
+ trace: endpointTraceCtx,
3929
+ masking,
3930
+ privacy: activePrivacy,
3931
+ },
3932
+ }, requestValueEntries);
3933
+ const respBodyValueCapture = responseBodyMaterialization.skipped
3934
+ ? undefined
3935
+ : await maybeCaptureRequestValueAsync({
3936
+ target: 'response.body',
3937
+ rawValue: capturedBody,
3938
+ previewValue: responseBody,
3939
+ capture: {
3940
+ runtimeConfig: cfg,
3941
+ captureHeaders: cfg.captureHeaders,
3942
+ maskReq,
3943
+ trace: endpointTraceCtx,
3944
+ masking,
3945
+ privacy: activePrivacy,
3946
+ },
3947
+ }, requestValueEntries);
3948
+ const requestPayload = {
3949
+ rid,
3950
+ method: req.method,
3951
+ url,
3952
+ path,
3953
+ status: res.statusCode,
3954
+ durMs: Date.now() - t0,
3955
+ headers: maskedHeaders,
3956
+ key,
3957
+ respBody: responseBody,
3958
+ trace: hasTraceEvents ? undefined : [],
3959
+ };
3960
+ if (requestBody !== undefined)
3961
+ requestPayload.body = requestBody;
3962
+ if (bodyValueCapture)
3963
+ requestPayload.bodyValueCapture = bodyValueCapture;
3964
+ if (requestParams !== undefined)
3965
+ requestPayload.params = requestParams;
3966
+ if (paramsValueCapture)
3967
+ requestPayload.paramsValueCapture = paramsValueCapture;
3968
+ if (requestQuery !== undefined)
3969
+ requestPayload.query = requestQuery;
3970
+ if (queryValueCapture)
3971
+ requestPayload.queryValueCapture = queryValueCapture;
3972
+ if (headersValueCapture)
3973
+ requestPayload.headersValueCapture = headersValueCapture;
3974
+ if (respBodyValueCapture)
3975
+ requestPayload.respBodyValueCapture = respBodyValueCapture;
3976
+ if (requestBodyMaterialization.skipped) {
3977
+ requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
3978
+ }
3979
+ if (responseBodyMaterialization.skipped) {
3980
+ requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
3981
+ }
3982
+ requestPayload.entryPoint = chosenEndpoint;
3983
+ return { requestPayload, requestValueEntries };
3984
+ };
3985
+ const emitRequestCaptureAsync = async () => {
3986
+ if (requestCaptureScheduled)
3987
+ return;
3988
+ requestCaptureScheduled = true;
3989
+ try {
3990
+ const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
3991
+ const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
3992
+ post(cfg, sid, {
3993
+ entries: [{
3994
+ actionId: aid,
3995
+ request: requestPayload,
3996
+ requestValues: requestValueEntries.length ? requestValueEntries : undefined,
3997
+ t: requestEpochMs,
3998
+ }]
3999
+ });
4000
+ }
4001
+ catch {
4002
+ // never break user code
4003
+ }
4004
+ };
3787
4005
  try {
3788
4006
  if (__TRACER__?.tracer?.on) {
3789
4007
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -3876,6 +4094,7 @@ function reproMiddleware(cfg) {
3876
4094
  : Buffer.from(chunks.map(String).join(''));
3877
4095
  capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
3878
4096
  }
4097
+ void emitRequestCaptureAsync();
3879
4098
  if (!flushPayload) {
3880
4099
  flushPayload = async () => {
3881
4100
  try {
@@ -3890,155 +4109,7 @@ function reproMiddleware(cfg) {
3890
4109
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
3891
4110
  ? reorderTraceEvents(baseEvents)
3892
4111
  : sortTraceEventsChronologically(baseEvents);
3893
- const summary = summarizeEndpointFromEvents(orderedEvents);
3894
- const chosenEndpoint = summary.endpointTrace
3895
- ?? summary.preferredAppTrace
3896
- ?? summary.firstAppTrace
3897
- ?? endpointTrace
3898
- ?? preferredAppTrace
3899
- ?? firstAppTrace
3900
- ?? { fn: null, file: null, line: null, functionType: null };
3901
4112
  const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
3902
- const endpointTraceCtx = (() => {
3903
- if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
3904
- return null;
3905
- return {
3906
- type: 'enter',
3907
- eventType: 'enter',
3908
- fn: chosenEndpoint.fn ?? undefined,
3909
- wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
3910
- file: chosenEndpoint.file ?? null,
3911
- line: chosenEndpoint.line ?? null,
3912
- functionType: chosenEndpoint.functionType ?? null,
3913
- library: inferLibraryNameFromFile(chosenEndpoint.file),
3914
- };
3915
- })();
3916
- const activePrivacy = resolvePrivacy();
3917
- const requestBodyRaw = req.body;
3918
- const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
3919
- ?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3920
- const requestBody = requestBodyMaterialization.value;
3921
- const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3922
- const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3923
- const maskedHeaders = await applyPrivacyThenMaskAsync('request.headers', requestHeaders, cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3924
- const responseBodyMaterialization = capturedBody === undefined
3925
- ? { value: undefined }
3926
- : limitRawInlinePrivacyValue('response.body', capturedBody)
3927
- ?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3928
- const responseBody = responseBodyMaterialization.value;
3929
- const requestValueEntries = [];
3930
- const bodyValueCapture = requestBodyMaterialization.skipped
3931
- ? undefined
3932
- : await maybeCaptureRequestValueAsync({
3933
- target: 'request.body',
3934
- rawValue: req.body,
3935
- previewValue: requestBody,
3936
- capture: {
3937
- runtimeConfig: cfg,
3938
- captureHeaders: cfg.captureHeaders,
3939
- maskReq,
3940
- trace: endpointTraceCtx,
3941
- masking,
3942
- privacy: activePrivacy,
3943
- },
3944
- }, requestValueEntries);
3945
- const paramsValueCapture = await maybeCaptureRequestValueAsync({
3946
- target: 'request.params',
3947
- rawValue: req.params,
3948
- previewValue: requestParams,
3949
- capture: {
3950
- runtimeConfig: cfg,
3951
- captureHeaders: cfg.captureHeaders,
3952
- maskReq,
3953
- trace: endpointTraceCtx,
3954
- masking,
3955
- privacy: activePrivacy,
3956
- },
3957
- }, requestValueEntries);
3958
- const queryValueCapture = await maybeCaptureRequestValueAsync({
3959
- target: 'request.query',
3960
- rawValue: req.query,
3961
- previewValue: requestQuery,
3962
- capture: {
3963
- runtimeConfig: cfg,
3964
- captureHeaders: cfg.captureHeaders,
3965
- maskReq,
3966
- trace: endpointTraceCtx,
3967
- masking,
3968
- privacy: activePrivacy,
3969
- },
3970
- }, requestValueEntries);
3971
- const headersValueCapture = await maybeCaptureRequestValueAsync({
3972
- target: 'request.headers',
3973
- rawValue: req.headers,
3974
- previewValue: maskedHeaders,
3975
- capture: {
3976
- runtimeConfig: cfg,
3977
- captureHeaders: cfg.captureHeaders,
3978
- maskReq,
3979
- trace: endpointTraceCtx,
3980
- masking,
3981
- privacy: activePrivacy,
3982
- },
3983
- }, requestValueEntries);
3984
- const respBodyValueCapture = responseBodyMaterialization.skipped
3985
- ? undefined
3986
- : await maybeCaptureRequestValueAsync({
3987
- target: 'response.body',
3988
- rawValue: capturedBody,
3989
- previewValue: responseBody,
3990
- capture: {
3991
- runtimeConfig: cfg,
3992
- captureHeaders: cfg.captureHeaders,
3993
- maskReq,
3994
- trace: endpointTraceCtx,
3995
- masking,
3996
- privacy: activePrivacy,
3997
- },
3998
- }, requestValueEntries);
3999
- const requestPayload = {
4000
- rid,
4001
- method: req.method,
4002
- url,
4003
- path,
4004
- status: res.statusCode,
4005
- durMs: Date.now() - t0,
4006
- headers: maskedHeaders,
4007
- key,
4008
- respBody: responseBody,
4009
- trace: traceBatches.length ? undefined : [],
4010
- };
4011
- if (requestBody !== undefined)
4012
- requestPayload.body = requestBody;
4013
- if (bodyValueCapture)
4014
- requestPayload.bodyValueCapture = bodyValueCapture;
4015
- if (requestParams !== undefined)
4016
- requestPayload.params = requestParams;
4017
- if (paramsValueCapture)
4018
- requestPayload.paramsValueCapture = paramsValueCapture;
4019
- if (requestQuery !== undefined)
4020
- requestPayload.query = requestQuery;
4021
- if (queryValueCapture)
4022
- requestPayload.queryValueCapture = queryValueCapture;
4023
- if (headersValueCapture)
4024
- requestPayload.headersValueCapture = headersValueCapture;
4025
- if (respBodyValueCapture)
4026
- requestPayload.respBodyValueCapture = respBodyValueCapture;
4027
- if (requestBodyMaterialization.skipped) {
4028
- requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4029
- }
4030
- if (responseBodyMaterialization.skipped) {
4031
- requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4032
- }
4033
- requestPayload.entryPoint = chosenEndpoint;
4034
- post(cfg, sid, {
4035
- entries: [{
4036
- actionId: aid,
4037
- request: requestPayload,
4038
- requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4039
- t: requestEpochMs,
4040
- }]
4041
- });
4042
4113
  if (traceBatches.length) {
4043
4114
  for (let i = 0; i < traceBatches.length; i++) {
4044
4115
  const batch = traceBatches[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reproapp/node-sdk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
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 -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/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.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/express-trace-http-args.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/privacy-runtime-policy.test.js && node test/runtime-privacy-materialization.test.js && node test/kafka-runtime-privacy-policy.test.js"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "express": "^5.1.0",
package/src/index.ts CHANGED
@@ -2318,6 +2318,55 @@ function sanitizeTraceValue(
2318
2318
  const mongoId = coerceMongoId(value);
2319
2319
  if (mongoId !== null) return mongoId;
2320
2320
 
2321
+ if (isHttpRequestLike(value)) {
2322
+ const projected: Record<string, any> = {
2323
+ __kind: 'http-request',
2324
+ };
2325
+ if (typeof (value as any).method === 'string') {
2326
+ projected.method = (value as any).method;
2327
+ }
2328
+ const url =
2329
+ typeof (value as any).originalUrl === 'string'
2330
+ ? (value as any).originalUrl
2331
+ : typeof (value as any).url === 'string'
2332
+ ? (value as any).url
2333
+ : undefined;
2334
+ if (url) {
2335
+ projected.url = url;
2336
+ }
2337
+
2338
+ const headers = sanitizeHeaders((value as any).headers, true);
2339
+ if (headers !== undefined) {
2340
+ projected.headers = sanitizeTraceValue(headers, depth + 1, seen, options, childCapturePath(valuePath, 'headers'));
2341
+ }
2342
+ if ((value as any).params !== undefined) {
2343
+ projected.params = sanitizeTraceValue((value as any).params, depth + 1, seen, options, childCapturePath(valuePath, 'params'));
2344
+ }
2345
+ if ((value as any).query !== undefined) {
2346
+ projected.query = sanitizeTraceValue((value as any).query, depth + 1, seen, options, childCapturePath(valuePath, 'query'));
2347
+ }
2348
+ if ((value as any).body !== undefined) {
2349
+ projected.body = sanitizeTraceValue((value as any).body, depth + 1, seen, options, childCapturePath(valuePath, 'body'));
2350
+ }
2351
+ return projected;
2352
+ }
2353
+
2354
+ if (isHttpResponseLike(value)) {
2355
+ const projected: Record<string, any> = {
2356
+ __kind: 'http-response',
2357
+ statusCode: Number((value as any).statusCode) || 0,
2358
+ };
2359
+ const rawHeaders =
2360
+ typeof (value as any).getHeaders === 'function'
2361
+ ? (value as any).getHeaders()
2362
+ : (value as any)._headers;
2363
+ const headers = sanitizeHeaders(rawHeaders, true);
2364
+ if (headers !== undefined) {
2365
+ projected.headers = sanitizeTraceValue(headers, depth + 1, seen, options, childCapturePath(valuePath, 'headers'));
2366
+ }
2367
+ return projected;
2368
+ }
2369
+
2321
2370
  if (isMongooseQueryLike(value)) {
2322
2371
  const captured = (value as any).__repro_result;
2323
2372
  if (captured !== undefined) {
@@ -4499,6 +4548,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4499
4548
  let idleTimer: NodeJS.Timeout | null = null;
4500
4549
  let hardStopTimer: NodeJS.Timeout | null = null;
4501
4550
  let flushPayload: null | (() => Promise<void>) = null;
4551
+ let requestCaptureScheduled = false;
4502
4552
  let sessionDrainWait: Promise<void> | null = null;
4503
4553
  const activeSpans = new Set<string>();
4504
4554
  let anonymousSpanDepth = 0;
@@ -4572,6 +4622,226 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4572
4622
  scheduleIdleFlush();
4573
4623
  };
4574
4624
 
4625
+ const chooseRequestEndpoint = (): {
4626
+ chosenEndpoint: EndpointTraceInfo;
4627
+ hasTraceEvents: boolean;
4628
+ } => {
4629
+ const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
4630
+ const baseEvents = balanceTraceEvents(pendingEvents.slice() as TraceEventRecord[]);
4631
+ const orderedEvents = TRACE_ORDER_MODE === 'tree'
4632
+ ? reorderTraceEvents(baseEvents)
4633
+ : sortTraceEventsChronologically(baseEvents);
4634
+ const summary = summarizeEndpointFromEvents(orderedEvents);
4635
+ return {
4636
+ chosenEndpoint: summary.endpointTrace
4637
+ ?? summary.preferredAppTrace
4638
+ ?? summary.firstAppTrace
4639
+ ?? endpointTrace
4640
+ ?? preferredAppTrace
4641
+ ?? firstAppTrace
4642
+ ?? { fn: null, file: null, line: null, functionType: null },
4643
+ hasTraceEvents: orderedEvents.length > 0,
4644
+ };
4645
+ };
4646
+
4647
+ const buildRequestCapturePayloadAsync = async (
4648
+ chosenEndpoint: EndpointTraceInfo,
4649
+ hasTraceEvents: boolean,
4650
+ ): Promise<{
4651
+ requestPayload: Record<string, any>;
4652
+ requestValueEntries: TraceValueBatchEntry[];
4653
+ }> => {
4654
+ const endpointTraceCtx: TraceEventForFilter | null = (() => {
4655
+ if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
4656
+ return {
4657
+ type: 'enter',
4658
+ eventType: 'enter',
4659
+ fn: chosenEndpoint.fn ?? undefined,
4660
+ wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
4661
+ file: chosenEndpoint.file ?? null,
4662
+ line: chosenEndpoint.line ?? null,
4663
+ functionType: chosenEndpoint.functionType ?? null,
4664
+ library: inferLibraryNameFromFile(chosenEndpoint.file),
4665
+ };
4666
+ })();
4667
+ const activePrivacy = resolvePrivacy();
4668
+
4669
+ const requestBodyRaw = (req as any).body;
4670
+ const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
4671
+ ?? await materializeInlinePrivacyValueAsync(
4672
+ 'request.body',
4673
+ sanitizeRequestSnapshot(requestBodyRaw),
4674
+ cfg,
4675
+ maskReq,
4676
+ endpointTraceCtx,
4677
+ masking,
4678
+ activePrivacy,
4679
+ );
4680
+ const requestBody = requestBodyMaterialization.value;
4681
+ const requestParams = await applyPrivacyThenMaskAsync(
4682
+ 'request.params',
4683
+ sanitizeRequestSnapshot((req as any).params),
4684
+ cfg,
4685
+ maskReq,
4686
+ endpointTraceCtx,
4687
+ masking,
4688
+ activePrivacy,
4689
+ );
4690
+ const requestQuery = await applyPrivacyThenMaskAsync(
4691
+ 'request.query',
4692
+ sanitizeRequestSnapshot((req as any).query),
4693
+ cfg,
4694
+ maskReq,
4695
+ endpointTraceCtx,
4696
+ masking,
4697
+ activePrivacy,
4698
+ );
4699
+ const maskedHeaders = await applyPrivacyThenMaskAsync(
4700
+ 'request.headers',
4701
+ requestHeaders,
4702
+ cfg,
4703
+ maskReq,
4704
+ endpointTraceCtx,
4705
+ masking,
4706
+ activePrivacy,
4707
+ );
4708
+ const responseBodyMaterialization = capturedBody === undefined
4709
+ ? { value: undefined }
4710
+ : limitRawInlinePrivacyValue('response.body', capturedBody)
4711
+ ?? await materializeInlinePrivacyValueAsync(
4712
+ 'response.body',
4713
+ sanitizeRequestSnapshot(capturedBody),
4714
+ cfg,
4715
+ maskReq,
4716
+ endpointTraceCtx,
4717
+ masking,
4718
+ activePrivacy,
4719
+ );
4720
+ const responseBody = responseBodyMaterialization.value;
4721
+ const requestValueEntries: TraceValueBatchEntry[] = [];
4722
+ const bodyValueCapture = requestBodyMaterialization.skipped
4723
+ ? undefined
4724
+ : await maybeCaptureRequestValueAsync({
4725
+ target: 'request.body',
4726
+ rawValue: (req as any).body,
4727
+ previewValue: requestBody,
4728
+ capture: {
4729
+ runtimeConfig: cfg,
4730
+ captureHeaders: cfg.captureHeaders,
4731
+ maskReq,
4732
+ trace: endpointTraceCtx,
4733
+ masking,
4734
+ privacy: activePrivacy,
4735
+ },
4736
+ }, requestValueEntries);
4737
+ const paramsValueCapture = await maybeCaptureRequestValueAsync({
4738
+ target: 'request.params',
4739
+ rawValue: (req as any).params,
4740
+ previewValue: requestParams,
4741
+ capture: {
4742
+ runtimeConfig: cfg,
4743
+ captureHeaders: cfg.captureHeaders,
4744
+ maskReq,
4745
+ trace: endpointTraceCtx,
4746
+ masking,
4747
+ privacy: activePrivacy,
4748
+ },
4749
+ }, requestValueEntries);
4750
+ const queryValueCapture = await maybeCaptureRequestValueAsync({
4751
+ target: 'request.query',
4752
+ rawValue: (req as any).query,
4753
+ previewValue: requestQuery,
4754
+ capture: {
4755
+ runtimeConfig: cfg,
4756
+ captureHeaders: cfg.captureHeaders,
4757
+ maskReq,
4758
+ trace: endpointTraceCtx,
4759
+ masking,
4760
+ privacy: activePrivacy,
4761
+ },
4762
+ }, requestValueEntries);
4763
+ const headersValueCapture = await maybeCaptureRequestValueAsync({
4764
+ target: 'request.headers',
4765
+ rawValue: req.headers,
4766
+ previewValue: maskedHeaders,
4767
+ capture: {
4768
+ runtimeConfig: cfg,
4769
+ captureHeaders: cfg.captureHeaders,
4770
+ maskReq,
4771
+ trace: endpointTraceCtx,
4772
+ masking,
4773
+ privacy: activePrivacy,
4774
+ },
4775
+ }, requestValueEntries);
4776
+ const respBodyValueCapture = responseBodyMaterialization.skipped
4777
+ ? undefined
4778
+ : await maybeCaptureRequestValueAsync({
4779
+ target: 'response.body',
4780
+ rawValue: capturedBody,
4781
+ previewValue: responseBody,
4782
+ capture: {
4783
+ runtimeConfig: cfg,
4784
+ captureHeaders: cfg.captureHeaders,
4785
+ maskReq,
4786
+ trace: endpointTraceCtx,
4787
+ masking,
4788
+ privacy: activePrivacy,
4789
+ },
4790
+ }, requestValueEntries);
4791
+
4792
+ const requestPayload: Record<string, any> = {
4793
+ rid,
4794
+ method: req.method,
4795
+ url,
4796
+ path,
4797
+ status: res.statusCode,
4798
+ durMs: Date.now() - t0,
4799
+ headers: maskedHeaders,
4800
+ key,
4801
+ respBody: responseBody,
4802
+ trace: hasTraceEvents ? undefined : [],
4803
+ };
4804
+ if (requestBody !== undefined) requestPayload.body = requestBody;
4805
+ if (bodyValueCapture) requestPayload.bodyValueCapture = bodyValueCapture;
4806
+ if (requestParams !== undefined) requestPayload.params = requestParams;
4807
+ if (paramsValueCapture) requestPayload.paramsValueCapture = paramsValueCapture;
4808
+ if (requestQuery !== undefined) requestPayload.query = requestQuery;
4809
+ if (queryValueCapture) requestPayload.queryValueCapture = queryValueCapture;
4810
+ if (headersValueCapture) requestPayload.headersValueCapture = headersValueCapture;
4811
+ if (respBodyValueCapture) requestPayload.respBodyValueCapture = respBodyValueCapture;
4812
+ if (requestBodyMaterialization.skipped) {
4813
+ requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4814
+ }
4815
+ if (responseBodyMaterialization.skipped) {
4816
+ requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4817
+ }
4818
+ requestPayload.entryPoint = chosenEndpoint;
4819
+
4820
+ return { requestPayload, requestValueEntries };
4821
+ };
4822
+
4823
+ const emitRequestCaptureAsync = async (): Promise<void> => {
4824
+ if (requestCaptureScheduled) return;
4825
+ requestCaptureScheduled = true;
4826
+ try {
4827
+ const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
4828
+ const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
4829
+ chosenEndpoint,
4830
+ hasTraceEvents,
4831
+ );
4832
+ post(cfg, sid, {
4833
+ entries: [{
4834
+ actionId: aid,
4835
+ request: requestPayload,
4836
+ requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4837
+ t: requestEpochMs,
4838
+ }]
4839
+ });
4840
+ } catch {
4841
+ // never break user code
4842
+ }
4843
+ };
4844
+
4575
4845
  try {
4576
4846
  if (__TRACER__?.tracer?.on) {
4577
4847
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -4667,6 +4937,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4667
4937
  capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
4668
4938
  }
4669
4939
 
4940
+ void emitRequestCaptureAsync();
4941
+
4670
4942
  if (!flushPayload) {
4671
4943
  flushPayload = async () => {
4672
4944
  try {
@@ -4681,189 +4953,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4681
4953
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4682
4954
  ? reorderTraceEvents(baseEvents)
4683
4955
  : sortTraceEventsChronologically(baseEvents);
4684
- const summary = summarizeEndpointFromEvents(orderedEvents);
4685
- const chosenEndpoint = summary.endpointTrace
4686
- ?? summary.preferredAppTrace
4687
- ?? summary.firstAppTrace
4688
- ?? endpointTrace
4689
- ?? preferredAppTrace
4690
- ?? firstAppTrace
4691
- ?? { fn: null, file: null, line: null, functionType: null };
4692
4956
  const traceBatches = chunkArray(orderedEvents, TRACE_BATCH_SIZE);
4693
- const endpointTraceCtx: TraceEventForFilter | null = (() => {
4694
- if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
4695
- return {
4696
- type: 'enter',
4697
- eventType: 'enter',
4698
- fn: chosenEndpoint.fn ?? undefined,
4699
- wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
4700
- file: chosenEndpoint.file ?? null,
4701
- line: chosenEndpoint.line ?? null,
4702
- functionType: chosenEndpoint.functionType ?? null,
4703
- library: inferLibraryNameFromFile(chosenEndpoint.file),
4704
- };
4705
- })();
4706
- const activePrivacy = resolvePrivacy();
4707
-
4708
- const requestBodyRaw = (req as any).body;
4709
- const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
4710
- ?? await materializeInlinePrivacyValueAsync(
4711
- 'request.body',
4712
- sanitizeRequestSnapshot(requestBodyRaw),
4713
- cfg,
4714
- maskReq,
4715
- endpointTraceCtx,
4716
- masking,
4717
- activePrivacy,
4718
- );
4719
- const requestBody = requestBodyMaterialization.value;
4720
- const requestParams = await applyPrivacyThenMaskAsync(
4721
- 'request.params',
4722
- sanitizeRequestSnapshot((req as any).params),
4723
- cfg,
4724
- maskReq,
4725
- endpointTraceCtx,
4726
- masking,
4727
- activePrivacy,
4728
- );
4729
- const requestQuery = await applyPrivacyThenMaskAsync(
4730
- 'request.query',
4731
- sanitizeRequestSnapshot((req as any).query),
4732
- cfg,
4733
- maskReq,
4734
- endpointTraceCtx,
4735
- masking,
4736
- activePrivacy,
4737
- );
4738
- const maskedHeaders = await applyPrivacyThenMaskAsync(
4739
- 'request.headers',
4740
- requestHeaders,
4741
- cfg,
4742
- maskReq,
4743
- endpointTraceCtx,
4744
- masking,
4745
- activePrivacy,
4746
- );
4747
- const responseBodyMaterialization = capturedBody === undefined
4748
- ? { value: undefined }
4749
- : limitRawInlinePrivacyValue('response.body', capturedBody)
4750
- ?? await materializeInlinePrivacyValueAsync(
4751
- 'response.body',
4752
- sanitizeRequestSnapshot(capturedBody),
4753
- cfg,
4754
- maskReq,
4755
- endpointTraceCtx,
4756
- masking,
4757
- activePrivacy,
4758
- );
4759
- const responseBody = responseBodyMaterialization.value;
4760
- const requestValueEntries: TraceValueBatchEntry[] = [];
4761
- const bodyValueCapture = requestBodyMaterialization.skipped
4762
- ? undefined
4763
- : await maybeCaptureRequestValueAsync({
4764
- target: 'request.body',
4765
- rawValue: (req as any).body,
4766
- previewValue: requestBody,
4767
- capture: {
4768
- runtimeConfig: cfg,
4769
- captureHeaders: cfg.captureHeaders,
4770
- maskReq,
4771
- trace: endpointTraceCtx,
4772
- masking,
4773
- privacy: activePrivacy,
4774
- },
4775
- }, requestValueEntries);
4776
- const paramsValueCapture = await maybeCaptureRequestValueAsync({
4777
- target: 'request.params',
4778
- rawValue: (req as any).params,
4779
- previewValue: requestParams,
4780
- capture: {
4781
- runtimeConfig: cfg,
4782
- captureHeaders: cfg.captureHeaders,
4783
- maskReq,
4784
- trace: endpointTraceCtx,
4785
- masking,
4786
- privacy: activePrivacy,
4787
- },
4788
- }, requestValueEntries);
4789
- const queryValueCapture = await maybeCaptureRequestValueAsync({
4790
- target: 'request.query',
4791
- rawValue: (req as any).query,
4792
- previewValue: requestQuery,
4793
- capture: {
4794
- runtimeConfig: cfg,
4795
- captureHeaders: cfg.captureHeaders,
4796
- maskReq,
4797
- trace: endpointTraceCtx,
4798
- masking,
4799
- privacy: activePrivacy,
4800
- },
4801
- }, requestValueEntries);
4802
- const headersValueCapture = await maybeCaptureRequestValueAsync({
4803
- target: 'request.headers',
4804
- rawValue: req.headers,
4805
- previewValue: maskedHeaders,
4806
- capture: {
4807
- runtimeConfig: cfg,
4808
- captureHeaders: cfg.captureHeaders,
4809
- maskReq,
4810
- trace: endpointTraceCtx,
4811
- masking,
4812
- privacy: activePrivacy,
4813
- },
4814
- }, requestValueEntries);
4815
- const respBodyValueCapture = responseBodyMaterialization.skipped
4816
- ? undefined
4817
- : await maybeCaptureRequestValueAsync({
4818
- target: 'response.body',
4819
- rawValue: capturedBody,
4820
- previewValue: responseBody,
4821
- capture: {
4822
- runtimeConfig: cfg,
4823
- captureHeaders: cfg.captureHeaders,
4824
- maskReq,
4825
- trace: endpointTraceCtx,
4826
- masking,
4827
- privacy: activePrivacy,
4828
- },
4829
- }, requestValueEntries);
4830
-
4831
- const requestPayload: Record<string, any> = {
4832
- rid,
4833
- method: req.method,
4834
- url,
4835
- path,
4836
- status: res.statusCode,
4837
- durMs: Date.now() - t0,
4838
- headers: maskedHeaders,
4839
- key,
4840
- respBody: responseBody,
4841
- trace: traceBatches.length ? undefined : [],
4842
- };
4843
- if (requestBody !== undefined) requestPayload.body = requestBody;
4844
- if (bodyValueCapture) requestPayload.bodyValueCapture = bodyValueCapture;
4845
- if (requestParams !== undefined) requestPayload.params = requestParams;
4846
- if (paramsValueCapture) requestPayload.paramsValueCapture = paramsValueCapture;
4847
- if (requestQuery !== undefined) requestPayload.query = requestQuery;
4848
- if (queryValueCapture) requestPayload.queryValueCapture = queryValueCapture;
4849
- if (headersValueCapture) requestPayload.headersValueCapture = headersValueCapture;
4850
- if (respBodyValueCapture) requestPayload.respBodyValueCapture = respBodyValueCapture;
4851
- if (requestBodyMaterialization.skipped) {
4852
- requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4853
- }
4854
- if (responseBodyMaterialization.skipped) {
4855
- requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4856
- }
4857
- requestPayload.entryPoint = chosenEndpoint;
4858
-
4859
- post(cfg, sid, {
4860
- entries: [{
4861
- actionId: aid,
4862
- request: requestPayload,
4863
- requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4864
- t: requestEpochMs,
4865
- }]
4866
- });
4867
4957
 
4868
4958
  if (traceBatches.length) {
4869
4959
  for (let i = 0; i < traceBatches.length; i++) {
@@ -0,0 +1,126 @@
1
+ const assert = require('assert');
2
+ const http = require('http');
3
+
4
+ const originalFetch = global.fetch;
5
+ const capturedBodies = [];
6
+
7
+ global.fetch = async (url, init = {}) => {
8
+ const target = String(url || '');
9
+ if (!target.includes('/v1/ingest/events')) {
10
+ throw new Error(`unexpected fetch target: ${target}`);
11
+ }
12
+ capturedBodies.push({
13
+ at: Date.now(),
14
+ url: target,
15
+ body: JSON.parse(String(init.body || '{}')),
16
+ });
17
+ return {
18
+ ok: true,
19
+ status: 200,
20
+ json: async () => ({ ok: true }),
21
+ text: async () => '{"ok":true}',
22
+ };
23
+ };
24
+
25
+ const { initRepro } = require('../dist');
26
+ const { flushIngestQueue } = require('../dist/ingest/client');
27
+
28
+ function findEvents(eventType) {
29
+ return capturedBodies.flatMap((entry) => Array.isArray(entry.body?.events) ? entry.body.events : [])
30
+ .filter((event) => event?.event_type === eventType);
31
+ }
32
+
33
+ function sendGet(url) {
34
+ return new Promise((resolve, reject) => {
35
+ const request = http.get(url, (response) => {
36
+ let text = '';
37
+ response.setEncoding('utf8');
38
+ response.on('data', (chunk) => {
39
+ text += chunk;
40
+ });
41
+ response.on('end', () => resolve({
42
+ statusCode: response.statusCode,
43
+ body: text,
44
+ }));
45
+ });
46
+ request.on('error', reject);
47
+ });
48
+ }
49
+
50
+ async function main() {
51
+ await initRepro({
52
+ tenantId: 'TENANT_express_trace_http_args',
53
+ appId: 'APP_express_trace_http_args',
54
+ appSecret: 'secret',
55
+ appName: 'express-trace-http-args',
56
+ serviceName: 'express-trace-http-args',
57
+ ingestBase: 'http://127.0.0.1:65535',
58
+ tracing: {
59
+ disableFunctionTypes: ['constructor'],
60
+ logFunctionCalls: false,
61
+ },
62
+ });
63
+
64
+ const { startServer } = require('./fixtures/express-trace-http-args-server');
65
+ const server = await startServer({
66
+ tenantId: 'TENANT_express_trace_http_args',
67
+ appId: 'APP_express_trace_http_args',
68
+ appSecret: 'secret',
69
+ appName: 'express-trace-http-args',
70
+ serviceName: 'express-trace-http-args',
71
+ ingestBase: 'http://127.0.0.1:65535',
72
+ });
73
+
74
+ try {
75
+ const port = server.address().port;
76
+ const requestUrl = `http://127.0.0.1:${port}/ping?name=Avery&__repro_sid=S_express_trace_http_args&__repro_aid=A_express_trace_http_args&__repro_start=${Date.now()}`;
77
+ const response = await sendGet(requestUrl);
78
+ assert.equal(response.statusCode, 200);
79
+
80
+ await new Promise((resolve) => setTimeout(resolve, 2500));
81
+ await flushIngestQueue();
82
+
83
+ const requestEvents = findEvents('backend_request');
84
+ const traceEvents = findEvents('trace_batch');
85
+
86
+ assert.equal(requestEvents.length, 1, JSON.stringify(capturedBodies));
87
+ assert.equal(traceEvents.length, 1, JSON.stringify(capturedBodies));
88
+
89
+ const batch = traceEvents[0]?.payload?.trace;
90
+ assert(Array.isArray(batch) && batch.length > 0, JSON.stringify(traceEvents[0]));
91
+
92
+ const handlePingEnter = batch.find((event) => event?.type === 'enter' && event?.fn === 'handlePing');
93
+ assert(handlePingEnter, JSON.stringify(batch));
94
+ assert(Array.isArray(handlePingEnter.args), JSON.stringify(handlePingEnter));
95
+ assert.equal(handlePingEnter.args[0]?.__kind, 'http-request', JSON.stringify(handlePingEnter.args[0]));
96
+ assert.equal(handlePingEnter.args[1]?.__kind, 'http-response', JSON.stringify(handlePingEnter.args[1]));
97
+ assert.equal(handlePingEnter.args[0]?.url, '/ping?name=Avery');
98
+ assert.equal(handlePingEnter.args[0]?.query?.name, 'Avery');
99
+
100
+ const serializedBatch = JSON.stringify(traceEvents[0]);
101
+ assert(serializedBatch.length < 250000, `trace batch still too large: ${serializedBatch.length}`);
102
+
103
+ // eslint-disable-next-line no-console
104
+ console.log('express trace http arg projection OK');
105
+ } finally {
106
+ await new Promise((resolve, reject) => {
107
+ server.close((error) => {
108
+ if (!error || error.code === 'ERR_SERVER_NOT_RUNNING') {
109
+ resolve();
110
+ return;
111
+ }
112
+ reject(error);
113
+ });
114
+ });
115
+ }
116
+ }
117
+
118
+ main()
119
+ .catch((error) => {
120
+ // eslint-disable-next-line no-console
121
+ console.error(error);
122
+ process.exitCode = 1;
123
+ })
124
+ .finally(() => {
125
+ global.fetch = originalFetch;
126
+ });
@@ -0,0 +1,29 @@
1
+ async function buildPayload(name) {
2
+ const normalized = normalizeName(name);
3
+ const emphasized = emphasize(normalized);
4
+ return {
5
+ original: name,
6
+ normalized,
7
+ emphasized,
8
+ };
9
+ }
10
+
11
+ function normalizeName(name) {
12
+ return String(name || 'anonymous').trim().toLowerCase();
13
+ }
14
+
15
+ function emphasize(name) {
16
+ return `${name.toUpperCase()}!`;
17
+ }
18
+
19
+ async function handlePing(req, res) {
20
+ const payload = await buildPayload(req.query?.name || 'Avery Debugson');
21
+ res.json({
22
+ ok: true,
23
+ payload,
24
+ });
25
+ }
26
+
27
+ module.exports = {
28
+ handlePing,
29
+ };
@@ -0,0 +1,21 @@
1
+ const express = require('express');
2
+ const { reproMiddleware } = require('../../dist');
3
+ const { handlePing } = require('./express-trace-http-args-controller');
4
+
5
+ async function startServer(cfg) {
6
+ const app = express();
7
+ app.use(reproMiddleware(cfg));
8
+ app.get('/ping', handlePing);
9
+
10
+ const server = await new Promise((resolve, reject) => {
11
+ const instance = app.listen(0);
12
+ instance.once('listening', () => resolve(instance));
13
+ instance.once('error', reject);
14
+ });
15
+
16
+ return server;
17
+ }
18
+
19
+ module.exports = {
20
+ startServer,
21
+ };
@@ -0,0 +1,123 @@
1
+ process.env.REPRO_SDK_BACKGROUND_MAX_DEFER_MS = '50';
2
+
3
+ const assert = require('assert');
4
+ const { EventEmitter } = require('events');
5
+ const { initReproTracing, reproMiddleware } = require('../dist');
6
+ const { flushIngestQueue } = require('../dist/ingest/client');
7
+
8
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
+
10
+ async function waitFor(predicate, timeoutMs = 3000) {
11
+ const deadline = Date.now() + timeoutMs;
12
+ while (Date.now() < deadline) {
13
+ if (predicate()) return;
14
+ await sleep(25);
15
+ }
16
+ assert(predicate(), 'timed out waiting for expected condition');
17
+ }
18
+
19
+ function makeTaggedReqRes(sessionId, actionId) {
20
+ const req = new EventEmitter();
21
+ req.method = 'POST';
22
+ req.url = '/flush-timing';
23
+ req.headers = {
24
+ 'content-type': 'application/json',
25
+ 'x-bug-session-id': sessionId,
26
+ 'x-bug-action-id': actionId,
27
+ 'x-bug-request-start': String(Date.now()),
28
+ };
29
+ req.body = { subject: 'Avery Debugson' };
30
+ req.params = {};
31
+ req.query = {};
32
+
33
+ const res = new EventEmitter();
34
+ res.statusCode = 200;
35
+ res.getHeader = () => undefined;
36
+ res.setHeader = () => {};
37
+ res.json = function (body) { this.body = body; this.emit('finish'); return body; };
38
+ res.send = function (body) { this.body = body; this.emit('finish'); return body; };
39
+ res.write = () => true;
40
+ res.end = () => { res.emit('finish'); return true; };
41
+
42
+ return { req, res };
43
+ }
44
+
45
+ function flattenEvents(posts) {
46
+ return posts.flatMap((body) => Array.isArray(body?.events) ? body.events : []);
47
+ }
48
+
49
+ function countEvents(posts, eventType) {
50
+ return flattenEvents(posts).filter((event) => event?.event_type === eventType).length;
51
+ }
52
+
53
+ async function lateAsyncTrace() {
54
+ await sleep(2500);
55
+ return { done: true };
56
+ }
57
+
58
+ async function main() {
59
+ const capturedBodies = [];
60
+ const originalFetch = global.fetch;
61
+ global.fetch = async (_url, init) => {
62
+ capturedBodies.push(JSON.parse(String(init?.body || '{}')));
63
+ return { ok: true, status: 200, json: async () => ({ ok: true }) };
64
+ };
65
+
66
+ try {
67
+ initReproTracing({ instrument: false, logFunctionCalls: false });
68
+ const cfg = {
69
+ tenantId: 'TENANT_test',
70
+ appId: 'APP_test',
71
+ appSecret: 'secret',
72
+ captureHeaders: false,
73
+ privacy: { environment: 'dev' },
74
+ ingestBase: 'http://127.0.0.1:65535',
75
+ };
76
+ const { req, res } = makeTaggedReqRes('S_request_flush_timing', 'A_request_flush_timing');
77
+
78
+ await new Promise((resolve, reject) => {
79
+ void reproMiddleware(cfg)(req, res, async (err) => {
80
+ if (err) {
81
+ reject(err);
82
+ return;
83
+ }
84
+
85
+ try {
86
+ const pending = global.__repro_call(
87
+ lateAsyncTrace,
88
+ null,
89
+ [],
90
+ 'app',
91
+ 1,
92
+ 'lateAsyncTrace',
93
+ true,
94
+ );
95
+ Promise.resolve(pending).catch(() => undefined);
96
+ res.json({ ok: true });
97
+ resolve();
98
+ } catch (error) {
99
+ reject(error);
100
+ }
101
+ });
102
+ });
103
+
104
+ await waitFor(() => countEvents(capturedBodies, 'backend_request') === 1, 1500);
105
+ assert.strictEqual(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
106
+
107
+ await sleep(3500);
108
+ await flushIngestQueue();
109
+
110
+ assert.strictEqual(countEvents(capturedBodies, 'backend_request'), 1, JSON.stringify(capturedBodies));
111
+ assert(countEvents(capturedBodies, 'trace_batch') >= 1, JSON.stringify(capturedBodies));
112
+
113
+ console.log('request flush timing OK');
114
+ } finally {
115
+ await flushIngestQueue();
116
+ global.fetch = originalFetch;
117
+ }
118
+ }
119
+
120
+ main().catch((error) => {
121
+ console.error(error);
122
+ process.exitCode = 1;
123
+ });