Skip to content
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

Nuxt/Vue: error 500 when exception captured #72696 #12515

Open
saint-james-fr opened this issue Jun 13, 2024 · 21 comments
Open

Nuxt/Vue: error 500 when exception captured #72696 #12515

saint-james-fr opened this issue Jun 13, 2024 · 21 comments
Assignees
Labels
Package: vue Issues related to the Sentry Vue SDK Waiting for: Product Owner

Comments

@saint-james-fr
Copy link

saint-james-fr commented Jun 13, 2024

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

@getsantry
Copy link

getsantry bot commented Jun 13, 2024

Assigning to @getsentry/support for routing ⏲️

@dalnoki dalnoki transferred this issue from getsentry/sentry Jun 17, 2024
@AbhiPrasad
Copy link
Member

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

@AbhiPrasad AbhiPrasad added the Package: vue Issues related to the Sentry Vue SDK label Jun 18, 2024
@saint-james-fr
Copy link
Author

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

@AbhiPrasad
Copy link
Member

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

@saint-james-fr
Copy link
Author

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.

@falfituri
Copy link

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
Copy link

falfituri commented Jun 24, 2024

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

@AbhiPrasad
Copy link
Member

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: #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?

@manniL
Copy link

manniL commented Jun 25, 2024

have seen this based on manniL/nuxt3-sentry-recipe#8 but had no time to investigate. Shouldn't happen with v7 though.

@saint-james-fr
Copy link
Author

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

@kochax
Copy link

kochax commented Aug 30, 2024

Are there any updates on this?

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

@Kylyi
Copy link

Kylyi commented Aug 30, 2024

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

@s1gr1d
Copy link
Member

s1gr1d commented Sep 2, 2024

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

@s1gr1d s1gr1d self-assigned this Sep 2, 2024
@Saeid-Za
Copy link

Saeid-Za commented Sep 2, 2024

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

@Dylan0916
Copy link

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").

@s1gr1d
Copy link
Member

s1gr1d commented Sep 9, 2024

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

@suleyman
Copy link

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

@chargome
Copy link
Member

@suleyman are you using @sentry/nuxt?

@suleyman
Copy link

@suleyman are you using @sentry/nuxt?

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

@chargome
Copy link
Member

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

@konkarin
Copy link

@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.
    if (typeof errorHandler === 'function') {
    (errorHandler as UnknownFunc).call(app, error, vm, lifecycleHook);
    }

The workaround provided here seems to be functioning to suppress the behavior in step 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Package: vue Issues related to the Sentry Vue SDK Waiting for: Product Owner
Projects
Status: Waiting for: Product Owner
Development

No branches or pull requests