wrapApiHandlerWithSentry.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import { _optionalChain } from '@sentry/utils';
  2. import { addTracingExtensions, runWithAsyncContext, continueTrace, getCurrentScope, startSpanManual, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setHttpStatus, captureException } from '@sentry/core';
  3. import { logger, isString, stripUrlQueryAndFragment, consoleSandbox, objectify } from '@sentry/utils';
  4. import { platformSupportsStreaming } from './utils/platformSupportsStreaming.js';
  5. import { flushQueue } from './utils/responseEnd.js';
  6. /**
  7. * Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only
  8. * applies it if it hasn't already been applied.
  9. *
  10. * @param apiHandler The handler exported from the user's API page route file, which may or may not already be
  11. * wrapped with `withSentry`
  12. * @param parameterizedRoute The page's route, passed in via the proxy loader
  13. * @returns The wrapped handler
  14. */
  15. function wrapApiHandlerWithSentry(apiHandler, parameterizedRoute) {
  16. return new Proxy(apiHandler, {
  17. apply: (wrappingTarget, thisArg, args) => {
  18. // eslint-disable-next-line deprecation/deprecation
  19. return withSentry(wrappingTarget, parameterizedRoute).apply(thisArg, args);
  20. },
  21. });
  22. }
  23. /**
  24. * @deprecated Use `wrapApiHandlerWithSentry()` instead
  25. */
  26. const withSentryAPI = wrapApiHandlerWithSentry;
  27. /**
  28. * Legacy function for manually wrapping API route handlers, now used as the innards of `wrapApiHandlerWithSentry`.
  29. *
  30. * @param apiHandler The user's original API route handler
  31. * @param parameterizedRoute The route whose handler is being wrapped. Meant for internal use only.
  32. * @returns A wrapped version of the handler
  33. *
  34. * @deprecated Use `wrapApiHandlerWithSentry()` instead
  35. */
  36. function withSentry(apiHandler, parameterizedRoute) {
  37. return new Proxy(apiHandler, {
  38. apply: (
  39. wrappingTarget,
  40. thisArg,
  41. args,
  42. ) => {
  43. const [req, res] = args;
  44. if (!req) {
  45. logger.debug(
  46. `Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`,
  47. );
  48. return wrappingTarget.apply(thisArg, args);
  49. } else if (!res) {
  50. logger.debug(
  51. `Wrapped API handler on route "${parameterizedRoute}" was not passed a response object. Will not instrument.`,
  52. );
  53. return wrappingTarget.apply(thisArg, args);
  54. }
  55. // We're now auto-wrapping API route handlers using `wrapApiHandlerWithSentry` (which uses `withSentry` under the hood), but
  56. // users still may have their routes manually wrapped with `withSentry`. This check makes `sentryWrappedHandler`
  57. // idempotent so that those cases don't break anything.
  58. if (req.__withSentry_applied__) {
  59. return wrappingTarget.apply(thisArg, args);
  60. }
  61. req.__withSentry_applied__ = true;
  62. addTracingExtensions();
  63. return runWithAsyncContext(() => {
  64. return continueTrace(
  65. {
  66. sentryTrace: req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined,
  67. baggage: _optionalChain([req, 'access', _ => _.headers, 'optionalAccess', _2 => _2.baggage]),
  68. },
  69. () => {
  70. // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler)
  71. let reqPath = parameterizedRoute;
  72. // If not, fake it by just replacing parameter values with their names, hoping that none of them match either
  73. // each other or any hard-coded parts of the path
  74. if (!reqPath) {
  75. const url = `${req.url}`;
  76. // pull off query string, if any
  77. reqPath = stripUrlQueryAndFragment(url);
  78. // Replace with placeholder
  79. if (req.query) {
  80. for (const [key, value] of Object.entries(req.query)) {
  81. reqPath = reqPath.replace(`${value}`, `[${key}]`);
  82. }
  83. }
  84. }
  85. const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
  86. getCurrentScope().setSDKProcessingMetadata({ request: req });
  87. return startSpanManual(
  88. {
  89. name: `${reqMethod}${reqPath}`,
  90. op: 'http.server',
  91. attributes: {
  92. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
  93. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
  94. },
  95. metadata: {
  96. request: req,
  97. },
  98. },
  99. async span => {
  100. // eslint-disable-next-line @typescript-eslint/unbound-method
  101. res.end = new Proxy(res.end, {
  102. apply(target, thisArg, argArray) {
  103. if (span) {
  104. setHttpStatus(span, res.statusCode);
  105. span.end();
  106. }
  107. if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) {
  108. target.apply(thisArg, argArray);
  109. } else {
  110. // flushQueue will not reject
  111. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  112. flushQueue().then(() => {
  113. target.apply(thisArg, argArray);
  114. });
  115. }
  116. },
  117. });
  118. try {
  119. const handlerResult = await wrappingTarget.apply(thisArg, args);
  120. if (
  121. process.env.NODE_ENV === 'development' &&
  122. !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR &&
  123. !res.finished
  124. // TODO(v8): Remove this warning?
  125. // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating.
  126. // Warning suppression on Next.JS is only necessary in that case.
  127. ) {
  128. consoleSandbox(() => {
  129. // eslint-disable-next-line no-console
  130. console.warn(
  131. '[sentry] If Next.js logs a warning "API resolved without sending a response", it\'s a false positive, which may happen when you use `withSentry` manually to wrap your routes. To suppress this warning, set `SENTRY_IGNORE_API_RESOLUTION_ERROR` to 1 in your env. To suppress the nextjs warning, use the `externalResolver` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).',
  132. );
  133. });
  134. }
  135. return handlerResult;
  136. } catch (e) {
  137. // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
  138. // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced
  139. // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a
  140. // way to prevent it from actually being reported twice.)
  141. const objectifiedErr = objectify(e);
  142. captureException(objectifiedErr, {
  143. mechanism: {
  144. type: 'instrument',
  145. handled: false,
  146. data: {
  147. wrapped_handler: wrappingTarget.name,
  148. function: 'withSentry',
  149. },
  150. },
  151. });
  152. // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet
  153. // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that
  154. // the transaction was error-free
  155. res.statusCode = 500;
  156. res.statusMessage = 'Internal Server Error';
  157. if (span) {
  158. setHttpStatus(span, res.statusCode);
  159. span.end();
  160. }
  161. // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors
  162. // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the
  163. // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not
  164. // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already
  165. // be finished and the queue will already be empty, so effectively it'll just no-op.)
  166. if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) {
  167. await flushQueue();
  168. }
  169. // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it
  170. // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark
  171. // the error as already having been captured.)
  172. throw objectifiedErr;
  173. }
  174. },
  175. );
  176. },
  177. );
  178. });
  179. },
  180. });
  181. }
  182. export { withSentry, withSentryAPI, wrapApiHandlerWithSentry };
  183. //# sourceMappingURL=wrapApiHandlerWithSentry.js.map