Skip to content

Commit 714a9eb

Browse files
authored
feat(sveltekit): Add performance monitoring for server load (#7536)
1 parent d265fe5 commit 714a9eb

File tree

2 files changed

+117
-17
lines changed

2 files changed

+117
-17
lines changed

packages/sveltekit/src/server/load.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
import { trace } from '@sentry/core';
13
import { captureException } from '@sentry/node';
2-
import { addExceptionMechanism, isThenable, objectify } from '@sentry/utils';
4+
import {
5+
addExceptionMechanism,
6+
baggageHeaderToDynamicSamplingContext,
7+
extractTraceparentData,
8+
objectify,
9+
} from '@sentry/utils';
310
import type { HttpError, ServerLoad } from '@sveltejs/kit';
11+
import * as domain from 'domain';
412

513
function isHttpError(err: unknown): err is HttpError {
614
return typeof err === 'object' && err !== null && 'status' in err && 'body' in err;
@@ -44,21 +52,30 @@ function sendErrorToSentry(e: unknown): unknown {
4452
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
4553
return new Proxy(origLoad, {
4654
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
47-
let maybePromiseResult;
55+
return domain.create().bind(() => {
56+
const [event] = args;
4857

49-
try {
50-
maybePromiseResult = wrappingTarget.apply(thisArg, args);
51-
} catch (e) {
52-
throw sendErrorToSentry(e);
53-
}
58+
const sentryTraceHeader = event.request.headers.get('sentry-trace');
59+
const baggageHeader = event.request.headers.get('baggage');
60+
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
61+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
5462

55-
if (isThenable(maybePromiseResult)) {
56-
Promise.resolve(maybePromiseResult).then(null, e => {
57-
sendErrorToSentry(e);
58-
});
59-
}
60-
61-
return maybePromiseResult;
63+
const routeId = event.route.id;
64+
return trace(
65+
{
66+
op: 'function.sveltekit.load',
67+
name: routeId ? routeId : event.url.pathname,
68+
status: 'ok',
69+
...traceparentData,
70+
metadata: {
71+
source: routeId ? 'route' : 'url',
72+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
73+
},
74+
},
75+
() => wrappingTarget.apply(thisArg, args),
76+
sendErrorToSentry,
77+
);
78+
})();
6279
},
6380
});
6481
}

packages/sveltekit/test/server/load.test.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { addTracingExtensions } from '@sentry/core';
12
import { Scope } from '@sentry/node';
23
import type { ServerLoad } from '@sveltejs/kit';
34
import { error } from '@sveltejs/kit';
@@ -20,6 +21,19 @@ vi.mock('@sentry/node', async () => {
2021
};
2122
});
2223

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

2539
vi.mock('@sentry/utils', async () => {
@@ -34,10 +48,42 @@ function getById(_id?: string) {
3448
throw new Error('error');
3549
}
3650

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

@@ -49,12 +95,49 @@ describe('wrapLoadWithSentry', () => {
4995
}
5096

5197
const wrappedLoad = wrapLoadWithSentry(load);
52-
const res = wrappedLoad({ params: { id: '1' } } as any);
98+
const res = wrappedLoad(MOCK_LOAD_ARGS);
5399
await expect(res).rejects.toThrow();
54100

55101
expect(mockCaptureException).toHaveBeenCalledTimes(1);
56102
});
57103

104+
it('calls trace function', async () => {
105+
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
106+
return {
107+
post: params.id,
108+
};
109+
}
110+
111+
const wrappedLoad = wrapLoadWithSentry(load);
112+
await wrappedLoad(MOCK_LOAD_ARGS);
113+
114+
expect(mockTrace).toHaveBeenCalledTimes(1);
115+
expect(mockTrace).toHaveBeenCalledWith(
116+
{
117+
op: 'function.sveltekit.load',
118+
name: '/users/[id]',
119+
parentSampled: true,
120+
parentSpanId: '1234567890abcdef',
121+
status: 'ok',
122+
traceId: '1234567890abcdef1234567890abcdef',
123+
metadata: {
124+
dynamicSamplingContext: {
125+
environment: 'production',
126+
public_key: 'dogsarebadatkeepingsecrets',
127+
release: '1.0.0',
128+
sample_rate: '1',
129+
trace_id: '1234567890abcdef1234567890abcdef',
130+
transaction: 'dogpark',
131+
user_segment: 'segmentA',
132+
},
133+
source: 'route',
134+
},
135+
},
136+
expect.any(Function),
137+
expect.any(Function),
138+
);
139+
});
140+
58141
describe('with error() helper', () => {
59142
it.each([
60143
// [statusCode, timesCalled]
@@ -75,7 +158,7 @@ describe('wrapLoadWithSentry', () => {
75158
}
76159

77160
const wrappedLoad = wrapLoadWithSentry(load);
78-
const res = wrappedLoad({ params: { id: '1' } } as any);
161+
const res = wrappedLoad(MOCK_LOAD_ARGS);
79162
await expect(res).rejects.toThrow();
80163

81164
expect(mockCaptureException).toHaveBeenCalledTimes(times);
@@ -95,7 +178,7 @@ describe('wrapLoadWithSentry', () => {
95178
}
96179

97180
const wrappedLoad = wrapLoadWithSentry(load);
98-
const res = wrappedLoad({ params: { id: '1' } } as any);
181+
const res = wrappedLoad(MOCK_LOAD_ARGS);
99182
await expect(res).rejects.toThrow();
100183

101184
expect(addEventProcessorSpy).toBeCalledTimes(1);

0 commit comments

Comments
 (0)