Skip to content

Commit 263bb4b

Browse files
committed
feat(sveltekit): Introduce client-side handleError wrapper
1 parent 8b0d536 commit 263bb4b

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { captureException } from '@sentry/svelte';
2+
import { addExceptionMechanism } from '@sentry/utils';
3+
import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
4+
5+
/**
6+
* Wrapper for the SvelteKit error handler that sends the error to Sentry.
7+
*
8+
* @param handleError The original SvelteKit error handler.
9+
*/
10+
export function wrapHandleError(handleError: HandleClientError): HandleClientError {
11+
return (input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> => {
12+
captureException(input.error, scope => {
13+
scope.addEventProcessor(event => {
14+
addExceptionMechanism(event, {
15+
type: 'sveltekit',
16+
handled: false,
17+
});
18+
return event;
19+
});
20+
return scope;
21+
});
22+
return handleError(input);
23+
};
24+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Scope } from '@sentry/svelte';
2+
import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
3+
4+
import { wrapHandleError } from '../../src/client/handleError';
5+
6+
const mockCaptureException = jest.fn();
7+
let mockScope = new Scope();
8+
9+
jest.mock('@sentry/svelte', () => {
10+
const original = jest.requireActual('@sentry/core');
11+
return {
12+
...original,
13+
captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
14+
cb(mockScope);
15+
mockCaptureException(err, cb);
16+
return original.captureException(err, cb);
17+
},
18+
};
19+
});
20+
21+
const mockAddExceptionMechanism = jest.fn();
22+
23+
jest.mock('@sentry/utils', () => {
24+
const original = jest.requireActual('@sentry/utils');
25+
return {
26+
...original,
27+
addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
28+
};
29+
});
30+
31+
function handleError(_input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> {
32+
return {
33+
message: 'Whoops!',
34+
};
35+
}
36+
37+
const navigationEvent: NavigationEvent = {
38+
params: {
39+
id: '123',
40+
},
41+
route: {
42+
id: 'users/[id]',
43+
},
44+
url: new URL('http://example.org/users/123'),
45+
};
46+
47+
describe('handleError', () => {
48+
beforeEach(() => {
49+
mockCaptureException.mockClear();
50+
mockAddExceptionMechanism.mockClear();
51+
mockScope = new Scope();
52+
});
53+
54+
it('calls captureException', async () => {
55+
const wrappedHandleError = wrapHandleError(handleError);
56+
const mockError = new Error('test');
57+
const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent });
58+
59+
expect(returnVal!.message).toEqual('Whoops!');
60+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
61+
expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function));
62+
});
63+
64+
it('adds an exception mechanism', async () => {
65+
const addEventProcessorSpy = jest.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
66+
void callback({}, { event_id: 'fake-event-id' });
67+
return mockScope;
68+
});
69+
70+
const wrappedHandleError = wrapHandleError(handleError);
71+
const mockError = new Error('test');
72+
await wrappedHandleError({ error: mockError, event: navigationEvent });
73+
74+
expect(addEventProcessorSpy).toBeCalledTimes(1);
75+
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
76+
expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' });
77+
});
78+
});

0 commit comments

Comments
 (0)