Skip to content

Commit b2db998

Browse files
committed
feat(sveltekit): Add wrapper for server load function
1 parent ed1347e commit b2db998

File tree

6 files changed

+118
-3
lines changed

6 files changed

+118
-3
lines changed

packages/sveltekit/src/client/handleError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function handleErrorWithSentry(handleError?: HandleClientError): HandleCl
1616
captureException(input.error, scope => {
1717
scope.addEventProcessor(event => {
1818
addExceptionMechanism(event, {
19-
type: 'sveltekit',
19+
type: 'instrument',
2020
handled: false,
2121
});
2222
return event;

packages/sveltekit/src/server/handleError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function handleErrorWithSentry(handleError?: HandleServerError): HandleSe
1616
captureException(input.error, scope => {
1717
scope.addEventProcessor(event => {
1818
addExceptionMechanism(event, {
19-
type: 'sveltekit',
19+
type: 'instrument',
2020
handled: false,
2121
});
2222
return event;

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from '@sentry/node';
22

33
export { init } from './sdk';
44
export { handleErrorWithSentry } from './handleError';
5+
export { wrapLoadWithSentry } from './load';

packages/sveltekit/src/server/load.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { captureException } from '@sentry/node';
2+
import { addExceptionMechanism, objectify } from '@sentry/utils';
3+
// eslint-disable-next-line import/no-unresolved
4+
import type { ServerLoad } from '@sveltejs/kit';
5+
6+
/**
7+
* Wrap load function with Sentry
8+
*
9+
* @param origLoad SvelteKit user defined load function
10+
*/
11+
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
12+
return new Proxy(origLoad, {
13+
apply: async (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
14+
try {
15+
return await wrappingTarget.apply(thisArg, args);
16+
} catch (e) {
17+
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
18+
// store a seen flag on it.
19+
const objectifiedErr = objectify(e);
20+
21+
captureException(objectifiedErr, scope => {
22+
scope.addEventProcessor(event => {
23+
addExceptionMechanism(event, {
24+
type: 'instrument',
25+
handled: false,
26+
data: {
27+
function: 'load',
28+
},
29+
});
30+
return event;
31+
});
32+
33+
return scope;
34+
});
35+
36+
throw objectifiedErr;
37+
}
38+
},
39+
});
40+
}

packages/sveltekit/test/server/handleError.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const mockCaptureException = jest.fn();
1111
let mockScope = new Scope();
1212

1313
jest.mock('@sentry/node', () => {
14-
const original = jest.requireActual('@sentry/core');
14+
const original = jest.requireActual('@sentry/node');
1515
return {
1616
...original,
1717
captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Scope } from '@sentry/node';
2+
// eslint-disable-next-line import/no-unresolved
3+
import type { ServerLoad } from '@sveltejs/kit';
4+
5+
import { wrapLoadWithSentry } from '../../src/server/load';
6+
7+
const mockCaptureException = jest.fn();
8+
let mockScope = new Scope();
9+
10+
jest.mock('@sentry/node', () => {
11+
const original = jest.requireActual('@sentry/node');
12+
return {
13+
...original,
14+
captureException: (err: unknown, cb: (arg0: unknown) => unknown) => {
15+
cb(mockScope);
16+
mockCaptureException(err, cb);
17+
return original.captureException(err, cb);
18+
},
19+
};
20+
});
21+
22+
const mockAddExceptionMechanism = jest.fn();
23+
24+
jest.mock('@sentry/utils', () => {
25+
const original = jest.requireActual('@sentry/utils');
26+
return {
27+
...original,
28+
addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
29+
};
30+
});
31+
32+
function getById(_id?: string) {
33+
throw new Error('error');
34+
}
35+
36+
async function erroringLoad({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
37+
return {
38+
post: getById(params.id),
39+
};
40+
}
41+
42+
describe('wrapLoadWithSentry', () => {
43+
beforeEach(() => {
44+
mockCaptureException.mockClear();
45+
mockAddExceptionMechanism.mockClear();
46+
mockScope = new Scope();
47+
});
48+
49+
it('calls captureException', async () => {
50+
const wrappedLoad = wrapLoadWithSentry(erroringLoad);
51+
const res = wrappedLoad({ params: { id: '1' } } as any);
52+
await expect(res).rejects.toThrow();
53+
54+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
55+
});
56+
57+
it('adds an exception mechanism', async () => {
58+
const addEventProcessorSpy = jest.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
59+
void callback({}, { event_id: 'fake-event-id' });
60+
return mockScope;
61+
});
62+
63+
const wrappedLoad = wrapLoadWithSentry(erroringLoad);
64+
const res = wrappedLoad({ params: { id: '1' } } as any);
65+
await expect(res).rejects.toThrow();
66+
67+
expect(addEventProcessorSpy).toBeCalledTimes(1);
68+
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
69+
expect(mockAddExceptionMechanism).toBeCalledWith(
70+
{},
71+
{ handled: false, type: 'instrument', data: { function: 'load' } },
72+
);
73+
});
74+
});

0 commit comments

Comments
 (0)