290 lines
10 KiB
JavaScript
290 lines
10 KiB
JavaScript
|
import { _optionalChain } from '@sentry/utils';
|
||
|
import { hasTracingEnabled, continueTrace, startTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getCurrentScope, setHttpStatus, getClient, flush, runWithAsyncContext, withScope, getActiveSpan, captureException } from '@sentry/core';
|
||
|
import { isString, extractPathForTransaction, extractRequestData, addRequestDataToTransaction, logger, dropUndefinedKeys } from '@sentry/utils';
|
||
|
import { DEBUG_BUILD } from './debug-build.js';
|
||
|
import { isAutoSessionTrackingEnabled } from './sdk.js';
|
||
|
import { trpcMiddleware as trpcMiddleware$1 } from './trpc.js';
|
||
|
export { extractRequestData, parseRequest } from './requestDataDeprecated.js';
|
||
|
|
||
|
/**
|
||
|
* Express-compatible tracing handler.
|
||
|
* @see Exposed as `Handlers.tracingHandler`
|
||
|
*/
|
||
|
function tracingHandler()
|
||
|
|
||
|
{
|
||
|
return function sentryTracingMiddleware(
|
||
|
req,
|
||
|
res,
|
||
|
next,
|
||
|
) {
|
||
|
const options = _optionalChain([getClient, 'call', _ => _(), 'optionalAccess', _2 => _2.getOptions, 'call', _3 => _3()]);
|
||
|
|
||
|
if (
|
||
|
!options ||
|
||
|
options.instrumenter !== 'sentry' ||
|
||
|
_optionalChain([req, 'access', _4 => _4.method, 'optionalAccess', _5 => _5.toUpperCase, 'call', _6 => _6()]) === 'OPTIONS' ||
|
||
|
_optionalChain([req, 'access', _7 => _7.method, 'optionalAccess', _8 => _8.toUpperCase, 'call', _9 => _9()]) === 'HEAD'
|
||
|
) {
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined;
|
||
|
const baggage = _optionalChain([req, 'access', _10 => _10.headers, 'optionalAccess', _11 => _11.baggage]);
|
||
|
if (!hasTracingEnabled(options)) {
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
const [name, source] = extractPathForTransaction(req, { path: true, method: true });
|
||
|
const transaction = continueTrace({ sentryTrace, baggage }, ctx =>
|
||
|
// TODO: Refactor this to use `startSpan()`
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
startTransaction(
|
||
|
{
|
||
|
name,
|
||
|
op: 'http.server',
|
||
|
origin: 'auto.http.node.tracingHandler',
|
||
|
...ctx,
|
||
|
data: {
|
||
|
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
|
||
|
},
|
||
|
metadata: {
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
...ctx.metadata,
|
||
|
// 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,
|
||
|
},
|
||
|
},
|
||
|
// extra context passed to the tracesSampler
|
||
|
{ request: extractRequestData(req) },
|
||
|
),
|
||
|
);
|
||
|
|
||
|
// We put the transaction on the scope so users can attach children to it
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
getCurrentScope().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(() => {
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
addRequestDataToTransaction(transaction, req);
|
||
|
setHttpStatus(transaction, res.statusCode);
|
||
|
transaction.end();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
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 client = 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 = getCurrentScope();
|
||
|
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 => {
|
||
|
DEBUG_BUILD && logger.error(e);
|
||
|
_end.call(this, chunk, encoding, cb);
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
runWithAsyncContext(() => {
|
||
|
const scope = getCurrentScope();
|
||
|
scope.setSDKProcessingMetadata({
|
||
|
request: req,
|
||
|
// TODO (v8): Stop passing this
|
||
|
requestDataOptionsFromExpressHandler: requestDataOptions,
|
||
|
});
|
||
|
|
||
|
const client = getClient();
|
||
|
if (isAutoSessionTrackingEnabled(client)) {
|
||
|
// Set `status` of `RequestSession` to Ok, at the beginning of the request
|
||
|
scope.setRequestSession({ status: 'ok' });
|
||
|
}
|
||
|
|
||
|
res.once('finish', () => {
|
||
|
const client = 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 && !getActiveSpan()) {
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
_scope.setSpan(transaction);
|
||
|
}
|
||
|
|
||
|
const client = 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';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } });
|
||
|
// 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.
|
||
|
*
|
||
|
* @deprecated Please use the top level export instead:
|
||
|
* ```
|
||
|
* // OLD
|
||
|
* import * as Sentry from '@sentry/node';
|
||
|
* Sentry.Handlers.trpcMiddleware();
|
||
|
*
|
||
|
* // NEW
|
||
|
* import * as Sentry from '@sentry/node';
|
||
|
* Sentry.trpcMiddleware();
|
||
|
* ```
|
||
|
*/
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
const trpcMiddleware = trpcMiddleware$1;
|
||
|
|
||
|
export { errorHandler, requestHandler, tracingHandler, trpcMiddleware };
|
||
|
//# sourceMappingURL=handlers.js.map
|