handlers.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import { _optionalChain } from '@sentry/utils';
  2. import { hasTracingEnabled, continueTrace, startTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getCurrentScope, setHttpStatus, getClient, flush, runWithAsyncContext, withScope, getActiveSpan, captureException } from '@sentry/core';
  3. import { isString, extractPathForTransaction, extractRequestData, addRequestDataToTransaction, logger, normalize, isThenable, dropUndefinedKeys } from '@sentry/utils';
  4. import { DEBUG_BUILD } from './debug-build.js';
  5. import { isAutoSessionTrackingEnabled } from './sdk.js';
  6. export { extractRequestData, parseRequest } from './requestDataDeprecated.js';
  7. /**
  8. * Express-compatible tracing handler.
  9. * @see Exposed as `Handlers.tracingHandler`
  10. */
  11. function tracingHandler()
  12. {
  13. return function sentryTracingMiddleware(
  14. req,
  15. res,
  16. next,
  17. ) {
  18. const options = _optionalChain([getClient, 'call', _ => _(), 'optionalAccess', _2 => _2.getOptions, 'call', _3 => _3()]);
  19. if (
  20. !options ||
  21. options.instrumenter !== 'sentry' ||
  22. _optionalChain([req, 'access', _4 => _4.method, 'optionalAccess', _5 => _5.toUpperCase, 'call', _6 => _6()]) === 'OPTIONS' ||
  23. _optionalChain([req, 'access', _7 => _7.method, 'optionalAccess', _8 => _8.toUpperCase, 'call', _9 => _9()]) === 'HEAD'
  24. ) {
  25. return next();
  26. }
  27. const sentryTrace = req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined;
  28. const baggage = _optionalChain([req, 'access', _10 => _10.headers, 'optionalAccess', _11 => _11.baggage]);
  29. if (!hasTracingEnabled(options)) {
  30. return next();
  31. }
  32. const [name, source] = extractPathForTransaction(req, { path: true, method: true });
  33. const transaction = continueTrace({ sentryTrace, baggage }, ctx =>
  34. // TODO: Refactor this to use `startSpan()`
  35. // eslint-disable-next-line deprecation/deprecation
  36. startTransaction(
  37. {
  38. name,
  39. op: 'http.server',
  40. origin: 'auto.http.node.tracingHandler',
  41. ...ctx,
  42. data: {
  43. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
  44. },
  45. metadata: {
  46. // eslint-disable-next-line deprecation/deprecation
  47. ...ctx.metadata,
  48. // The request should already have been stored in `scope.sdkProcessingMetadata` (which will become
  49. // `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the
  50. // off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to
  51. // be sure
  52. request: req,
  53. },
  54. },
  55. // extra context passed to the tracesSampler
  56. { request: extractRequestData(req) },
  57. ),
  58. );
  59. // We put the transaction on the scope so users can attach children to it
  60. // eslint-disable-next-line deprecation/deprecation
  61. getCurrentScope().setSpan(transaction);
  62. // We also set __sentry_transaction on the response so people can grab the transaction there to add
  63. // spans to it later.
  64. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  65. (res ).__sentry_transaction = transaction;
  66. res.once('finish', () => {
  67. // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
  68. // closes
  69. setImmediate(() => {
  70. addRequestDataToTransaction(transaction, req);
  71. setHttpStatus(transaction, res.statusCode);
  72. transaction.end();
  73. });
  74. });
  75. next();
  76. };
  77. }
  78. /**
  79. * Backwards compatibility shim which can be removed in v8. Forces the given options to follow the
  80. * `AddRequestDataToEventOptions` interface.
  81. *
  82. * TODO (v8): Get rid of this, and stop passing `requestDataOptionsFromExpressHandler` to `setSDKProcessingMetadata`.
  83. */
  84. function convertReqHandlerOptsToAddReqDataOpts(
  85. reqHandlerOptions = {},
  86. ) {
  87. let addRequestDataOptions;
  88. if ('include' in reqHandlerOptions) {
  89. addRequestDataOptions = { include: reqHandlerOptions.include };
  90. } else {
  91. // eslint-disable-next-line deprecation/deprecation
  92. const { ip, request, transaction, user } = reqHandlerOptions ;
  93. if (ip || request || transaction || user) {
  94. addRequestDataOptions = { include: dropUndefinedKeys({ ip, request, transaction, user }) };
  95. }
  96. }
  97. return addRequestDataOptions;
  98. }
  99. /**
  100. * Express compatible request handler.
  101. * @see Exposed as `Handlers.requestHandler`
  102. */
  103. function requestHandler(
  104. options,
  105. ) {
  106. // TODO (v8): Get rid of this
  107. const requestDataOptions = convertReqHandlerOptsToAddReqDataOpts(options);
  108. const client = getClient();
  109. // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the
  110. // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode
  111. if (client && isAutoSessionTrackingEnabled(client)) {
  112. client.initSessionFlusher();
  113. // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode
  114. const scope = getCurrentScope();
  115. if (scope.getSession()) {
  116. scope.setSession();
  117. }
  118. }
  119. return function sentryRequestMiddleware(
  120. req,
  121. res,
  122. next,
  123. ) {
  124. if (options && options.flushTimeout && options.flushTimeout > 0) {
  125. // eslint-disable-next-line @typescript-eslint/unbound-method
  126. const _end = res.end;
  127. res.end = function (chunk, encoding, cb) {
  128. void flush(options.flushTimeout)
  129. .then(() => {
  130. _end.call(this, chunk, encoding, cb);
  131. })
  132. .then(null, e => {
  133. DEBUG_BUILD && logger.error(e);
  134. _end.call(this, chunk, encoding, cb);
  135. });
  136. };
  137. }
  138. runWithAsyncContext(() => {
  139. const scope = getCurrentScope();
  140. scope.setSDKProcessingMetadata({
  141. request: req,
  142. // TODO (v8): Stop passing this
  143. requestDataOptionsFromExpressHandler: requestDataOptions,
  144. });
  145. const client = getClient();
  146. if (isAutoSessionTrackingEnabled(client)) {
  147. // Set `status` of `RequestSession` to Ok, at the beginning of the request
  148. scope.setRequestSession({ status: 'ok' });
  149. }
  150. res.once('finish', () => {
  151. const client = getClient();
  152. if (isAutoSessionTrackingEnabled(client)) {
  153. setImmediate(() => {
  154. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  155. if (client && (client )._captureRequestSession) {
  156. // Calling _captureRequestSession to capture request session at the end of the request by incrementing
  157. // the correct SessionAggregates bucket i.e. crashed, errored or exited
  158. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  159. (client )._captureRequestSession();
  160. }
  161. });
  162. }
  163. });
  164. next();
  165. });
  166. };
  167. }
  168. /** JSDoc */
  169. /** JSDoc */
  170. function getStatusCodeFromResponse(error) {
  171. const statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode);
  172. return statusCode ? parseInt(statusCode , 10) : 500;
  173. }
  174. /** Returns true if response code is internal server error */
  175. function defaultShouldHandleError(error) {
  176. const status = getStatusCodeFromResponse(error);
  177. return status >= 500;
  178. }
  179. /**
  180. * Express compatible error handler.
  181. * @see Exposed as `Handlers.errorHandler`
  182. */
  183. function errorHandler(options
  184. )
  185. {
  186. return function sentryErrorMiddleware(
  187. error,
  188. _req,
  189. res,
  190. next,
  191. ) {
  192. const shouldHandleError = (options && options.shouldHandleError) || defaultShouldHandleError;
  193. if (shouldHandleError(error)) {
  194. withScope(_scope => {
  195. // The request should already have been stored in `scope.sdkProcessingMetadata` by `sentryRequestMiddleware`,
  196. // but on the off chance someone is using `sentryErrorMiddleware` without `sentryRequestMiddleware`, it doesn't
  197. // hurt to be sure
  198. _scope.setSDKProcessingMetadata({ request: _req });
  199. // For some reason we need to set the transaction on the scope again
  200. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  201. const transaction = (res ).__sentry_transaction ;
  202. if (transaction && !getActiveSpan()) {
  203. // eslint-disable-next-line deprecation/deprecation
  204. _scope.setSpan(transaction);
  205. }
  206. const client = getClient();
  207. if (client && isAutoSessionTrackingEnabled(client)) {
  208. // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the
  209. // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only
  210. // instantiated when the the`requestHandler` middleware is initialised, which indicates that we should be
  211. // running in SessionAggregates mode
  212. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  213. const isSessionAggregatesMode = (client )._sessionFlusher !== undefined;
  214. if (isSessionAggregatesMode) {
  215. const requestSession = _scope.getRequestSession();
  216. // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a
  217. // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within
  218. // the bounds of a request, and if so the status is updated
  219. if (requestSession && requestSession.status !== undefined) {
  220. requestSession.status = 'crashed';
  221. }
  222. }
  223. }
  224. const eventId = captureException(error, { mechanism: { type: 'middleware', handled: false } });
  225. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  226. (res ).sentry = eventId;
  227. next(error);
  228. });
  229. return;
  230. }
  231. next(error);
  232. };
  233. }
  234. /**
  235. * Sentry tRPC middleware that names the handling transaction after the called procedure.
  236. *
  237. * Use the Sentry tRPC middleware in combination with the Sentry server integration,
  238. * e.g. Express Request Handlers or Next.js SDK.
  239. */
  240. function trpcMiddleware(options = {}) {
  241. return function ({ path, type, next, rawInput }) {
  242. const clientOptions = _optionalChain([getClient, 'call', _12 => _12(), 'optionalAccess', _13 => _13.getOptions, 'call', _14 => _14()]);
  243. // eslint-disable-next-line deprecation/deprecation
  244. const sentryTransaction = getCurrentScope().getTransaction();
  245. if (sentryTransaction) {
  246. sentryTransaction.updateName(`trpc/${path}`);
  247. sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
  248. sentryTransaction.op = 'rpc.server';
  249. const trpcContext = {
  250. procedure_type: type,
  251. };
  252. if (options.attachRpcInput !== undefined ? options.attachRpcInput : _optionalChain([clientOptions, 'optionalAccess', _15 => _15.sendDefaultPii])) {
  253. trpcContext.input = normalize(rawInput);
  254. }
  255. // TODO: Can we rewrite this to an attribute? Or set this on the scope?
  256. // eslint-disable-next-line deprecation/deprecation
  257. sentryTransaction.setContext('trpc', trpcContext);
  258. }
  259. function captureIfError(nextResult) {
  260. if (!nextResult.ok) {
  261. captureException(nextResult.error, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } });
  262. }
  263. }
  264. let maybePromiseResult;
  265. try {
  266. maybePromiseResult = next();
  267. } catch (e) {
  268. captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } });
  269. throw e;
  270. }
  271. if (isThenable(maybePromiseResult)) {
  272. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  273. Promise.resolve(maybePromiseResult).then(
  274. nextResult => {
  275. captureIfError(nextResult );
  276. },
  277. e => {
  278. captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } });
  279. },
  280. );
  281. } else {
  282. captureIfError(maybePromiseResult );
  283. }
  284. // We return the original promise just to be safe.
  285. return maybePromiseResult;
  286. };
  287. }
  288. export { errorHandler, requestHandler, tracingHandler, trpcMiddleware };
  289. //# sourceMappingURL=handlers.js.map