Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions packages/nuxt/src/runtime/plugins/sentry.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ interface VueRouter {

type VueRouteProviderRouter = ReturnType<Parameters<typeof _INTERNAL_createVueRouteProvider>[0]>;

// 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;
}
// 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;

Expand Down Expand Up @@ -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 });
});
},
Expand Down
58 changes: 58 additions & 0 deletions packages/nuxt/test/runtime/plugins/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 setupHook(hookName: string): Promise<Hook> {
const { default: plugin } = await import('../../../src/runtime/plugins/sentry.client');
const hooks: Record<string, Hook> = {};
const nuxtApp = { hook: (name: string, hook: Hook) => (hooks[name] = hook) };
await (plugin as unknown as { setup: (app: unknown) => Promise<void> }).setup(nuxtApp);
expect(hooks).toHaveProperty(hookName);
return hooks[hookName]!;
}

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 hook = await setupHook(hookName);

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 hook = await setupHook(hookName);

hook(nuxtError(503));

expect(reportNuxtError).toHaveBeenCalledTimes(1);
});

it('reports errors that are not Nuxt errors from vue:error', async () => {
const hook = await setupHook('vue:error');
const error = Object.assign(new Error('boom'), { statusCode: 404 });

hook(error, undefined, 'setup function');

expect(reportNuxtError).toHaveBeenCalledWith({ error, instance: undefined, info: 'setup function' });
});
});
Loading