Skip to content

feat(nuxt): Add enableNitroErrorHandler to server options #15444

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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);
}
}
15 changes: 14 additions & 1 deletion packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Parameters<typeof initVue>[0] & object, 'app'>;
export type SentryNuxtServerOptions = Parameters<typeof initNode>[0] & object;
export type SentryNuxtServerOptions = Parameters<typeof initNode>[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 = {
/**
Expand Down
20 changes: 15 additions & 5 deletions packages/nuxt/src/runtime/plugins/sentry.server.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand All @@ -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 },
});
Expand Down Expand Up @@ -67,7 +77,7 @@ async function flushIfServerless(): Promise<void> {
}

async function flushWithTimeout(): Promise<void> {
const sentryClient = getClient();
const sentryClient = SentryNode.getClient();
const isDebug = sentryClient ? sentryClient.getOptions().debug : false;

try {
Expand Down
Loading