diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index fbaa5f98799f..32f310e3bd2d 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -1,6 +1,7 @@ +import { trace } from '@sentry/core'; import { captureException } from '@sentry/svelte'; -import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils'; -import type { ServerLoad } from '@sveltejs/kit'; +import { addExceptionMechanism, objectify } from '@sentry/utils'; +import type { Load } 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 @@ -30,24 +31,24 @@ function sendErrorToSentry(e: unknown): unknown { * * @param origLoad SvelteKit user defined load function */ -export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad { +export function wrapLoadWithSentry(origLoad: Load): Load { 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; + apply: (wrappingTarget, thisArg, args: Parameters) => { + const [event] = args; + + const routeId = event.route.id; + return trace( + { + op: 'function.sveltekit.load', + name: routeId ? routeId : event.url.pathname, + status: 'ok', + metadata: { + source: routeId ? 'route' : 'url', + }, + }, + () => wrappingTarget.apply(thisArg, args), + sendErrorToSentry, + ); }, }); } diff --git a/packages/sveltekit/test/client/load.test.ts b/packages/sveltekit/test/client/load.test.ts index 7cbfd3593c03..f4d18c9f9909 100644 --- a/packages/sveltekit/test/client/load.test.ts +++ b/packages/sveltekit/test/client/load.test.ts @@ -1,5 +1,5 @@ -import { Scope } from '@sentry/svelte'; -import type { ServerLoad } from '@sveltejs/kit'; +import { addTracingExtensions, Scope } from '@sentry/svelte'; +import type { Load } from '@sveltejs/kit'; import { vi } from 'vitest'; import { wrapLoadWithSentry } from '../../src/client/load'; @@ -19,6 +19,19 @@ vi.mock('@sentry/svelte', async () => { }; }); +const mockTrace = vi.fn(); + +vi.mock('@sentry/core', async () => { + const original = (await vi.importActual('@sentry/core')) as any; + return { + ...original, + trace: (...args: unknown[]) => { + mockTrace(...args); + return original.trace(...args); + }, + }; +}); + const mockAddExceptionMechanism = vi.fn(); vi.mock('@sentry/utils', async () => { @@ -33,41 +46,98 @@ function getById(_id?: string) { throw new Error('error'); } +const MOCK_LOAD_ARGS: any = { + params: { id: '123' }, + route: { + id: '/users/[id]', + }, + url: new URL('http://localhost:3000/users/123'), + request: { + headers: { + get: (key: string) => { + if (key === 'sentry-trace') { + return '1234567890abcdef1234567890abcdef-1234567890abcdef-1'; + } + + if (key === 'baggage') { + return ( + 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' + + 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' + + 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1' + ); + } + + return null; + }, + }, + }, +}; + +beforeAll(() => { + addTracingExtensions(); +}); + describe('wrapLoadWithSentry', () => { beforeEach(() => { mockCaptureException.mockClear(); mockAddExceptionMechanism.mockClear(); + mockTrace.mockClear(); mockScope = new Scope(); }); it('calls captureException', async () => { - async function load({ params }: Parameters[0]): Promise> { + async function load({ params }: Parameters[0]): Promise> { return { post: getById(params.id), }; } const wrappedLoad = wrapLoadWithSentry(load); - const res = wrappedLoad({ params: { id: '1' } } as any); + const res = wrappedLoad(MOCK_LOAD_ARGS); await expect(res).rejects.toThrow(); expect(mockCaptureException).toHaveBeenCalledTimes(1); }); + it('calls trace function', async () => { + async function load({ params }: Parameters[0]): Promise> { + return { + post: params.id, + }; + } + + const wrappedLoad = wrapLoadWithSentry(load); + await wrappedLoad(MOCK_LOAD_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.load', + name: '/users/[id]', + status: 'ok', + metadata: { + source: 'route', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); + 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> { + async function load({ params }: Parameters[0]): Promise> { return { post: getById(params.id), }; } const wrappedLoad = wrapLoadWithSentry(load); - const res = wrappedLoad({ params: { id: '1' } } as any); + const res = wrappedLoad(MOCK_LOAD_ARGS); await expect(res).rejects.toThrow(); expect(addEventProcessorSpy).toBeCalledTimes(1);