@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 +219 -148
- package/package.json +2 -2
- package/src/index.ts +272 -182
- package/test/express-trace-http-args.test.js +126 -0
- package/test/fixtures/express-trace-http-args-controller.js +29 -0
- package/test/fixtures/express-trace-http-args-server.js +21 -0
- package/test/request-flush-timing.test.js +123 -0
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.
|
|
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
|
+
});
|