Nuxt/Vue: error 500 when exception captured #72696
Environment
SaaS (https://sentry.io/)
Steps to Reproduce
Dependencies: "@sentry/vue": "8.7.0" "nuxt": "3.11.2" "h3": "1.11.1", ...
This is my Nuxt/Vue config inside plugins/sentry.ts
import * as Sentry from "@sentry/vue";
export default defineNuxtPlugin((nuxtApp) => {
const router = useRouter();
const {
public: { sentry },
} = useRuntimeConfig();
if (!sentry.dsnFront) {
return;
}
Sentry.init({
app: nuxtApp.vueApp,
dsn: sentry.dsnFront,
environment: sentry.environment,
enabled: true,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 0.25,
·
});
});
I run a single vue component
<template>
<button @click="triggerError">Error ?</button>
</template>
<script setup lang="ts">
const triggerError = () => {
throw new Error("Ceci est une erreur!");
};
</script>
Expected Result
It should capture the error and don't crash the app.
Actual Result
The app crashed with a 500 error (even if it happens on front-end side only)
Product Area
Unknown
Link
No response
DSN
No response
Version
No response
Assigning to @getsentry/support for routing ⏲️
Hi @saint-james-fr, could you share a minimal reproduction of this error happening? Thanks!
Hi @AbhiPrasad,
here it is: https://github.com/saint-james-fr/sentry-error
Simple nuxt app created with npx nuxi@latest init
Then I pinned the same versions of packages mentioned in my earlier post.
And i've got the same result:
@saint-james-fr This seems to work as intended. You are not handling the error so it leads to an error state. Sentry should still capture the error, but you have to code some fallback UI for these error states.
I recommend taking a look at Nuxt's error handling docs.
Hi @AbhiPrasad thanks for checking this out,
I'll try this but without Sentry, it just logged an error in the console, it does not crash like this just so you know.
it happened to my project too. when sentry is capturing errors and an error occurs, the application crashes completely. while without sentry the project continue to work normally
My workaround to fix this issue. of course you need to copy the utils from /vendor/components in this repo
export default defineNuxtPlugin((nuxtApp) => {
Sentry.init({ app: nuxtApp.vueApp })
nuxtApp.vueApp.config.errorHandler = undefined
nuxtApp.hook('vue:error', (error, vm, lifecycleHook) => {
const componentName = formatComponentName(vm, false)
const trace = vm ? generateComponentTrace(vm) : ''
const metadata = { componentName, lifecycleHook, trace }
metadata.propsData = vm.$props
setTimeout(() => {
captureException(error, {
captureContext: { contexts: { vue: metadata } },
mechanism: { handled: false },
})
})
})
})
I lack context into how Nuxt works, but I assume you all followed https://www.lichter.io/articles/nuxt3-sentry-recipe as a guide?
We're working on a proper Nuxt SDK here: https://github.com/getsentry/sentry-javascript/issues/9095, that should make this experience a lot better.
maybe @manniL do you have any context about why adding Sentry via defineNuxtPlugin is causing application crashes?
have seen this based on https://github.com/manniL/nuxt3-sentry-recipe/pull/8 but had no time to investigate. Shouldn't happen with v7 though.
Hey, thanks for checking, indeed I followed these steps @AbhiPrasad. I'll try the workaround while waiting for the proper SDK.
Are there any updates on this?
Same here, without Sentry, the error is displayed in the console but without crashing the app.
Same here. This issue prevents us from using the Nuxt module.
I will take a look at this! In the meantime, please take a look at Nuxt Error Handling to capture unhandled errors.
Could Confirm that the error is only happening on client side, and the workaround provided by @falfituri is working !
Here's my setup for nuxt 3 based on https://www.lichter.io/articles/nuxt3-sentry-recipe.
I've built a simple Nuxt module for sentry:
Folder structure (inside ~/modules/sentry)
- index.ts
- runtime
- plugin.server.ts
- plugin.client.ts
- utils.ts
index.ts (Module entry)
import { addPlugin, addServerPlugin, addVitePlugin, createResolver, defineNuxtModule, extendViteConfig, useRuntimeConfig } from "@nuxt/kit"
import { sentryVitePlugin } from "@sentry/vite-plugin"
export default defineNuxtModule({
meta: {
name: "nuxt-sentry",
configKey: "sentry",
},
async setup() {
const resolver = createResolver(import.meta.url)
const config = useRuntimeConfig()
const isProd = import.meta.env.NODE_ENV === "production"
if (!isProd) {
console.warn("Not in Production, Disabling Sentry")
return
}
addPlugin({
src: resolver.resolve("runtime/plugin.client"),
mode: "client",
})
addServerPlugin(resolver.resolve("runtime/plugin.server"))
extendViteConfig((config) => {
config.build ??= {}
config.build.sourcemap = true
})
addVitePlugin(
sentryVitePlugin({
org: config.sentry.organization,
project: config.sentry.project,
authToken: config.sentry.token,
}),
)
},
})
plugin.client.ts (Thanks to @falfituri)
import { browserTracingIntegration, captureException, init, replayIntegration } from "@sentry/vue"
import { formatComponentName, generateComponentTrace } from "./utils"
export default defineNuxtPlugin((nuxtApp) => {
const router = useRouter()
const config = useRuntimeConfig()
if (!config.public.sentry.dsn) {
console.warn("Sentry DSN not set, skipping Sentry initialization")
return
}
init({
app: nuxtApp.vueApp,
dsn: config.public.sentry.dsn,
integrations: [
browserTracingIntegration({ router }),
replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
],
tracesSampleRate: 0.2,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 1.0,
})
nuxtApp.vueApp.config.errorHandler = undefined
nuxtApp.hook("vue:error", (error, vm, lifecycleHook) => {
const componentName = formatComponentName(vm, false)
const trace = vm ? generateComponentTrace(vm) : ""
const metadata = { componentName, lifecycleHook, trace }
// @ts-expect-error Sentry Error
metadata.propsData = vm.$props
setTimeout(() => {
captureException(error, {
captureContext: { contexts: { vue: metadata } },
mechanism: { handled: false },
})
})
})
})
plugin.server.ts:
import { captureException, init } from "@sentry/node"
import { nodeProfilingIntegration } from "@sentry/profiling-node"
export default defineNitroPlugin((nitroApp) => {
const config = useRuntimeConfig()
if (!config.public.sentry.dsn) {
console.warn("Sentry DSN not set, skipping Sentry initialization")
return
}
init({
dsn: config.public.sentry.dsn,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
})
nitroApp.hooks.hook("error", (error) => {
captureException(error)
})
})
utils.ts which is the file that @falfituri referenced.
import type { ComponentPublicInstance } from "vue"
const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string => str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, "")
const ROOT_COMPONENT_NAME = "<Root>"
const ANONYMOUS_COMPONENT_NAME = "<Anonymous>"
function repeat(str: string, n: number): string {
return str.repeat(n)
}
export function formatComponentName(vm: ComponentPublicInstance | null, includeFile?: boolean): string {
if (!vm) {
return ANONYMOUS_COMPONENT_NAME
}
if (vm.$root === vm) {
return ROOT_COMPONENT_NAME
}
if (!vm.$options) {
return ANONYMOUS_COMPONENT_NAME
}
const options = vm.$options
let name = options.name || options._componentTag || options.__name
const file = options.__file
if (!name && file) {
const match = file.match(/([^/\\]+)\.vue$/)
if (match) {
name = match[1]
}
}
return (
(name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : "")
)
}
export function generateComponentTrace(vm: ComponentPublicInstance | null): string {
// @ts-expect-error Sentry Error
if (vm && (vm._isVue || vm.__isVue) && vm.$parent) {
const tree = []
let currentRecursiveSequence = 0
while (vm) {
if (tree.length > 0) {
const last = tree[tree.length - 1] as any
if (last.constructor === vm.constructor) {
currentRecursiveSequence++
vm = vm.$parent
continue
}
else if (currentRecursiveSequence > 0) {
tree[tree.length - 1] = [last, currentRecursiveSequence]
currentRecursiveSequence = 0
}
}
tree.push(vm)
vm = vm.$parent
}
const formattedTree = tree
.map(
(vm, i) =>
`${
(i === 0 ? "---> " : repeat(" ", 5 + i * 2))
+ (Array.isArray(vm)
? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
: formatComponentName(vm))
}`,
)
.join("\n")
return `\n\nfound in\n\n${formattedTree}`
}
return `\n\n(found in ${formatComponentName(vm)})`
}
Could Confirm that the error is only happening on
clientside, and the workaround provided by@falfituri is working ! Here's my setup for
nuxt 3based on https://www.lichter.io/articles/nuxt3-sentry-recipe. I've built a simpleNuxtmodule for sentry:
I used your "index.ts (Module entry)" code, but I got an error of useRuntimeConfig is not defined, to avoid this error, I changed it to get the env variables from process.env.
when I ran it again, I got an error of sentryVitePlugin is not defined (I'm sure I have installed "@sentry/vite-plugin").
@Dylan0916 Are you using the latest version of the Nuxt SDK? (btw it is still in alpha, but we are always happy for feedback)
The runtimeConfig is only available in the client-side Nuxt config. It currently does not work on the server-side due to technical reasons (the file is interpreted at a time where Nuxt cannot be available).
How does your setup look like? Feel free to open an issue regarding sentryVitePlugin in not defined - this should not be the case.
same here
server/sentry.ts
export default defineNitroPlugin((nitroApp) => {
const { public: { sentry } } = useRuntimeConfig()
if (!sentry.dsn) {
console.warn('Sentry DSN not set, skipping Sentry initialization')
return
}
// Initialize Sentry
Sentry.init({
dsn: sentry.dsn,
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0
})
nitroApp.hooks.hook('request', (event) => {
event.context.$sentry = Sentry
})
nitroApp.hooks.hookOnce('close', async () => {
await Sentry.close(2000)
})
nitroApp.hooks.hook('error', (error) => {
Sentry.captureException(error)
})
})
pages/example.vue
if (!data.value) {
throw new Error("Not found");
}
sentry was not capture this error
@suleyman are you using @sentry/nuxt?
@suleyman are you using
@sentry/nuxt?
no, im using @sentry/node for serve-side, @sentry/vue for client-side
@suleyman we have a nuxt SDK in alpha state if you want to give it a try.
Regarding your issue: Have you tried using the workaround provided in this issue?
@s1gr1d @chargome
The cause of this issue lies in the initialization behavior of the client-side code of Nuxt and sentry/vue.
- During Nuxt initialization, an error handler that triggers a 500 error during initialization is registered in
app.config.errorHandler. https://github.com/nuxt/nuxt/blob/d3fdbcaac6cf66d21e25d259390d7824696f1a87/packages/nuxt/src/app/entry.ts#L64-L73 - sentry/vue saves the error handler from step 1, merges it with code to send errors to Sentry, and overwrites
app.config.errorHandler. https://github.com/getsentry/sentry-javascript/blob/216aaeba1ee27cce8a4876e1f9212ba374eb30b3/packages/vue/src/errorhandler.ts#L39-L41
The workaround provided here seems to be functioning to suppress the behavior in step 2.
@konkarin Good debugging! 🔎 This is what I found last week as well :) Nuxt is un-setting their own error handler after the page setup if it wasn't overwritten.
- There is an error handler defined by Nuxt during app startup from
app:createduntilapp:suspense:resolve(handleVueErrorhere) - After the setup, this error handler is either un-set or the user-defined error handler is used.
The Sentry SDK however should not re-use the Nuxt handleVueError handler (as it is right now) or overwrite the users' handler. And this is what I am on right now. A fix is coming in the next days! :)
A PR closing this issue has just been released 🚀
This issue was closed by PR #13748, which was included in the 8.32.0 release.