Skip to content

Commit defcb52

Browse files
authored
feat(sveltekit): Add wrapper for client load function (#7447)
1 parent b1ef00d commit defcb52

File tree

4 files changed

+137
-4
lines changed

4 files changed

+137
-4
lines changed

packages/sveltekit/src/client/index.ts

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

33
export { init } from './sdk';
44
export { handleErrorWithSentry } from './handleError';
5-
6-
// Just here so that eslint is happy until we export more stuff here
7-
export const PLACEHOLDER_CLIENT = 'PLACEHOLDER';
5+
export { wrapLoadWithSentry } from './load';

packages/sveltekit/src/client/load.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { captureException } from '@sentry/svelte';
2+
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
3+
import type { ServerLoad } from '@sveltejs/kit';
4+
5+
function sendErrorToSentry(e: unknown): unknown {
6+
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
7+
// store a seen flag on it.
8+
const objectifiedErr = objectify(e);
9+
10+
captureException(objectifiedErr, scope => {
11+
scope.addEventProcessor(event => {
12+
addExceptionMechanism(event, {
13+
type: 'sveltekit',
14+
handled: false,
15+
data: {
16+
function: 'load',
17+
},
18+
});
19+
return event;
20+
});
21+
22+
return scope;
23+
});
24+
25+
return objectifiedErr;
26+
}
27+
28+
/**
29+
* Wrap load function with Sentry
30+
*
31+
* @param origLoad SvelteKit user defined load function
32+
*/
33+
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
34+
return new Proxy(origLoad, {
35+
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
36+
let maybePromiseResult;
37+
38+
try {
39+
maybePromiseResult = wrappingTarget.apply(thisArg, args);
40+
} catch (e) {
41+
throw sendErrorToSentry(e);
42+
}
43+
44+
if (isThenable(maybePromiseResult)) {
45+
Promise.resolve(maybePromiseResult).then(null, e => {
46+
sendErrorToSentry(e);
47+
});
48+
}
49+
50+
return maybePromiseResult;
51+
},
52+
});
53+
}

packages/sveltekit/src/index.types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export * from './server';
99

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

1414
import type * as clientSdk from './client';
1515
import type * as serverSdk from './server';
@@ -21,6 +21,8 @@ export declare function handleErrorWithSentry<T extends HandleClientError | Hand
2121
handleError: T,
2222
): ReturnType<T>;
2323

24+
export declare function wrapLoadWithSentry<S extends ServerLoad>(origLoad: S): S;
25+
2426
// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
2527
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;
2628

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Scope } from '@sentry/svelte';
2+
import type { ServerLoad } from '@sveltejs/kit';
3+
import { vi } from 'vitest';
4+
5+
import { wrapLoadWithSentry } from '../../src/client/load';
6+
7+
const mockCaptureException = vi.fn();
8+
let mockScope = new Scope();
9+
10+
vi.mock('@sentry/svelte', async () => {
11+
const original = (await vi.importActual('@sentry/svelte')) as any;
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 = vi.fn();
23+
24+
vi.mock('@sentry/utils', async () => {
25+
const original = (await vi.importActual('@sentry/utils')) as any;
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+
describe('wrapLoadWithSentry', () => {
37+
beforeEach(() => {
38+
mockCaptureException.mockClear();
39+
mockAddExceptionMechanism.mockClear();
40+
mockScope = new Scope();
41+
});
42+
43+
it('calls captureException', async () => {
44+
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
45+
return {
46+
post: getById(params.id),
47+
};
48+
}
49+
50+
const wrappedLoad = wrapLoadWithSentry(load);
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 = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
59+
void callback({}, { event_id: 'fake-event-id' });
60+
return mockScope;
61+
});
62+
63+
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
64+
return {
65+
post: getById(params.id),
66+
};
67+
}
68+
69+
const wrappedLoad = wrapLoadWithSentry(load);
70+
const res = wrappedLoad({ params: { id: '1' } } as any);
71+
await expect(res).rejects.toThrow();
72+
73+
expect(addEventProcessorSpy).toBeCalledTimes(1);
74+
expect(mockAddExceptionMechanism).toBeCalledTimes(1);
75+
expect(mockAddExceptionMechanism).toBeCalledWith(
76+
{},
77+
{ handled: false, type: 'sveltekit', data: { function: 'load' } },
78+
);
79+
});
80+
});

0 commit comments

Comments
 (0)