diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts index 729b2296c683..e04331934f99 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts @@ -5,4 +5,5 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions tunnel: 'http://localhost:3031/', // proxy server + enableNitroErrorHandler: false, // Error handler is defined in server/plugins/customNitroErrorHandler.ts }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts new file mode 100644 index 000000000000..2d9258936169 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts @@ -0,0 +1,85 @@ +import { Context, GLOBAL_OBJ, dropUndefinedKeys, flush, logger, vercelWaitUntil } from '@sentry/core'; +import * as SentryNode from '@sentry/node'; +import { H3Error } from 'h3'; +import type { CapturedErrorContext } from 'nitropack'; +import { defineNitroPlugin } from '#imports'; + +// Copy from SDK-internal error handler (nuxt/src/runtime/plugins/sentry.server.ts) +export default defineNitroPlugin(nitroApp => { + nitroApp.hooks.hook('error', async (error, errorContext) => { + // Do not handle 404 and 422 + if (error instanceof H3Error) { + // Do not report if status code is 3xx or 4xx + if (error.statusCode >= 300 && error.statusCode < 500) { + return; + } + } + + const { method, path } = { + method: errorContext.event?._method ? errorContext.event._method : '', + path: errorContext.event?._path ? errorContext.event._path : null, + }; + + if (path) { + SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); + } + + const structuredContext = extractErrorContext(errorContext); + + SentryNode.captureException(error, { + captureContext: { contexts: { nuxt: structuredContext } }, + mechanism: { handled: false }, + }); + + await flushIfServerless(); + }); +}); + +function extractErrorContext(errorContext: CapturedErrorContext): Context { + const structuredContext: Context = { + method: undefined, + path: undefined, + tags: undefined, + }; + + if (errorContext) { + if (errorContext.event) { + structuredContext.method = errorContext.event._method || undefined; + structuredContext.path = errorContext.event._path || undefined; + } + + if (Array.isArray(errorContext.tags)) { + structuredContext.tags = errorContext.tags || undefined; + } + } + + return dropUndefinedKeys(structuredContext); +} + +async function flushIfServerless(): Promise { + const isServerless = + !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions + !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda + !!process.env.VERCEL || + !!process.env.NETLIFY; + + // @ts-expect-error This is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} + +async function flushWithTimeout(): Promise { + const sentryClient = SentryNode.getClient(); + const isDebug = sentryClient ? sentryClient.getOptions().debug : false; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 02e0b53bac7c..599b564f62a2 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -6,7 +6,20 @@ import type { init as initVue } from '@sentry/vue'; // Omitting Vue 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) // Adding `& object` helps TS with inferring that this is not `undefined` but an object type export type SentryNuxtClientOptions = Omit[0] & object, 'app'>; -export type SentryNuxtServerOptions = Parameters[0] & object; +export type SentryNuxtServerOptions = Parameters[0] & { + /** + * Enables the Sentry error handler for the Nitro error hook. + * + * When enabled, exceptions are automatically sent to Sentry with additional data such as the transaction name and Nitro error context. + * It's recommended to keep this enabled unless you need to implement a custom error handler. + * + * If you need a custom implementation, disable this option and refer to the default handler as a reference: + * https://github.com/getsentry/sentry-javascript/blob/da8ba8d77a28b43da5014acc8dd98906d2180cc1/packages/nuxt/src/runtime/plugins/sentry.server.ts#L20-L46 + * + * @default true + */ + enableNitroErrorHandler?: boolean; +}; type SourceMapsOptions = { /** diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 5d828775b62f..f65ac64b9982 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,14 +1,13 @@ import { GLOBAL_OBJ, flush, - getClient, getDefaultIsolationScope, getIsolationScope, logger, vercelWaitUntil, withIsolationScope, } from '@sentry/core'; -import * as Sentry from '@sentry/node'; +import * as SentryNode from '@sentry/node'; import { type EventHandler, H3Error } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -18,6 +17,17 @@ export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); nitroApp.hooks.hook('error', async (error, errorContext) => { + const sentryClient = SentryNode.getClient(); + const sentryClientOptions = sentryClient?.getOptions(); + + if ( + sentryClientOptions && + 'enableNitroErrorHandler' in sentryClientOptions && + sentryClientOptions.enableNitroErrorHandler === false + ) { + return; + } + // Do not handle 404 and 422 if (error instanceof H3Error) { // Do not report if status code is 3xx or 4xx @@ -32,12 +42,12 @@ export default defineNitroPlugin(nitroApp => { }; if (path) { - Sentry.getCurrentScope().setTransactionName(`${method} ${path}`); + SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); } const structuredContext = extractErrorContext(errorContext); - Sentry.captureException(error, { + SentryNode.captureException(error, { captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); @@ -67,7 +77,7 @@ async function flushIfServerless(): Promise { } async function flushWithTimeout(): Promise { - const sentryClient = getClient(); + const sentryClient = SentryNode.getClient(); const isDebug = sentryClient ? sentryClient.getOptions().debug : false; try {