Skip to content

Commit 4bc2407

Browse files
committed
fix(sveltekit): Handle server-only and shared load functions
1 parent 8e78e6e commit 4bc2407

File tree

2 files changed

+187
-45
lines changed

2 files changed

+187
-45
lines changed

packages/sveltekit/src/server/load.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
22
import { trace } from '@sentry/core';
33
import { captureException } from '@sentry/node';
4+
import type { TransactionContext } from '@sentry/types';
45
import {
56
addExceptionMechanism,
67
baggageHeaderToDynamicSamplingContext,
78
extractTraceparentData,
89
objectify,
910
} from '@sentry/utils';
10-
import type { HttpError, ServerLoad } from '@sveltejs/kit';
11+
import type { HttpError, Load, LoadEvent, ServerLoad, ServerLoadEvent } from '@sveltejs/kit';
1112
import * as domain from 'domain';
1213

1314
function isHttpError(err: unknown): err is HttpError {
@@ -49,33 +50,55 @@ function sendErrorToSentry(e: unknown): unknown {
4950
*
5051
* @param origLoad SvelteKit user defined load function
5152
*/
52-
export function wrapLoadWithSentry(origLoad: ServerLoad): ServerLoad {
53+
export function wrapLoadWithSentry<T extends Load | ServerLoad>(origLoad: T): T {
5354
return new Proxy(origLoad, {
54-
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad>) => {
55+
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad | Load>) => {
5556
return domain.create().bind(() => {
5657
const [event] = args;
58+
const routeId = event.route && event.route.id;
5759

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);
60+
const traceSharedLoadContext: TransactionContext = {
61+
op: 'function.sveltekit.load',
62+
name: routeId ? routeId : event.url.pathname,
63+
status: 'ok',
64+
metadata: {
65+
source: routeId ? 'route' : 'url',
66+
},
67+
};
68+
69+
let finalTraceLoadContext = { ...traceSharedLoadContext };
70+
71+
if (isServerOnlyLoad(event)) {
72+
const sentryTraceHeader = event.request.headers.get('sentry-trace');
73+
const baggageHeader = event.request.headers.get('baggage');
74+
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
75+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
6276

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',
77+
const traceSeverOnlyLoadContext = {
6978
...traceparentData,
7079
metadata: {
71-
source: routeId ? 'route' : 'url',
80+
...traceSharedLoadContext.metadata,
7281
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
7382
},
74-
},
75-
() => wrappingTarget.apply(thisArg, args),
76-
sendErrorToSentry,
77-
);
83+
};
84+
85+
finalTraceLoadContext = { ...traceSharedLoadContext, ...traceSeverOnlyLoadContext };
86+
}
87+
88+
return trace(finalTraceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry);
7889
})();
7990
},
8091
});
8192
}
93+
94+
/**
95+
* Our server-side wrapLoadWithSentry can be used to wrap two different kinds of `load` functions:
96+
* - load functions from `+(page|layout).ts`: These can be called both on client and on server
97+
* - load functions from `+(page|layout).server.ts`: These are only called on the server
98+
*
99+
* In both cases, load events look differently. We can distinguish them by checking if the
100+
* event has a `request` field (which only the server-exclusive load event has).
101+
*/
102+
function isServerOnlyLoad(event: ServerLoadEvent | LoadEvent): event is ServerLoadEvent {
103+
return 'request' in event;
104+
}

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

Lines changed: 146 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ const MOCK_LOAD_ARGS: any = {
5454
id: '/users/[id]',
5555
},
5656
url: new URL('http://localhost:3000/users/123'),
57+
};
58+
59+
const MOCK_LOAD_NO_ROUTE_ARGS: any = {
60+
params: { id: '123' },
61+
url: new URL('http://localhost:3000/users/123'),
62+
};
63+
64+
const MOCK_SERVER_ONLY_LOAD_ARGS: any = {
65+
...MOCK_LOAD_ARGS,
5766
request: {
5867
headers: {
5968
get: (key: string) => {
@@ -75,6 +84,32 @@ const MOCK_LOAD_ARGS: any = {
7584
},
7685
};
7786

87+
const MOCK_SERVER_ONLY_NO_TRACE_LOAD_ARGS: any = {
88+
...MOCK_LOAD_ARGS,
89+
request: {
90+
headers: {
91+
get: (_: string) => {
92+
return null;
93+
},
94+
},
95+
},
96+
};
97+
98+
const MOCK_SERVER_ONLY_NO_BAGGAGE_LOAD_ARGS: any = {
99+
...MOCK_LOAD_ARGS,
100+
request: {
101+
headers: {
102+
get: (key: string) => {
103+
if (key === 'sentry-trace') {
104+
return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
105+
}
106+
107+
return null;
108+
},
109+
},
110+
},
111+
};
112+
78113
beforeAll(() => {
79114
addTracingExtensions();
80115
});
@@ -101,41 +136,125 @@ describe('wrapLoadWithSentry', () => {
101136
expect(mockCaptureException).toHaveBeenCalledTimes(1);
102137
});
103138

104-
it('calls trace function', async () => {
139+
describe('calls trace', () => {
105140
async function load({ params }: Parameters<ServerLoad>[0]): Promise<ReturnType<ServerLoad>> {
106141
return {
107142
post: params.id,
108143
};
109144
}
110145

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',
146+
describe('for server-only load', () => {
147+
it('attaches trace data if available', async () => {
148+
const wrappedLoad = wrapLoadWithSentry(load);
149+
await wrappedLoad(MOCK_SERVER_ONLY_LOAD_ARGS);
150+
151+
expect(mockTrace).toHaveBeenCalledTimes(1);
152+
expect(mockTrace).toHaveBeenCalledWith(
153+
{
154+
op: 'function.sveltekit.load',
155+
name: '/users/[id]',
156+
parentSampled: true,
157+
parentSpanId: '1234567890abcdef',
158+
status: 'ok',
159+
traceId: '1234567890abcdef1234567890abcdef',
160+
metadata: {
161+
dynamicSamplingContext: {
162+
environment: 'production',
163+
public_key: 'dogsarebadatkeepingsecrets',
164+
release: '1.0.0',
165+
sample_rate: '1',
166+
trace_id: '1234567890abcdef1234567890abcdef',
167+
transaction: 'dogpark',
168+
user_segment: 'segmentA',
169+
},
170+
source: 'route',
171+
},
172+
},
173+
expect.any(Function),
174+
expect.any(Function),
175+
);
176+
});
177+
178+
it("doesn't attach trace data if it's not available", async () => {
179+
const wrappedLoad = wrapLoadWithSentry(load);
180+
await wrappedLoad(MOCK_SERVER_ONLY_NO_TRACE_LOAD_ARGS);
181+
182+
expect(mockTrace).toHaveBeenCalledTimes(1);
183+
expect(mockTrace).toHaveBeenCalledWith(
184+
{
185+
op: 'function.sveltekit.load',
186+
name: '/users/[id]',
187+
status: 'ok',
188+
metadata: {
189+
source: 'route',
190+
},
191+
},
192+
expect.any(Function),
193+
expect.any(Function),
194+
);
195+
});
196+
197+
it("doesn't attach the DSC data if the baggage header not available", async () => {
198+
const wrappedLoad = wrapLoadWithSentry(load);
199+
await wrappedLoad(MOCK_SERVER_ONLY_NO_BAGGAGE_LOAD_ARGS);
200+
201+
expect(mockTrace).toHaveBeenCalledTimes(1);
202+
expect(mockTrace).toHaveBeenCalledWith(
203+
{
204+
op: 'function.sveltekit.load',
205+
name: '/users/[id]',
206+
parentSampled: true,
207+
parentSpanId: '1234567890abcdef',
208+
status: 'ok',
209+
traceId: '1234567890abcdef1234567890abcdef',
210+
metadata: {
211+
dynamicSamplingContext: {},
212+
source: 'route',
213+
},
214+
},
215+
expect.any(Function),
216+
expect.any(Function),
217+
);
218+
});
219+
});
220+
221+
it('for shared load', async () => {
222+
const wrappedLoad = wrapLoadWithSentry(load);
223+
await wrappedLoad(MOCK_LOAD_ARGS);
224+
225+
expect(mockTrace).toHaveBeenCalledTimes(1);
226+
expect(mockTrace).toHaveBeenCalledWith(
227+
{
228+
op: 'function.sveltekit.load',
229+
name: '/users/[id]',
230+
status: 'ok',
231+
metadata: {
232+
source: 'route',
132233
},
133-
source: 'route',
134234
},
135-
},
136-
expect.any(Function),
137-
expect.any(Function),
138-
);
235+
expect.any(Function),
236+
expect.any(Function),
237+
);
238+
});
239+
240+
it('falls back to the raw url if `event.route.id` is not available', async () => {
241+
const wrappedLoad = wrapLoadWithSentry(load);
242+
await wrappedLoad(MOCK_LOAD_NO_ROUTE_ARGS);
243+
244+
expect(mockTrace).toHaveBeenCalledTimes(1);
245+
expect(mockTrace).toHaveBeenCalledWith(
246+
{
247+
op: 'function.sveltekit.load',
248+
name: '/users/123',
249+
status: 'ok',
250+
metadata: {
251+
source: 'url',
252+
},
253+
},
254+
expect.any(Function),
255+
expect.any(Function),
256+
);
257+
});
139258
});
140259

141260
describe('with error() helper', () => {

0 commit comments

Comments
 (0)