sentry-javascript icon indicating copy to clipboard operation
sentry-javascript copied to clipboard

Nuxt/Vue: error 500 when exception captured #72696

Open saint-james-fr opened this issue 1 year ago • 9 comments

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)

image

Product Area

Unknown

Link

No response

DSN

No response

Version

No response

saint-james-fr avatar Jun 13 '24 16:06 saint-james-fr

Assigning to @getsentry/support for routing ⏲️

getsantry[bot] avatar Jun 13 '24 16:06 getsantry[bot]

Hi @saint-james-fr, could you share a minimal reproduction of this error happening? Thanks!

AbhiPrasad avatar Jun 18 '24 19:06 AbhiPrasad

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:

image

saint-james-fr avatar Jun 19 '24 11:06 saint-james-fr

@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.

AbhiPrasad avatar Jun 20 '24 17:06 AbhiPrasad

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.

saint-james-fr avatar Jun 23 '24 18:06 saint-james-fr

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

falfituri avatar Jun 24 '24 08:06 falfituri

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 },
      })
    })
  })
})

falfituri avatar Jun 24 '24 10:06 falfituri

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?

AbhiPrasad avatar Jun 24 '24 14:06 AbhiPrasad

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.

TheAlexLichter avatar Jun 25 '24 14:06 TheAlexLichter

Hey, thanks for checking, indeed I followed these steps @AbhiPrasad. I'll try the workaround while waiting for the proper SDK.

saint-james-fr avatar Jul 01 '24 11:07 saint-james-fr

Are there any updates on this?

Same here, without Sentry, the error is displayed in the console but without crashing the app.

kochax avatar Aug 30 '24 10:08 kochax

Same here. This issue prevents us from using the Nuxt module.

Kylyi avatar Aug 30 '24 10:08 Kylyi

I will take a look at this! In the meantime, please take a look at Nuxt Error Handling to capture unhandled errors.

s1gr1d avatar Sep 02 '24 10:09 s1gr1d

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)})`
}

Saeid-Za avatar Sep 02 '24 13:09 Saeid-Za

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:

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 avatar Sep 08 '24 12:09 Dylan0916

@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.

s1gr1d avatar Sep 09 '24 07:09 s1gr1d

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 avatar Sep 10 '24 12:09 suleyman

@suleyman are you using @sentry/nuxt?

chargome avatar Sep 11 '24 07:09 chargome

@suleyman are you using @sentry/nuxt?

no, im using @sentry/node for serve-side, @sentry/vue for client-side

suleyman avatar Sep 11 '24 13:09 suleyman

@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?

chargome avatar Sep 12 '24 12:09 chargome

@s1gr1d @chargome

The cause of this issue lies in the initialization behavior of the client-side code of Nuxt and sentry/vue.

  1. 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
  2. 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 avatar Sep 22 '24 07:09 konkarin

@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.

  1. There is an error handler defined by Nuxt during app startup from app:created until app:suspense:resolve (handleVueError here)
  2. 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! :)

s1gr1d avatar Sep 23 '24 07:09 s1gr1d

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.

github-actions[bot] avatar Sep 26 '24 09:09 github-actions[bot]