From defcb520374fa149a7ea3cbf646407d098d84e35 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 14 Mar 2023 19:25:55 +0100 Subject: [PATCH] feat(sveltekit): Add wrapper for client load function (#7447) --- packages/sveltekit/src/client/index.ts | 4 +- packages/sveltekit/src/client/load.ts | 53 ++++++++++++++ packages/sveltekit/src/index.types.ts | 4 +- packages/sveltekit/test/client/load.test.ts | 80 +++++++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 packages/sveltekit/src/client/load.ts create mode 100644 packages/sveltekit/test/client/load.test.ts diff --git a/packages/sveltekit/src/client/index.ts b/packages/sveltekit/src/client/index.ts index dc6ad3407264..f60a353d8b1d 100644 --- a/packages/sveltekit/src/client/index.ts +++ b/packages/sveltekit/src/client/index.ts @@ -2,6 +2,4 @@ export * from '@sentry/svelte'; export { init } from './sdk'; export { handleErrorWithSentry } from './handleError'; - -// Just here so that eslint is happy until we export more stuff here -export const PLACEHOLDER_CLIENT = 'PLACEHOLDER'; +export { wrapLoadWithSentry } from './load'; diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts new file mode 100644 index 000000000000..fbaa5f98799f --- /dev/null +++ b/packages/sveltekit/src/client/load.ts @@ -0,0 +1,53 @@ +import { captureException } from '@sentry/svelte'; +import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils'; +import type { ServerLoad } from '@sveltejs/kit'; + +function sendErrorToSentry(e: unknown): unknown { + // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can + // store a seen flag on it. + const objectifiedErr = objectify(e); + + captureException(objectifiedErr, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'sveltekit', + handled: false, + data: { + function: 'load', + }, + }); + return event; + }); + + return scope; + }); + + return objectifiedErr; +} + +/** + * Wrap load function with Sentry + * + * @param origLoad SvelteKit user defined load function + */ +export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad { + return new Proxy(origLoad, { + apply: (wrappingTarget, thisArg, args: Parameters) => { + let maybePromiseResult; + + try { + maybePromiseResult = wrappingTarget.apply(thisArg, args); + } catch (e) { + throw sendErrorToSentry(e); + } + + if (isThenable(maybePromiseResult)) { + Promise.resolve(maybePromiseResult).then(null, e => { + sendErrorToSentry(e); + }); + } + + return maybePromiseResult; + }, + }); +} diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 06ff806a8377..f332a526019f 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -9,7 +9,7 @@ export * from './server'; import type { Integration, Options, StackParser } from '@sentry/types'; // eslint-disable-next-line import/no-unresolved -import type { HandleClientError, HandleServerError } from '@sveltejs/kit'; +import type { HandleClientError, HandleServerError, ServerLoad } from '@sveltejs/kit'; import type * as clientSdk from './client'; import type * as serverSdk from './server'; @@ -21,6 +21,8 @@ export declare function handleErrorWithSentry; +export declare function wrapLoadWithSentry(origLoad: S): S; + // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts new file mode 100644 index 000000000000..7cbfd3593c03 --- /dev/null +++ b/packages/sveltekit/test/client/load.test.ts @@ -0,0 +1,80 @@ +import { Scope } from '@sentry/svelte'; +import type { ServerLoad } from '@sveltejs/kit'; +import { vi } from 'vitest'; + +import { wrapLoadWithSentry } from '../../src/client/load'; + +const mockCaptureException = vi.fn(); +let mockScope = new Scope(); + +vi.mock('@sentry/svelte', async () => { + const original = (await vi.importActual('@sentry/svelte')) as any; + return { + ...original, + captureException: (err: unknown, cb: (arg0: unknown) => unknown) => { + cb(mockScope); + mockCaptureException(err, cb); + return original.captureException(err, cb); + }, + }; +}); + +const mockAddExceptionMechanism = vi.fn(); + +vi.mock('@sentry/utils', async () => { + const original = (await vi.importActual('@sentry/utils')) as any; + return { + ...original, + addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), + }; +}); + +function getById(_id?: string) { + throw new Error('error'); +} + +describe('wrapLoadWithSentry', () => { + beforeEach(() => { + mockCaptureException.mockClear(); + mockAddExceptionMechanism.mockClear(); + mockScope = new Scope(); + }); + + it('calls captureException', async () => { + async function load({ params }: Parameters[0]): Promise> { + return { + post: getById(params.id), + }; + } + + const wrappedLoad = wrapLoadWithSentry(load); + const res = wrappedLoad({ params: { id: '1' } } as any); + await expect(res).rejects.toThrow(); + + expect(mockCaptureException).toHaveBeenCalledTimes(1); + }); + + it('adds an exception mechanism', async () => { + const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => { + void callback({}, { event_id: 'fake-event-id' }); + return mockScope; + }); + + async function load({ params }: Parameters[0]): Promise> { + return { + post: getById(params.id), + }; + } + + const wrappedLoad = wrapLoadWithSentry(load); + const res = wrappedLoad({ params: { id: '1' } } as any); + await expect(res).rejects.toThrow(); + + expect(addEventProcessorSpy).toBeCalledTimes(1); + expect(mockAddExceptionMechanism).toBeCalledTimes(1); + expect(mockAddExceptionMechanism).toBeCalledWith( + {}, + { handled: false, type: 'sveltekit', data: { function: 'load' } }, + ); + }); +});