From 160520e676683c39e74770a09f0c2593227780b5 Mon Sep 17 00:00:00 2001 From: mketiku <16418952+mketiku@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:11:18 -0400 Subject: [PATCH 1/2] fix(nuxt): Ignore 300-400 status codes on Vue errors in Nuxt --- .../nuxt/src/runtime/plugins/sentry.client.ts | 25 +++++--- .../nuxt/test/runtime/plugins/client.test.ts | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 packages/nuxt/test/runtime/plugins/client.test.ts diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index 92c9478b7079..caa88fe68fa7 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -36,6 +36,16 @@ interface VueRouter { type VueRouteProviderRouter = ReturnType[0]>; +// Do not report Nuxt errors with a 3xx or 4xx status code +function isIgnoredNuxtError(error: unknown): boolean { + if (!isNuxtError(error)) { + return false; + } + // oxlint-disable-next-line typescript/no-deprecated + const statusCode = error?.status || error?.statusCode; + return !!statusCode && statusCode >= 300 && statusCode < 500; +} + // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; @@ -87,19 +97,18 @@ export default defineNuxtPlugin({ }); nuxtApp.hook('app:error', error => { - if (isNuxtError(error)) { - // oxlint-disable-next-line typescript/no-deprecated - const statusCode = error?.status || error?.statusCode; - - // Do not report if status code is 3xx or 4xx - if (statusCode && statusCode >= 300 && statusCode < 500) { - return; - } + if (isIgnoredNuxtError(error)) { + return; } reportNuxtError({ error }); }); + // Nuxt calls `vue:error` for every error that reaches the root component, so an error thrown during setup + // (e.g. `createError({ statusCode: 404 })`) arrives here too, not only in `app:error`. nuxtApp.hook('vue:error', (error, instance, info) => { + if (isIgnoredNuxtError(error)) { + return; + } reportNuxtError({ error, instance, info }); }); }, diff --git a/packages/nuxt/test/runtime/plugins/client.test.ts b/packages/nuxt/test/runtime/plugins/client.test.ts new file mode 100644 index 000000000000..3134aa5bb43b --- /dev/null +++ b/packages/nuxt/test/runtime/plugins/client.test.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { reportNuxtError } from '../../../src/runtime/utils'; + +vi.mock('nuxt/app', () => ({ + defineNuxtPlugin: (plugin: unknown) => plugin, + isNuxtError: (error: unknown) => !!error && typeof error === 'object' && '__nuxt_error' in error, +})); + +vi.mock('../../../src/runtime/utils', () => ({ + reportNuxtError: vi.fn(), +})); + +type Hook = (...args: unknown[]) => void; + +async function setupHooks(): Promise> { + const { default: plugin } = await import('../../../src/runtime/plugins/sentry.client'); + const hooks: Record = {}; + const nuxtApp = { hook: (name: string, hook: Hook) => (hooks[name] = hook) }; + await (plugin as unknown as { setup: (app: unknown) => Promise }).setup(nuxtApp); + return hooks; +} + +function nuxtError(statusCode: number): Error { + return Object.assign(new Error(`status ${statusCode}`), { __nuxt_error: true, statusCode }); +} + +describe('sentry client plugin error hooks', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each(['app:error', 'vue:error'])('does not report 3xx/4xx Nuxt errors from %s', async hookName => { + const hooks = await setupHooks(); + + hooks[hookName]?.(nuxtError(404)); + hooks[hookName]?.(nuxtError(301)); + + expect(reportNuxtError).not.toHaveBeenCalled(); + }); + + it.each(['app:error', 'vue:error'])('reports 5xx Nuxt errors from %s', async hookName => { + const hooks = await setupHooks(); + + hooks[hookName]?.(nuxtError(503)); + + expect(reportNuxtError).toHaveBeenCalledTimes(1); + }); + + it('reports errors that are not Nuxt errors from vue:error', async () => { + const hooks = await setupHooks(); + const error = Object.assign(new Error('boom'), { statusCode: 404 }); + + hooks['vue:error']?.(error, undefined, 'setup function'); + + expect(reportNuxtError).toHaveBeenCalledWith({ error, instance: undefined, info: 'setup function' }); + }); +}); From 1b3602cc17b5206a12740985a2003e63e1053d2f Mon Sep 17 00:00:00 2001 From: mketiku <16418952+mketiku@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:47:18 -0400 Subject: [PATCH 2/2] test(nuxt): Assert the error hooks are registered; explain why 3xx/4xx are skipped --- .../nuxt/src/runtime/plugins/sentry.client.ts | 2 +- .../nuxt/test/runtime/plugins/client.test.ts | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index caa88fe68fa7..423e8c457492 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -36,7 +36,7 @@ interface VueRouter { type VueRouteProviderRouter = ReturnType[0]>; -// Do not report Nuxt errors with a 3xx or 4xx status code +// 3xx/4xx Nuxt errors are expected control flow (redirects, not-found pages), not bugs. The server skips them too. function isIgnoredNuxtError(error: unknown): boolean { if (!isNuxtError(error)) { return false; diff --git a/packages/nuxt/test/runtime/plugins/client.test.ts b/packages/nuxt/test/runtime/plugins/client.test.ts index 3134aa5bb43b..14b75383d1cb 100644 --- a/packages/nuxt/test/runtime/plugins/client.test.ts +++ b/packages/nuxt/test/runtime/plugins/client.test.ts @@ -12,12 +12,13 @@ vi.mock('../../../src/runtime/utils', () => ({ type Hook = (...args: unknown[]) => void; -async function setupHooks(): Promise> { +async function setupHook(hookName: string): Promise { const { default: plugin } = await import('../../../src/runtime/plugins/sentry.client'); const hooks: Record = {}; const nuxtApp = { hook: (name: string, hook: Hook) => (hooks[name] = hook) }; await (plugin as unknown as { setup: (app: unknown) => Promise }).setup(nuxtApp); - return hooks; + expect(hooks).toHaveProperty(hookName); + return hooks[hookName]!; } function nuxtError(statusCode: number): Error { @@ -30,27 +31,27 @@ describe('sentry client plugin error hooks', () => { }); it.each(['app:error', 'vue:error'])('does not report 3xx/4xx Nuxt errors from %s', async hookName => { - const hooks = await setupHooks(); + const hook = await setupHook(hookName); - hooks[hookName]?.(nuxtError(404)); - hooks[hookName]?.(nuxtError(301)); + hook(nuxtError(404)); + hook(nuxtError(301)); expect(reportNuxtError).not.toHaveBeenCalled(); }); it.each(['app:error', 'vue:error'])('reports 5xx Nuxt errors from %s', async hookName => { - const hooks = await setupHooks(); + const hook = await setupHook(hookName); - hooks[hookName]?.(nuxtError(503)); + hook(nuxtError(503)); expect(reportNuxtError).toHaveBeenCalledTimes(1); }); it('reports errors that are not Nuxt errors from vue:error', async () => { - const hooks = await setupHooks(); + const hook = await setupHook('vue:error'); const error = Object.assign(new Error('boom'), { statusCode: 404 }); - hooks['vue:error']?.(error, undefined, 'setup function'); + hook(error, undefined, 'setup function'); expect(reportNuxtError).toHaveBeenCalledWith({ error, instance: undefined, info: 'setup function' }); });