UNPKG

@sentry/node

Version:
310 lines (268 loc) 11.5 kB
import { _optionalChain } from '@sentry/utils/esm/buildPolyfills'; import { getCurrentHub, hasTracingEnabled, startTransaction, flush, runWithAsyncContext, withScope, captureException } from '@sentry/core'; import { isString, tracingContextFromHeaders, extractPathForTransaction, addRequestDataToTransaction, logger, addExceptionMechanism, normalize, dropUndefinedKeys } from '@sentry/utils'; import { extractRequestData } from './requestdata.js'; import { isAutoSessionTrackingEnabled } from './sdk.js'; export { extractRequestData, parseRequest } from './requestDataDeprecated.js'; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Express-compatible tracing handler. * @see Exposed as `Handlers.tracingHandler` */ function tracingHandler() { return function sentryTracingMiddleware( req, res, next, ) { const hub = getCurrentHub(); const options = _optionalChain([hub, 'access', _ => _.getClient, 'call', _2 => _2(), 'optionalAccess', _3 => _3.getOptions, 'call', _4 => _4()]); if ( !options || options.instrumenter !== 'sentry' || _optionalChain([req, 'access', _5 => _5.method, 'optionalAccess', _6 => _6.toUpperCase, 'call', _7 => _7()]) === 'OPTIONS' || _optionalChain([req, 'access', _8 => _8.method, 'optionalAccess', _9 => _9.toUpperCase, 'call', _10 => _10()]) === 'HEAD' ) { return next(); } const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; const baggage = _optionalChain([req, 'access', _11 => _11.headers, 'optionalAccess', _12 => _12.baggage]); const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTrace, baggage, ); hub.getScope().setPropagationContext(propagationContext); if (!hasTracingEnabled(options)) { return next(); } const [name, source] = extractPathForTransaction(req, { path: true, method: true }); const transaction = startTransaction( { name, op: 'http.server', origin: 'auto.http.node.tracingHandler', ...traceparentData, metadata: { dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, // The request should already have been stored in `scope.sdkProcessingMetadata` (which will become // `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the // off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to // be sure request: req, source, }, }, // extra context passed to the tracesSampler { request: extractRequestData(req) }, ); // We put the transaction on the scope so users can attach children to it hub.configureScope(scope => { scope.setSpan(transaction); }); // We also set __sentry_transaction on the response so people can grab the transaction there to add // spans to it later. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (res ).__sentry_transaction = transaction; res.once('finish', () => { // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction // closes setImmediate(() => { addRequestDataToTransaction(transaction, req); transaction.setHttpStatus(res.statusCode); transaction.finish(); }); }); next(); }; } /** * Backwards compatibility shim which can be removed in v8. Forces the given options to follow the * `AddRequestDataToEventOptions` interface. * * TODO (v8): Get rid of this, and stop passing `requestDataOptionsFromExpressHandler` to `setSDKProcessingMetadata`. */ function convertReqHandlerOptsToAddReqDataOpts( reqHandlerOptions = {}, ) { let addRequestDataOptions; if ('include' in reqHandlerOptions) { addRequestDataOptions = { include: reqHandlerOptions.include }; } else { // eslint-disable-next-line deprecation/deprecation const { ip, request, transaction, user } = reqHandlerOptions ; if (ip || request || transaction || user) { addRequestDataOptions = { include: dropUndefinedKeys({ ip, request, transaction, user }) }; } } return addRequestDataOptions; } /** * Express compatible request handler. * @see Exposed as `Handlers.requestHandler` */ function requestHandler( options, ) { // TODO (v8): Get rid of this const requestDataOptions = convertReqHandlerOptsToAddReqDataOpts(options); const currentHub = getCurrentHub(); const client = currentHub.getClient(); // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode if (client && isAutoSessionTrackingEnabled(client)) { client.initSessionFlusher(); // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode const scope = currentHub.getScope(); if (scope.getSession()) { scope.setSession(); } } return function sentryRequestMiddleware( req, res, next, ) { if (options && options.flushTimeout && options.flushTimeout > 0) { // eslint-disable-next-line @typescript-eslint/unbound-method const _end = res.end; res.end = function (chunk, encoding, cb) { void flush(options.flushTimeout) .then(() => { _end.call(this, chunk, encoding, cb); }) .then(null, e => { (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.error(e); _end.call(this, chunk, encoding, cb); }); }; } runWithAsyncContext(() => { const currentHub = getCurrentHub(); currentHub.configureScope(scope => { scope.setSDKProcessingMetadata({ request: req, // TODO (v8): Stop passing this requestDataOptionsFromExpressHandler: requestDataOptions, }); const client = currentHub.getClient(); if (isAutoSessionTrackingEnabled(client)) { const scope = currentHub.getScope(); // Set `status` of `RequestSession` to Ok, at the beginning of the request scope.setRequestSession({ status: 'ok' }); } }); res.once('finish', () => { const client = currentHub.getClient(); if (isAutoSessionTrackingEnabled(client)) { setImmediate(() => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (client && (client )._captureRequestSession) { // Calling _captureRequestSession to capture request session at the end of the request by incrementing // the correct SessionAggregates bucket i.e. crashed, errored or exited // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (client )._captureRequestSession(); } }); } }); next(); }); }; } /** JSDoc */ /** JSDoc */ function getStatusCodeFromResponse(error) { const statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode); return statusCode ? parseInt(statusCode , 10) : 500; } /** Returns true if response code is internal server error */ function defaultShouldHandleError(error) { const status = getStatusCodeFromResponse(error); return status >= 500; } /** * Express compatible error handler. * @see Exposed as `Handlers.errorHandler` */ function errorHandler(options ) { return function sentryErrorMiddleware( error, _req, res, next, ) { const shouldHandleError = (options && options.shouldHandleError) || defaultShouldHandleError; if (shouldHandleError(error)) { withScope(_scope => { // The request should already have been stored in `scope.sdkProcessingMetadata` by `sentryRequestMiddleware`, // but on the off chance someone is using `sentryErrorMiddleware` without `sentryRequestMiddleware`, it doesn't // hurt to be sure _scope.setSDKProcessingMetadata({ request: _req }); // For some reason we need to set the transaction on the scope again // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const transaction = (res ).__sentry_transaction ; if (transaction && _scope.getSpan() === undefined) { _scope.setSpan(transaction); } const client = getCurrentHub().getClient(); if (client && isAutoSessionTrackingEnabled(client)) { // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be // running in SessionAggregates mode // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const isSessionAggregatesMode = (client )._sessionFlusher !== undefined; if (isSessionAggregatesMode) { const requestSession = _scope.getRequestSession(); // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within // the bounds of a request, and if so the status is updated if (requestSession && requestSession.status !== undefined) { requestSession.status = 'crashed'; } } } _scope.addEventProcessor(event => { addExceptionMechanism(event, { type: 'middleware', handled: false }); return event; }); const eventId = captureException(error); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (res ).sentry = eventId; next(error); }); return; } next(error); }; } /** * Sentry tRPC middleware that names the handling transaction after the called procedure. * * Use the Sentry tRPC middleware in combination with the Sentry server integration, * e.g. Express Request Handlers or Next.js SDK. */ function trpcMiddleware(options = {}) { return function ({ path, type, next, rawInput }) { const hub = getCurrentHub(); const clientOptions = _optionalChain([hub, 'access', _13 => _13.getClient, 'call', _14 => _14(), 'optionalAccess', _15 => _15.getOptions, 'call', _16 => _16()]); const sentryTransaction = hub.getScope().getTransaction(); if (sentryTransaction) { sentryTransaction.setName(`trpc/${path}`, 'route'); sentryTransaction.op = 'rpc.server'; const trpcContext = { procedure_type: type, }; if (options.attachRpcInput !== undefined ? options.attachRpcInput : _optionalChain([clientOptions, 'optionalAccess', _17 => _17.sendDefaultPii])) { trpcContext.input = normalize(rawInput); } sentryTransaction.setContext('trpc', trpcContext); } return next(); }; } export { errorHandler, requestHandler, tracingHandler, trpcMiddleware }; //# sourceMappingURL=handlers.js.map