diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index f65603565285..a75e605fdd58 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -13,6 +13,18 @@ "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", "types": "build/types/index.types.d.ts", + "exports": { + "browser": { + "import": "./build/esm/index.client.js", + "require": "./build/cjs/index.client.js", + "default": "./build/esm/index.client.js" + }, + "node": { + "import": "./build/esm/index.server.js", + "require": "./build/cjs/index.server.js", + "default": "./build/esm/index.server.js" + } + }, "publishConfig": { "access": "public" }, @@ -28,7 +40,7 @@ "magic-string": "^0.30.0" }, "devDependencies": { - "@sveltejs/kit": "^1.5.0", + "@sveltejs/kit": "^1.11.0", "vite": "4.0.0", "typescript": "^4.9.3" }, diff --git a/packages/sveltekit/src/client/handleError.ts b/packages/sveltekit/src/client/handleError.ts new file mode 100644 index 000000000000..a4c20b895537 --- /dev/null +++ b/packages/sveltekit/src/client/handleError.ts @@ -0,0 +1,28 @@ +import { captureException } from '@sentry/svelte'; +import { addExceptionMechanism } from '@sentry/utils'; +// For now disable the import/no-unresolved rule, because we don't have a way to +// tell eslint that we are only importing types from the @sveltejs/kit package without +// adding a custom resolver, which will take too much time. +// eslint-disable-next-line import/no-unresolved +import type { HandleClientError, NavigationEvent } from '@sveltejs/kit'; + +/** + * Wrapper for the SvelteKit error handler that sends the error to Sentry. + * + * @param handleError The original SvelteKit error handler. + */ +export function wrapHandleError(handleError: HandleClientError): HandleClientError { + return (input: { error: unknown; event: NavigationEvent }): ReturnType => { + captureException(input.error, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'sveltekit', + handled: false, + }); + return event; + }); + return scope; + }); + return handleError(input); + }; +} diff --git a/packages/sveltekit/src/client/index.ts b/packages/sveltekit/src/client/index.ts index 92f3f38bcd88..e49e83c9ce96 100644 --- a/packages/sveltekit/src/client/index.ts +++ b/packages/sveltekit/src/client/index.ts @@ -1,3 +1,7 @@ export * from '@sentry/svelte'; export { init } from './sdk'; +export { wrapHandleError } from './handleError'; + +// Just here so that eslint is happy until we export more stuff here +export const PLACEHOLDER_CLIENT = 'PLACEHOLDER'; diff --git a/packages/sveltekit/test/client/handleError.test.ts b/packages/sveltekit/test/client/handleError.test.ts new file mode 100644 index 000000000000..a075ac611c1e --- /dev/null +++ b/packages/sveltekit/test/client/handleError.test.ts @@ -0,0 +1,82 @@ +import { Scope } from '@sentry/svelte'; +// For now disable the import/no-unresolved rule, because we don't have a way to +// tell eslint that we are only importing types from the @sveltejs/kit package without +// adding a custom resolver, which will take too much time. +// eslint-disable-next-line import/no-unresolved +import type { HandleClientError, NavigationEvent } from '@sveltejs/kit'; + +import { wrapHandleError } from '../../src/client/handleError'; + +const mockCaptureException = jest.fn(); +let mockScope = new Scope(); + +jest.mock('@sentry/svelte', () => { + const original = jest.requireActual('@sentry/core'); + return { + ...original, + captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { + cb(mockScope); + mockCaptureException(err, cb); + return original.captureException(err, cb); + }, + }; +}); + +const mockAddExceptionMechanism = jest.fn(); + +jest.mock('@sentry/utils', () => { + const original = jest.requireActual('@sentry/utils'); + return { + ...original, + addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), + }; +}); + +function handleError(_input: { error: unknown; event: NavigationEvent }): ReturnType { + return { + message: 'Whoops!', + }; +} + +const navigationEvent: NavigationEvent = { + params: { + id: '123', + }, + route: { + id: 'users/[id]', + }, + url: new URL('http://example.org/users/123'), +}; + +describe('handleError', () => { + beforeEach(() => { + mockCaptureException.mockClear(); + mockAddExceptionMechanism.mockClear(); + mockScope = new Scope(); + }); + + it('calls captureException', async () => { + const wrappedHandleError = wrapHandleError(handleError); + const mockError = new Error('test'); + const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent }); + + expect(returnVal!.message).toEqual('Whoops!'); + expect(mockCaptureException).toHaveBeenCalledTimes(1); + expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function)); + }); + + it('adds an exception mechanism', async () => { + const addEventProcessorSpy = jest.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { + void callback({}, { event_id: 'fake-event-id' }); + return mockScope; + }); + + const wrappedHandleError = wrapHandleError(handleError); + const mockError = new Error('test'); + await wrappedHandleError({ error: mockError, event: navigationEvent }); + + expect(addEventProcessorSpy).toBeCalledTimes(1); + expect(mockAddExceptionMechanism).toBeCalledTimes(1); + expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 803d62922290..3502d7f6c2ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4205,7 +4205,7 @@ dependencies: highlight.js "^9.15.6" -"@sveltejs/kit@^1.5.0": +"@sveltejs/kit@^1.11.0": version "1.11.0" resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.11.0.tgz#23f233c351e5956356ba6f3206e40637c5f5dbda" integrity sha512-PwViZcMoLgEU/jhLoSyjf5hSrHS67wvSm0ifBo4prP9irpGa5HuPOZeVDTL5tPDSBoKxtdYi1zlGdoiJfO86jA==