wrapApiHandlerWithSentry.js 9.4 KB

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