@sentry/node
Version:
Official Sentry SDK for Node.js
310 lines (268 loc) • 11.5 kB
JavaScript
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