@reproapp/node-sdk 0.0.6 → 0.0.7

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
@@ -3695,6 +3695,7 @@ function reproMiddleware(cfg) {
3695
3695
  let idleTimer = null;
3696
3696
  let hardStopTimer = null;
3697
3697
  let flushPayload = null;
3698
+ let requestCaptureScheduled = false;
3698
3699
  let sessionDrainWait = null;
3699
3700
  const activeSpans = new Set();
3700
3701
  let anonymousSpanDepth = 0;
@@ -3784,6 +3785,179 @@ function reproMiddleware(cfg) {
3784
3785
  }
3785
3786
  scheduleIdleFlush();
3786
3787
  };
3788
+ const chooseRequestEndpoint = () => {
3789
+ const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
3790
+ const baseEvents = balanceTraceEvents(pendingEvents.slice());
3791
+ const orderedEvents = TRACE_ORDER_MODE === 'tree'
3792
+ ? reorderTraceEvents(baseEvents)
3793
+ : sortTraceEventsChronologically(baseEvents);
3794
+ const summary = summarizeEndpointFromEvents(orderedEvents);
3795
+ return {
3796
+ chosenEndpoint: summary.endpointTrace
3797
+ ?? summary.preferredAppTrace
3798
+ ?? summary.firstAppTrace
3799
+ ?? endpointTrace
3800
+ ?? preferredAppTrace
3801
+ ?? firstAppTrace
3802
+ ?? { fn: null, file: null, line: null, functionType: null },
3803
+ hasTraceEvents: orderedEvents.length > 0,
3804
+ };
3805
+ };
3806
+ const buildRequestCapturePayloadAsync = async (chosenEndpoint, hasTraceEvents) => {
3807
+ const endpointTraceCtx = (() => {
3808
+ if (!chosenEndpoint?.fn && !chosenEndpoint?.file)
3809
+ return null;
3810
+ return {
3811
+ type: 'enter',
3812
+ eventType: 'enter',
3813
+ fn: chosenEndpoint.fn ?? undefined,
3814
+ wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
3815
+ file: chosenEndpoint.file ?? null,
3816
+ line: chosenEndpoint.line ?? null,
3817
+ functionType: chosenEndpoint.functionType ?? null,
3818
+ library: inferLibraryNameFromFile(chosenEndpoint.file),
3819
+ };
3820
+ })();
3821
+ const activePrivacy = resolvePrivacy();
3822
+ const requestBodyRaw = req.body;
3823
+ const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
3824
+ ?? await materializeInlinePrivacyValueAsync('request.body', sanitizeRequestSnapshot(requestBodyRaw), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3825
+ const requestBody = requestBodyMaterialization.value;
3826
+ const requestParams = await applyPrivacyThenMaskAsync('request.params', sanitizeRequestSnapshot(req.params), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3827
+ const requestQuery = await applyPrivacyThenMaskAsync('request.query', sanitizeRequestSnapshot(req.query), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3828
+ const maskedHeaders = await applyPrivacyThenMaskAsync('request.headers', requestHeaders, cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3829
+ const responseBodyMaterialization = capturedBody === undefined
3830
+ ? { value: undefined }
3831
+ : limitRawInlinePrivacyValue('response.body', capturedBody)
3832
+ ?? await materializeInlinePrivacyValueAsync('response.body', sanitizeRequestSnapshot(capturedBody), cfg, maskReq, endpointTraceCtx, masking, activePrivacy);
3833
+ const responseBody = responseBodyMaterialization.value;
3834
+ const requestValueEntries = [];
3835
+ const bodyValueCapture = requestBodyMaterialization.skipped
3836
+ ? undefined
3837
+ : await maybeCaptureRequestValueAsync({
3838
+ target: 'request.body',
3839
+ rawValue: req.body,
3840
+ previewValue: requestBody,
3841
+ capture: {
3842
+ runtimeConfig: cfg,
3843
+ captureHeaders: cfg.captureHeaders,
3844
+ maskReq,
3845
+ trace: endpointTraceCtx,
3846
+ masking,
3847
+ privacy: activePrivacy,
3848
+ },
3849
+ }, requestValueEntries);
3850
+ const paramsValueCapture = await maybeCaptureRequestValueAsync({
3851
+ target: 'request.params',
3852
+ rawValue: req.params,
3853
+ previewValue: requestParams,
3854
+ capture: {
3855
+ runtimeConfig: cfg,
3856
+ captureHeaders: cfg.captureHeaders,
3857
+ maskReq,
3858
+ trace: endpointTraceCtx,
3859
+ masking,
3860
+ privacy: activePrivacy,
3861
+ },
3862
+ }, requestValueEntries);
3863
+ const queryValueCapture = await maybeCaptureRequestValueAsync({
3864
+ target: 'request.query',
3865
+ rawValue: req.query,
3866
+ previewValue: requestQuery,
3867
+ capture: {
3868
+ runtimeConfig: cfg,
3869
+ captureHeaders: cfg.captureHeaders,
3870
+ maskReq,
3871
+ trace: endpointTraceCtx,
3872
+ masking,
3873
+ privacy: activePrivacy,
3874
+ },
3875
+ }, requestValueEntries);
3876
+ const headersValueCapture = await maybeCaptureRequestValueAsync({
3877
+ target: 'request.headers',
3878
+ rawValue: req.headers,
3879
+ previewValue: maskedHeaders,
3880
+ capture: {
3881
+ runtimeConfig: cfg,
3882
+ captureHeaders: cfg.captureHeaders,
3883
+ maskReq,
3884
+ trace: endpointTraceCtx,
3885
+ masking,
3886
+ privacy: activePrivacy,
3887
+ },
3888
+ }, requestValueEntries);
3889
+ const respBodyValueCapture = responseBodyMaterialization.skipped
3890
+ ? undefined
3891
+ : await maybeCaptureRequestValueAsync({
3892
+ target: 'response.body',
3893
+ rawValue: capturedBody,
3894
+ previewValue: responseBody,
3895
+ capture: {
3896
+ runtimeConfig: cfg,
3897
+ captureHeaders: cfg.captureHeaders,
3898
+ maskReq,
3899
+ trace: endpointTraceCtx,
3900
+ masking,
3901
+ privacy: activePrivacy,
3902
+ },
3903
+ }, requestValueEntries);
3904
+ const requestPayload = {
3905
+ rid,
3906
+ method: req.method,
3907
+ url,
3908
+ path,
3909
+ status: res.statusCode,
3910
+ durMs: Date.now() - t0,
3911
+ headers: maskedHeaders,
3912
+ key,
3913
+ respBody: responseBody,
3914
+ trace: hasTraceEvents ? undefined : [],
3915
+ };
3916
+ if (requestBody !== undefined)
3917
+ requestPayload.body = requestBody;
3918
+ if (bodyValueCapture)
3919
+ requestPayload.bodyValueCapture = bodyValueCapture;
3920
+ if (requestParams !== undefined)
3921
+ requestPayload.params = requestParams;
3922
+ if (paramsValueCapture)
3923
+ requestPayload.paramsValueCapture = paramsValueCapture;
3924
+ if (requestQuery !== undefined)
3925
+ requestPayload.query = requestQuery;
3926
+ if (queryValueCapture)
3927
+ requestPayload.queryValueCapture = queryValueCapture;
3928
+ if (headersValueCapture)
3929
+ requestPayload.headersValueCapture = headersValueCapture;
3930
+ if (respBodyValueCapture)
3931
+ requestPayload.respBodyValueCapture = respBodyValueCapture;
3932
+ if (requestBodyMaterialization.skipped) {
3933
+ requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
3934
+ }
3935
+ if (responseBodyMaterialization.skipped) {
3936
+ requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
3937
+ }
3938
+ requestPayload.entryPoint = chosenEndpoint;
3939
+ return { requestPayload, requestValueEntries };
3940
+ };
3941
+ const emitRequestCaptureAsync = async () => {
3942
+ if (requestCaptureScheduled)
3943
+ return;
3944
+ requestCaptureScheduled = true;
3945
+ try {
3946
+ const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
3947
+ const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(chosenEndpoint, hasTraceEvents);
3948
+ post(cfg, sid, {
3949
+ entries: [{
3950
+ actionId: aid,
3951
+ request: requestPayload,
3952
+ requestValues: requestValueEntries.length ? requestValueEntries : undefined,
3953
+ t: requestEpochMs,
3954
+ }]
3955
+ });
3956
+ }
3957
+ catch {
3958
+ // never break user code
3959
+ }
3960
+ };
3787
3961
  try {
3788
3962
  if (__TRACER__?.tracer?.on) {
3789
3963
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -3876,6 +4050,7 @@ function reproMiddleware(cfg) {
3876
4050
  : Buffer.from(chunks.map(String).join(''));
3877
4051
  capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
3878
4052
  }
4053
+ void emitRequestCaptureAsync();
3879
4054
  if (!flushPayload) {
3880
4055
  flushPayload = async () => {
3881
4056
  try {
@@ -3890,155 +4065,7 @@ function reproMiddleware(cfg) {
3890
4065
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
3891
4066
  ? reorderTraceEvents(baseEvents)
3892
4067
  : 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
4068
  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
4069
  if (traceBatches.length) {
4043
4070
  for (let i = 0; i < traceBatches.length; i++) {
4044
4071
  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.7",
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 -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
@@ -4499,6 +4499,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4499
4499
  let idleTimer: NodeJS.Timeout | null = null;
4500
4500
  let hardStopTimer: NodeJS.Timeout | null = null;
4501
4501
  let flushPayload: null | (() => Promise<void>) = null;
4502
+ let requestCaptureScheduled = false;
4502
4503
  let sessionDrainWait: Promise<void> | null = null;
4503
4504
  const activeSpans = new Set<string>();
4504
4505
  let anonymousSpanDepth = 0;
@@ -4572,6 +4573,226 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4572
4573
  scheduleIdleFlush();
4573
4574
  };
4574
4575
 
4576
+ const chooseRequestEndpoint = (): {
4577
+ chosenEndpoint: EndpointTraceInfo;
4578
+ hasTraceEvents: boolean;
4579
+ } => {
4580
+ const pendingEvents = preparePendingTraceEventsForFlush(events.slice());
4581
+ const baseEvents = balanceTraceEvents(pendingEvents.slice() as TraceEventRecord[]);
4582
+ const orderedEvents = TRACE_ORDER_MODE === 'tree'
4583
+ ? reorderTraceEvents(baseEvents)
4584
+ : sortTraceEventsChronologically(baseEvents);
4585
+ const summary = summarizeEndpointFromEvents(orderedEvents);
4586
+ return {
4587
+ chosenEndpoint: summary.endpointTrace
4588
+ ?? summary.preferredAppTrace
4589
+ ?? summary.firstAppTrace
4590
+ ?? endpointTrace
4591
+ ?? preferredAppTrace
4592
+ ?? firstAppTrace
4593
+ ?? { fn: null, file: null, line: null, functionType: null },
4594
+ hasTraceEvents: orderedEvents.length > 0,
4595
+ };
4596
+ };
4597
+
4598
+ const buildRequestCapturePayloadAsync = async (
4599
+ chosenEndpoint: EndpointTraceInfo,
4600
+ hasTraceEvents: boolean,
4601
+ ): Promise<{
4602
+ requestPayload: Record<string, any>;
4603
+ requestValueEntries: TraceValueBatchEntry[];
4604
+ }> => {
4605
+ const endpointTraceCtx: TraceEventForFilter | null = (() => {
4606
+ if (!chosenEndpoint?.fn && !chosenEndpoint?.file) return null;
4607
+ return {
4608
+ type: 'enter',
4609
+ eventType: 'enter',
4610
+ fn: chosenEndpoint.fn ?? undefined,
4611
+ wrapperClass: inferWrapperClassFromFn(chosenEndpoint.fn),
4612
+ file: chosenEndpoint.file ?? null,
4613
+ line: chosenEndpoint.line ?? null,
4614
+ functionType: chosenEndpoint.functionType ?? null,
4615
+ library: inferLibraryNameFromFile(chosenEndpoint.file),
4616
+ };
4617
+ })();
4618
+ const activePrivacy = resolvePrivacy();
4619
+
4620
+ const requestBodyRaw = (req as any).body;
4621
+ const requestBodyMaterialization = limitRawInlinePrivacyValue('request.body', requestBodyRaw)
4622
+ ?? await materializeInlinePrivacyValueAsync(
4623
+ 'request.body',
4624
+ sanitizeRequestSnapshot(requestBodyRaw),
4625
+ cfg,
4626
+ maskReq,
4627
+ endpointTraceCtx,
4628
+ masking,
4629
+ activePrivacy,
4630
+ );
4631
+ const requestBody = requestBodyMaterialization.value;
4632
+ const requestParams = await applyPrivacyThenMaskAsync(
4633
+ 'request.params',
4634
+ sanitizeRequestSnapshot((req as any).params),
4635
+ cfg,
4636
+ maskReq,
4637
+ endpointTraceCtx,
4638
+ masking,
4639
+ activePrivacy,
4640
+ );
4641
+ const requestQuery = await applyPrivacyThenMaskAsync(
4642
+ 'request.query',
4643
+ sanitizeRequestSnapshot((req as any).query),
4644
+ cfg,
4645
+ maskReq,
4646
+ endpointTraceCtx,
4647
+ masking,
4648
+ activePrivacy,
4649
+ );
4650
+ const maskedHeaders = await applyPrivacyThenMaskAsync(
4651
+ 'request.headers',
4652
+ requestHeaders,
4653
+ cfg,
4654
+ maskReq,
4655
+ endpointTraceCtx,
4656
+ masking,
4657
+ activePrivacy,
4658
+ );
4659
+ const responseBodyMaterialization = capturedBody === undefined
4660
+ ? { value: undefined }
4661
+ : limitRawInlinePrivacyValue('response.body', capturedBody)
4662
+ ?? await materializeInlinePrivacyValueAsync(
4663
+ 'response.body',
4664
+ sanitizeRequestSnapshot(capturedBody),
4665
+ cfg,
4666
+ maskReq,
4667
+ endpointTraceCtx,
4668
+ masking,
4669
+ activePrivacy,
4670
+ );
4671
+ const responseBody = responseBodyMaterialization.value;
4672
+ const requestValueEntries: TraceValueBatchEntry[] = [];
4673
+ const bodyValueCapture = requestBodyMaterialization.skipped
4674
+ ? undefined
4675
+ : await maybeCaptureRequestValueAsync({
4676
+ target: 'request.body',
4677
+ rawValue: (req as any).body,
4678
+ previewValue: requestBody,
4679
+ capture: {
4680
+ runtimeConfig: cfg,
4681
+ captureHeaders: cfg.captureHeaders,
4682
+ maskReq,
4683
+ trace: endpointTraceCtx,
4684
+ masking,
4685
+ privacy: activePrivacy,
4686
+ },
4687
+ }, requestValueEntries);
4688
+ const paramsValueCapture = await maybeCaptureRequestValueAsync({
4689
+ target: 'request.params',
4690
+ rawValue: (req as any).params,
4691
+ previewValue: requestParams,
4692
+ capture: {
4693
+ runtimeConfig: cfg,
4694
+ captureHeaders: cfg.captureHeaders,
4695
+ maskReq,
4696
+ trace: endpointTraceCtx,
4697
+ masking,
4698
+ privacy: activePrivacy,
4699
+ },
4700
+ }, requestValueEntries);
4701
+ const queryValueCapture = await maybeCaptureRequestValueAsync({
4702
+ target: 'request.query',
4703
+ rawValue: (req as any).query,
4704
+ previewValue: requestQuery,
4705
+ capture: {
4706
+ runtimeConfig: cfg,
4707
+ captureHeaders: cfg.captureHeaders,
4708
+ maskReq,
4709
+ trace: endpointTraceCtx,
4710
+ masking,
4711
+ privacy: activePrivacy,
4712
+ },
4713
+ }, requestValueEntries);
4714
+ const headersValueCapture = await maybeCaptureRequestValueAsync({
4715
+ target: 'request.headers',
4716
+ rawValue: req.headers,
4717
+ previewValue: maskedHeaders,
4718
+ capture: {
4719
+ runtimeConfig: cfg,
4720
+ captureHeaders: cfg.captureHeaders,
4721
+ maskReq,
4722
+ trace: endpointTraceCtx,
4723
+ masking,
4724
+ privacy: activePrivacy,
4725
+ },
4726
+ }, requestValueEntries);
4727
+ const respBodyValueCapture = responseBodyMaterialization.skipped
4728
+ ? undefined
4729
+ : await maybeCaptureRequestValueAsync({
4730
+ target: 'response.body',
4731
+ rawValue: capturedBody,
4732
+ previewValue: responseBody,
4733
+ capture: {
4734
+ runtimeConfig: cfg,
4735
+ captureHeaders: cfg.captureHeaders,
4736
+ maskReq,
4737
+ trace: endpointTraceCtx,
4738
+ masking,
4739
+ privacy: activePrivacy,
4740
+ },
4741
+ }, requestValueEntries);
4742
+
4743
+ const requestPayload: Record<string, any> = {
4744
+ rid,
4745
+ method: req.method,
4746
+ url,
4747
+ path,
4748
+ status: res.statusCode,
4749
+ durMs: Date.now() - t0,
4750
+ headers: maskedHeaders,
4751
+ key,
4752
+ respBody: responseBody,
4753
+ trace: hasTraceEvents ? undefined : [],
4754
+ };
4755
+ if (requestBody !== undefined) requestPayload.body = requestBody;
4756
+ if (bodyValueCapture) requestPayload.bodyValueCapture = bodyValueCapture;
4757
+ if (requestParams !== undefined) requestPayload.params = requestParams;
4758
+ if (paramsValueCapture) requestPayload.paramsValueCapture = paramsValueCapture;
4759
+ if (requestQuery !== undefined) requestPayload.query = requestQuery;
4760
+ if (queryValueCapture) requestPayload.queryValueCapture = queryValueCapture;
4761
+ if (headersValueCapture) requestPayload.headersValueCapture = headersValueCapture;
4762
+ if (respBodyValueCapture) requestPayload.respBodyValueCapture = respBodyValueCapture;
4763
+ if (requestBodyMaterialization.skipped) {
4764
+ requestPayload.bodyMaterialization = requestBodyMaterialization.skipped;
4765
+ }
4766
+ if (responseBodyMaterialization.skipped) {
4767
+ requestPayload.respBodyMaterialization = responseBodyMaterialization.skipped;
4768
+ }
4769
+ requestPayload.entryPoint = chosenEndpoint;
4770
+
4771
+ return { requestPayload, requestValueEntries };
4772
+ };
4773
+
4774
+ const emitRequestCaptureAsync = async (): Promise<void> => {
4775
+ if (requestCaptureScheduled) return;
4776
+ requestCaptureScheduled = true;
4777
+ try {
4778
+ const { chosenEndpoint, hasTraceEvents } = chooseRequestEndpoint();
4779
+ const { requestPayload, requestValueEntries } = await buildRequestCapturePayloadAsync(
4780
+ chosenEndpoint,
4781
+ hasTraceEvents,
4782
+ );
4783
+ post(cfg, sid, {
4784
+ entries: [{
4785
+ actionId: aid,
4786
+ request: requestPayload,
4787
+ requestValues: requestValueEntries.length ? requestValueEntries : undefined,
4788
+ t: requestEpochMs,
4789
+ }]
4790
+ });
4791
+ } catch {
4792
+ // never break user code
4793
+ }
4794
+ };
4795
+
4575
4796
  try {
4576
4797
  if (__TRACER__?.tracer?.on) {
4577
4798
  const getTid = __TRACER__?.getCurrentTraceId;
@@ -4667,6 +4888,8 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4667
4888
  capturedBody = coerceBodyToStorable(buf, res.getHeader?.('content-type'));
4668
4889
  }
4669
4890
 
4891
+ void emitRequestCaptureAsync();
4892
+
4670
4893
  if (!flushPayload) {
4671
4894
  flushPayload = async () => {
4672
4895
  try {
@@ -4681,189 +4904,7 @@ export function reproMiddleware(cfg: ReproMiddlewareConfig) {
4681
4904
  const orderedEvents = TRACE_ORDER_MODE === 'tree'
4682
4905
  ? reorderTraceEvents(baseEvents)
4683
4906
  : 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
4907
  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
4908
 
4868
4909
  if (traceBatches.length) {
4869
4910
  for (let i = 0; i < traceBatches.length; i++) {
@@ -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
+ });