Skip to content

Commit d42d04f

Browse files
authored
feat(nuxt): Add enableNitroErrorHandler to server options (#15444)
closes #15409
1 parent eaffd72 commit d42d04f

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Sentry.init({
55
environment: 'qa', // dynamic sampling bias to keep transactions
66
tracesSampleRate: 1.0, // Capture 100% of the transactions
77
tunnel: 'http://localhost:3031/', // proxy server
8+
enableNitroErrorHandler: false, // Error handler is defined in server/plugins/customNitroErrorHandler.ts
89
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Context, GLOBAL_OBJ, dropUndefinedKeys, flush, logger, vercelWaitUntil } from '@sentry/core';
2+
import * as SentryNode from '@sentry/node';
3+
import { H3Error } from 'h3';
4+
import type { CapturedErrorContext } from 'nitropack';
5+
import { defineNitroPlugin } from '#imports';
6+
7+
// Copy from SDK-internal error handler (nuxt/src/runtime/plugins/sentry.server.ts)
8+
export default defineNitroPlugin(nitroApp => {
9+
nitroApp.hooks.hook('error', async (error, errorContext) => {
10+
// Do not handle 404 and 422
11+
if (error instanceof H3Error) {
12+
// Do not report if status code is 3xx or 4xx
13+
if (error.statusCode >= 300 && error.statusCode < 500) {
14+
return;
15+
}
16+
}
17+
18+
const { method, path } = {
19+
method: errorContext.event?._method ? errorContext.event._method : '',
20+
path: errorContext.event?._path ? errorContext.event._path : null,
21+
};
22+
23+
if (path) {
24+
SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`);
25+
}
26+
27+
const structuredContext = extractErrorContext(errorContext);
28+
29+
SentryNode.captureException(error, {
30+
captureContext: { contexts: { nuxt: structuredContext } },
31+
mechanism: { handled: false },
32+
});
33+
34+
await flushIfServerless();
35+
});
36+
});
37+
38+
function extractErrorContext(errorContext: CapturedErrorContext): Context {
39+
const structuredContext: Context = {
40+
method: undefined,
41+
path: undefined,
42+
tags: undefined,
43+
};
44+
45+
if (errorContext) {
46+
if (errorContext.event) {
47+
structuredContext.method = errorContext.event._method || undefined;
48+
structuredContext.path = errorContext.event._path || undefined;
49+
}
50+
51+
if (Array.isArray(errorContext.tags)) {
52+
structuredContext.tags = errorContext.tags || undefined;
53+
}
54+
}
55+
56+
return dropUndefinedKeys(structuredContext);
57+
}
58+
59+
async function flushIfServerless(): Promise<void> {
60+
const isServerless =
61+
!!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions
62+
!!process.env.LAMBDA_TASK_ROOT || // AWS Lambda
63+
!!process.env.VERCEL ||
64+
!!process.env.NETLIFY;
65+
66+
// @ts-expect-error This is not typed
67+
if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) {
68+
vercelWaitUntil(flushWithTimeout());
69+
} else if (isServerless) {
70+
await flushWithTimeout();
71+
}
72+
}
73+
74+
async function flushWithTimeout(): Promise<void> {
75+
const sentryClient = SentryNode.getClient();
76+
const isDebug = sentryClient ? sentryClient.getOptions().debug : false;
77+
78+
try {
79+
isDebug && logger.log('Flushing events...');
80+
await flush(2000);
81+
isDebug && logger.log('Done flushing events');
82+
} catch (e) {
83+
isDebug && logger.log('Error while flushing events:\n', e);
84+
}
85+
}

packages/nuxt/src/common/types.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,20 @@ import type { init as initVue } from '@sentry/vue';
66
// Omitting Vue 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this)
77
// Adding `& object` helps TS with inferring that this is not `undefined` but an object type
88
export type SentryNuxtClientOptions = Omit<Parameters<typeof initVue>[0] & object, 'app'>;
9-
export type SentryNuxtServerOptions = Parameters<typeof initNode>[0] & object;
9+
export type SentryNuxtServerOptions = Parameters<typeof initNode>[0] & {
10+
/**
11+
* Enables the Sentry error handler for the Nitro error hook.
12+
*
13+
* When enabled, exceptions are automatically sent to Sentry with additional data such as the transaction name and Nitro error context.
14+
* It's recommended to keep this enabled unless you need to implement a custom error handler.
15+
*
16+
* If you need a custom implementation, disable this option and refer to the default handler as a reference:
17+
* https://github.com/getsentry/sentry-javascript/blob/da8ba8d77a28b43da5014acc8dd98906d2180cc1/packages/nuxt/src/runtime/plugins/sentry.server.ts#L20-L46
18+
*
19+
* @default true
20+
*/
21+
enableNitroErrorHandler?: boolean;
22+
};
1023

1124
type SourceMapsOptions = {
1225
/**

packages/nuxt/src/runtime/plugins/sentry.server.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {
22
GLOBAL_OBJ,
33
flush,
4-
getClient,
54
getDefaultIsolationScope,
65
getIsolationScope,
76
logger,
87
vercelWaitUntil,
98
withIsolationScope,
109
} from '@sentry/core';
11-
import * as Sentry from '@sentry/node';
10+
import * as SentryNode from '@sentry/node';
1211
import { type EventHandler, H3Error } from 'h3';
1312
import { defineNitroPlugin } from 'nitropack/runtime';
1413
import type { NuxtRenderHTMLContext } from 'nuxt/app';
@@ -18,6 +17,17 @@ export default defineNitroPlugin(nitroApp => {
1817
nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler);
1918

2019
nitroApp.hooks.hook('error', async (error, errorContext) => {
20+
const sentryClient = SentryNode.getClient();
21+
const sentryClientOptions = sentryClient?.getOptions();
22+
23+
if (
24+
sentryClientOptions &&
25+
'enableNitroErrorHandler' in sentryClientOptions &&
26+
sentryClientOptions.enableNitroErrorHandler === false
27+
) {
28+
return;
29+
}
30+
2131
// Do not handle 404 and 422
2232
if (error instanceof H3Error) {
2333
// Do not report if status code is 3xx or 4xx
@@ -32,12 +42,12 @@ export default defineNitroPlugin(nitroApp => {
3242
};
3343

3444
if (path) {
35-
Sentry.getCurrentScope().setTransactionName(`${method} ${path}`);
45+
SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`);
3646
}
3747

3848
const structuredContext = extractErrorContext(errorContext);
3949

40-
Sentry.captureException(error, {
50+
SentryNode.captureException(error, {
4151
captureContext: { contexts: { nuxt: structuredContext } },
4252
mechanism: { handled: false },
4353
});
@@ -67,7 +77,7 @@ async function flushIfServerless(): Promise<void> {
6777
}
6878

6979
async function flushWithTimeout(): Promise<void> {
70-
const sentryClient = getClient();
80+
const sentryClient = SentryNode.getClient();
7181
const isDebug = sentryClient ? sentryClient.getOptions().debug : false;
7282

7383
try {

0 commit comments

Comments
 (0)