Skip to content

Commit bd45dfc

Browse files
authored
feat(sveltekit): Introduce client-side handleError wrapper (#7406)
1 parent 2a22f24 commit bd45dfc

File tree

5 files changed

+128
-2
lines changed

5 files changed

+128
-2
lines changed

packages/sveltekit/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
"module": "build/esm/index.server.js",
1414
"browser": "build/esm/index.client.js",
1515
"types": "build/types/index.types.d.ts",
16+
"exports": {
17+
"browser": {
18+
"import": "./build/esm/index.client.js",
19+
"require": "./build/cjs/index.client.js",
20+
"default": "./build/esm/index.client.js"
21+
},
22+
"node": {
23+
"import": "./build/esm/index.server.js",
24+
"require": "./build/cjs/index.server.js",
25+
"default": "./build/esm/index.server.js"
26+
}
27+
},
1628
"publishConfig": {
1729
"access": "public"
1830
},
@@ -28,7 +40,7 @@
2840
"magic-string": "^0.30.0"
2941
},
3042
"devDependencies": {
31-
"@sveltejs/kit": "^1.5.0",
43+
"@sveltejs/kit": "^1.11.0",
3244
"vite": "4.0.0",
3345
"typescript": "^4.9.3"
3446
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { captureException } from '@sentry/svelte';
2+
import { addExceptionMechanism } from '@sentry/utils';
3+
// For now disable the import/no-unresolved rule, because we don't have a way to
4+
// tell eslint that we are only importing types from the @sveltejs/kit package without
5+
// adding a custom resolver, which will take too much time.
6+
// eslint-disable-next-line import/no-unresolved
7+
import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
8+
9+
/**
10+
* Wrapper for the SvelteKit error handler that sends the error to Sentry.
11+
*
12+
* @param handleError The original SvelteKit error handler.
13+
*/
14+
export function wrapHandleError(handleError: HandleClientError): HandleClientError {
15+
return (input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> => {
16+
captureException(input.error, scope => {
17+
scope.addEventProcessor(event => {
18+
addExceptionMechanism(event, {
19+
type: 'sveltekit',
20+
handled: false,
21+
});
22+
return event;
23+
});
24+
return scope;
25+
});
26+
return handleError(input);
27+
};
28+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export * from '@sentry/svelte';
22

33
export { init } from './sdk';
4+
export { wrapHandleError } from './handleError';
5+
6+
// Just here so that eslint is happy until we export more stuff here
7+
export const PLACEHOLDER_CLIENT = 'PLACEHOLDER';
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Scope } from '@sentry/svelte';
2+
// For now disable the import/no-unresolved rule, because we don't have a way to
3+
// tell eslint that we are only importing types from the @sveltejs/kit package without
4+
// adding a custom resolver, which will take too much time.
5+
// eslint-disable-next-line import/no-unresolved
6+
import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
7+
8+
import { wrapHandleError } from '../../src/client/handleError';
9+
10+
const mockCaptureException = jest.fn();
11+
let mockScope = new Scope();
12+
13+
jest.mock('@sentry/svelte', () => {
14+
const original = jest.requireActual('@sentry/core');
15+
return {
16+
...original,
17+
captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
18+
cb(mockScope);
19+
mockCaptureException(err, cb);
20+
return original.captureException(err, cb);
21+
},
22+
};
23+
});
24+
25+
const mockAddExceptionMechanism = jest.fn();
26+
27+
jest.mock('@sentry/utils', () => {
28+
const original = jest.requireActual('@sentry/utils');
29+
return {
30+
...original,
31+
addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
32+
};
33+
});
34+
35+
function handleError(_input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> {
36+
return {
37+
message: 'Whoops!',
38+
};
39+
}
40+
41+
const navigationEvent: NavigationEvent = {
42+
params: {
43+
id: '123',
44+
},
45+
route: {
46+
id: 'users/[id]',
47+
},
48+
url: new URL('http://example.org/users/123'),
49+
};
50+
51+
describe('handleError', () => {
52+
beforeEach(() => {
53+
mockCaptureException.mockClear();
54+
mockAddExceptionMechanism.mockClear();
55+
mockScope = new Scope();
56+
});
57+
58+
it('calls captureException', async () => {
59+
const wrappedHandleError = wrapHandleError(handleError);
60+
const mockError = new Error('test');
61+
const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent });
62+
63+
expect(returnVal!.message).toEqual('Whoops!');
64+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
65+
expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function));
66+
});
67+
68+
it('adds an exception mechanism', async () => {
69+
const addEventProcessorSpy = jest.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
70+
void callback({}, { event_id: 'fake-event-id' });
71+
return mockScope;
72+
});
73+
74+
const wrappedHandleError = wrapHandleError(handleError);
75+
const mockError = new Error('test');
76+
await wrappedHandleError({ error: mockError, event: navigationEvent });
77+
78+
expect(addEventProcessorSpy).toBeCalledTimes(1);
79+
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
80+
expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' });
81+
});
82+
});

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4205,7 +4205,7 @@
42054205
dependencies:
42064206
highlight.js "^9.15.6"
42074207

4208-
"@sveltejs/kit@^1.5.0":
4208+
"@sveltejs/kit@^1.11.0":
42094209
version "1.11.0"
42104210
resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.11.0.tgz#23f233c351e5956356ba6f3206e40637c5f5dbda"
42114211
integrity sha512-PwViZcMoLgEU/jhLoSyjf5hSrHS67wvSm0ifBo4prP9irpGa5HuPOZeVDTL5tPDSBoKxtdYi1zlGdoiJfO86jA==

0 commit comments

Comments
 (0)