Skip to content

Commit ed1347e

Browse files
authored
feat(sveltekit): Add server-side handleError wrapper (#7411)
1 parent bd45dfc commit ed1347e

File tree

7 files changed

+139
-6
lines changed

7 files changed

+139
-6
lines changed

packages/sveltekit/src/client/handleError.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
1111
*
1212
* @param handleError The original SvelteKit error handler.
1313
*/
14-
export function wrapHandleError(handleError: HandleClientError): HandleClientError {
14+
export function handleErrorWithSentry(handleError?: HandleClientError): HandleClientError {
1515
return (input: { error: unknown; event: NavigationEvent }): ReturnType<HandleClientError> => {
1616
captureException(input.error, scope => {
1717
scope.addEventProcessor(event => {
@@ -23,6 +23,8 @@ export function wrapHandleError(handleError: HandleClientError): HandleClientErr
2323
});
2424
return scope;
2525
});
26-
return handleError(input);
26+
if (handleError) {
27+
return handleError(input);
28+
}
2729
};
2830
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * from '@sentry/svelte';
22

33
export { init } from './sdk';
4-
export { wrapHandleError } from './handleError';
4+
export { handleErrorWithSentry } from './handleError';
55

66
// Just here so that eslint is happy until we export more stuff here
77
export const PLACEHOLDER_CLIENT = 'PLACEHOLDER';

packages/sveltekit/src/index.types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ export * from './config';
88
export * from './server';
99

1010
import type { Integration, Options, StackParser } from '@sentry/types';
11+
// eslint-disable-next-line import/no-unresolved
12+
import type { HandleClientError, HandleServerError } from '@sveltejs/kit';
1113

1214
import type * as clientSdk from './client';
1315
import type * as serverSdk from './server';
1416

1517
/** Initializes Sentry SvelteKit SDK */
1618
export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void;
1719

20+
export declare function handleErrorWithSentry<T extends HandleClientError | HandleServerError>(
21+
handleError: T,
22+
): ReturnType<T>;
23+
1824
// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
1925
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;
2026

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { captureException } from '@sentry/node';
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 { HandleServerError, RequestEvent } 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 handleErrorWithSentry(handleError?: HandleServerError): HandleServerError {
15+
return (input: { error: unknown; event: RequestEvent }): ReturnType<HandleServerError> => {
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+
if (handleError) {
27+
return handleError(input);
28+
}
29+
};
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from '@sentry/node';
22

33
export { init } from './sdk';
4+
export { handleErrorWithSentry } from './handleError';

packages/sveltekit/test/client/handleError.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Scope } from '@sentry/svelte';
55
// eslint-disable-next-line import/no-unresolved
66
import type { HandleClientError, NavigationEvent } from '@sveltejs/kit';
77

8-
import { wrapHandleError } from '../../src/client/handleError';
8+
import { handleErrorWithSentry } from '../../src/client/handleError';
99

1010
const mockCaptureException = jest.fn();
1111
let mockScope = new Scope();
@@ -55,8 +55,18 @@ describe('handleError', () => {
5555
mockScope = new Scope();
5656
});
5757

58+
it('works when a handleError func is not provided', async () => {
59+
const wrappedHandleError = handleErrorWithSentry();
60+
const mockError = new Error('test');
61+
const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent });
62+
63+
expect(returnVal).not.toBeDefined();
64+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
65+
expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function));
66+
});
67+
5868
it('calls captureException', async () => {
59-
const wrappedHandleError = wrapHandleError(handleError);
69+
const wrappedHandleError = handleErrorWithSentry(handleError);
6070
const mockError = new Error('test');
6171
const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent });
6272

@@ -71,7 +81,7 @@ describe('handleError', () => {
7181
return mockScope;
7282
});
7383

74-
const wrappedHandleError = wrapHandleError(handleError);
84+
const wrappedHandleError = handleErrorWithSentry(handleError);
7585
const mockError = new Error('test');
7686
await wrappedHandleError({ error: mockError, event: navigationEvent });
7787

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Scope } from '@sentry/node';
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 { HandleServerError, RequestEvent } from '@sveltejs/kit';
7+
8+
import { handleErrorWithSentry } from '../../src/server/handleError';
9+
10+
const mockCaptureException = jest.fn();
11+
let mockScope = new Scope();
12+
13+
jest.mock('@sentry/node', () => {
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: RequestEvent }): ReturnType<HandleServerError> {
36+
return {
37+
message: 'Whoops!',
38+
};
39+
}
40+
41+
const requestEvent = {} as RequestEvent;
42+
43+
describe('handleError', () => {
44+
beforeEach(() => {
45+
mockCaptureException.mockClear();
46+
mockAddExceptionMechanism.mockClear();
47+
mockScope = new Scope();
48+
});
49+
50+
it('works when a handleError func is not provided', async () => {
51+
const wrappedHandleError = handleErrorWithSentry();
52+
const mockError = new Error('test');
53+
const returnVal = await wrappedHandleError({ error: mockError, event: requestEvent });
54+
55+
expect(returnVal).not.toBeDefined();
56+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
57+
expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function));
58+
});
59+
60+
it('calls captureException', async () => {
61+
const wrappedHandleError = handleErrorWithSentry(handleError);
62+
const mockError = new Error('test');
63+
const returnVal = await wrappedHandleError({ error: mockError, event: requestEvent });
64+
65+
expect(returnVal!.message).toEqual('Whoops!');
66+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
67+
expect(mockCaptureException).toHaveBeenCalledWith(mockError, expect.any(Function));
68+
});
69+
70+
it('adds an exception mechanism', async () => {
71+
const addEventProcessorSpy = jest.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
72+
void callback({}, { event_id: 'fake-event-id' });
73+
return mockScope;
74+
});
75+
76+
const wrappedHandleError = handleErrorWithSentry(handleError);
77+
const mockError = new Error('test');
78+
await wrappedHandleError({ error: mockError, event: requestEvent });
79+
80+
expect(addEventProcessorSpy).toBeCalledTimes(1);
81+
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
82+
expect(mockAddExceptionMechanism).toBeCalledWith({}, { handled: false, type: 'sveltekit' });
83+
});
84+
});

0 commit comments

Comments
 (0)