Skip to content

Commit 09ee30b

Browse files
authored
feat(sveltekit): Add performance monitoring for client load (#7537)
1 parent 046c0c2 commit 09ee30b

File tree

2 files changed

+96
-25
lines changed

2 files changed

+96
-25
lines changed

packages/sveltekit/src/client/load.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { trace } from '@sentry/core';
12
import { captureException } from '@sentry/svelte';
2-
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
3-
import type { ServerLoad } from '@sveltejs/kit';
3+
import { addExceptionMechanism, objectify } from '@sentry/utils';
4+
import type { Load } from '@sveltejs/kit';
45

56
function sendErrorToSentry(e: unknown): unknown {
67
// 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 {
3031
*
3132
* @param origLoad SvelteKit user defined load function
3233
*/
33-
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
34+
export function wrapLoadWithSentry(origLoad: Load): Load {
3435
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;
36+
apply: (wrappingTarget, thisArg, args: Parameters<Load>) => {
37+
const [event] = args;
38+
39+
const routeId = event.route.id;
40+
return trace(
41+
{
42+
op: 'function.sveltekit.load',
43+
name: routeId ? routeId : event.url.pathname,
44+
status: 'ok',
45+
metadata: {
46+
source: routeId ? 'route' : 'url',
47+
},
48+
},
49+
() => wrappingTarget.apply(thisArg, args),
50+
sendErrorToSentry,
51+
);
5152
},
5253
});
5354
}

packages/sveltekit/test/client/load.test.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Scope } from '@sentry/svelte';
2-
import type { ServerLoad } from '@sveltejs/kit';
1+
import { addTracingExtensions, Scope } from '@sentry/svelte';
2+
import type { Load } from '@sveltejs/kit';
33
import { vi } from 'vitest';
44

55
import { wrapLoadWithSentry } from '../../src/client/load';
@@ -19,6 +19,19 @@ vi.mock('@sentry/svelte', async () => {
1919
};
2020
});
2121

22+
const mockTrace = vi.fn();
23+
24+
vi.mock('@sentry/core', async () => {
25+
const original = (await vi.importActual('@sentry/core')) as any;
26+
return {
27+
...original,
28+
trace: (...args: unknown[]) => {
29+
mockTrace(...args);
30+
return original.trace(...args);
31+
},
32+
};
33+
});
34+
2235
const mockAddExceptionMechanism = vi.fn();
2336

2437
vi.mock('@sentry/utils', async () => {
@@ -33,41 +46,98 @@ function getById(_id?: string) {
3346
throw new Error('error');
3447
}
3548

49+
const MOCK_LOAD_ARGS: any = {
50+
params: { id: '123' },
51+
route: {
52+
id: '/users/[id]',
53+
},
54+
url: new URL('http://localhost:3000/users/123'),
55+
request: {
56+
headers: {
57+
get: (key: string) => {
58+
if (key === 'sentry-trace') {
59+
return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
60+
}
61+
62+
if (key === 'baggage') {
63+
return (
64+
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
65+
'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
66+
'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
67+
);
68+
}
69+
70+
return null;
71+
},
72+
},
73+
},
74+
};
75+
76+
beforeAll(() => {
77+
addTracingExtensions();
78+
});
79+
3680
describe('wrapLoadWithSentry', () => {
3781
beforeEach(() => {
3882
mockCaptureException.mockClear();
3983
mockAddExceptionMechanism.mockClear();
84+
mockTrace.mockClear();
4085
mockScope = new Scope();
4186
});
4287

4388
it('calls captureException', async () => {
44-
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
89+
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
4590
return {
4691
post: getById(params.id),
4792
};
4893
}
4994

5095
const wrappedLoad = wrapLoadWithSentry(load);
51-
const res = wrappedLoad({ params: { id: '1' } } as any);
96+
const res = wrappedLoad(MOCK_LOAD_ARGS);
5297
await expect(res).rejects.toThrow();
5398

5499
expect(mockCaptureException).toHaveBeenCalledTimes(1);
55100
});
56101

102+
it('calls trace function', async () => {
103+
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
104+
return {
105+
post: params.id,
106+
};
107+
}
108+
109+
const wrappedLoad = wrapLoadWithSentry(load);
110+
await wrappedLoad(MOCK_LOAD_ARGS);
111+
112+
expect(mockTrace).toHaveBeenCalledTimes(1);
113+
expect(mockTrace).toHaveBeenCalledWith(
114+
{
115+
op: 'function.sveltekit.load',
116+
name: '/users/[id]',
117+
status: 'ok',
118+
metadata: {
119+
source: 'route',
120+
},
121+
},
122+
expect.any(Function),
123+
expect.any(Function),
124+
);
125+
});
126+
57127
it('adds an exception mechanism', async () => {
58128
const addEventProcessorSpy = vi.spyOn(mockScope, 'addEventProcessor').mockImplementationOnce(callback => {
59129
void callback({}, { event_id: 'fake-event-id' });
60130
return mockScope;
61131
});
62132

63-
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
133+
async function load({ params }: Parameters<Load>[0]): Promise<ReturnType<Load>> {
64134
return {
65135
post: getById(params.id),
66136
};
67137
}
68138

69139
const wrappedLoad = wrapLoadWithSentry(load);
70-
const res = wrappedLoad({ params: { id: '1' } } as any);
140+
const res = wrappedLoad(MOCK_LOAD_ARGS);
71141
await expect(res).rejects.toThrow();
72142

73143
expect(addEventProcessorSpy).toBeCalledTimes(1);

0 commit comments

Comments
 (0)