diff --git a/frontend/src/hooks.server.test.ts b/frontend/src/hooks.server.test.ts new file mode 100644 index 00000000..b24f2e16 --- /dev/null +++ b/frontend/src/hooks.server.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('@sentry/sveltekit', () => ({ + init: vi.fn(), + handleErrorWithSentry: (fn: (args: unknown) => unknown) => fn, + lastEventId: vi.fn(() => 'sentry-event-id-abc123') +})); + +vi.mock('@sveltejs/kit', () => ({ redirect: vi.fn() })); +vi.mock('@sveltejs/kit/hooks', () => ({ sequence: vi.fn((...fns: unknown[]) => fns[0]) })); +vi.mock('$lib/paraglide/server', () => ({ paraglideMiddleware: vi.fn() })); +vi.mock('$lib/paraglide/runtime', () => ({ cookieName: 'locale', cookieMaxAge: 86400 })); +vi.mock('$lib/shared/server/locale', () => ({ detectLocale: vi.fn(() => 'de') })); + +const makeEvent = () => ({ + url: { pathname: '/documents/123' }, + locals: {} +}); + +describe('hooks.server handleError', () => { + beforeEach(() => { + vi.resetModules(); + }); + + it('returns Sentry lastEventId as errorId', async () => { + const Sentry = await import('@sentry/sveltekit'); + vi.mocked(Sentry.lastEventId).mockReturnValue('sentry-event-id-abc123'); + + const { handleError } = await import('./hooks.server'); + const result = (handleError as (args: unknown) => { message: string; errorId: string })({ + error: new Error('boom'), + event: makeEvent(), + status: 500, + message: 'Internal Error' + }); + + expect(result.errorId).toBe('sentry-event-id-abc123'); + expect(result.message).toBe('An unexpected error occurred'); + }); + + it('falls back to crypto.randomUUID when lastEventId returns undefined', async () => { + const Sentry = await import('@sentry/sveltekit'); + vi.mocked(Sentry.lastEventId).mockReturnValue(undefined); + + const { handleError } = await import('./hooks.server'); + const result = (handleError as (args: unknown) => { message: string; errorId: string })({ + error: new Error('boom'), + event: makeEvent(), + status: 500, + message: 'Internal Error' + }); + + expect(result.errorId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ); + expect(result.message).toBe('An unexpected error occurred'); + }); +}); diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 4baaa592..710e269a 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -10,6 +10,7 @@ Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: import.meta.env.MODE, tracesSampleRate: 1.0, + sendDefaultPii: false, enabled: !!import.meta.env.VITE_SENTRY_DSN }); @@ -122,4 +123,7 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => { export const handle = sequence(userGroup, handleAuth, handleLocaleDetection, handleParaglide); -export const handleError = Sentry.handleErrorWithSentry(); +export const handleError = Sentry.handleErrorWithSentry(() => { + const errorId = Sentry.lastEventId() ?? crypto.randomUUID(); + return { message: 'An unexpected error occurred', errorId }; +});