Skip to content

Commit e950daf

Browse files
committed
ref(sveltekit): Split up universal and server page load wrappers
1 parent 751d26a commit e950daf

File tree

6 files changed

+135
-28
lines changed

6 files changed

+135
-28
lines changed

packages/sveltekit/src/client/load.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { trace } from '@sentry/core';
22
import { captureException } from '@sentry/svelte';
33
import { addExceptionMechanism, objectify } from '@sentry/utils';
4-
import type { Load } from '@sveltejs/kit';
4+
import type { LoadEvent } from '@sveltejs/kit';
55

66
function sendErrorToSentry(e: unknown): unknown {
77
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
@@ -27,15 +27,18 @@ function sendErrorToSentry(e: unknown): unknown {
2727
}
2828

2929
/**
30-
* Wrap load function with Sentry
31-
* TODO: usage
32-
*
33-
* @param origLoad SvelteKit user defined load function
30+
* @inheritdoc
3431
*/
35-
export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {
32+
// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`.
33+
// This function needs to tell TS that it returns exactly the type that it was called with
34+
// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types
35+
// at build time for every route.
36+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37+
export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T {
3638
return new Proxy(origLoad, {
3739
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
38-
const [event] = args;
40+
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
41+
const event = args[0] as LoadEvent;
3942

4043
const routeId = event.route.id;
4144
return trace(

packages/sveltekit/src/index.types.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export * from './server';
99

1010
import type { Integration, Options, StackParser } from '@sentry/types';
1111
// eslint-disable-next-line import/no-unresolved
12-
import type { HandleClientError, HandleServerError, Load } from '@sveltejs/kit';
12+
import type { HandleClientError, HandleServerError } from '@sveltejs/kit';
1313

1414
import type * as clientSdk from './client';
1515
import type * as serverSdk from './server';
@@ -21,7 +21,25 @@ export declare function handleErrorWithSentry<T extends HandleClientError | Hand
2121
handleError?: T,
2222
): ReturnType<T>;
2323

24-
export declare function wrapLoadWithSentry<T extends Load>(origLoad: T): T;
24+
/**
25+
* Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality
26+
*
27+
* Usage:
28+
*
29+
* ```js
30+
* // +page.js
31+
*
32+
* import { wrapLoadWithSentry }
33+
*
34+
* export const load = wrapLoadWithSentry((event) => {
35+
* // your load code
36+
* });
37+
* ```
38+
*
39+
* @param origLoad SvelteKit user defined universal `load` function
40+
*/
41+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
42+
export declare function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T;
2543

2644
// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere.
2745
export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations;

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export * from '@sentry/node';
22

33
export { init } from './sdk';
44
export { handleErrorWithSentry } from './handleError';
5-
export { wrapLoadWithSentry } from './load';
5+
export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load';
66
export { sentryHandle } from './handle';

packages/sveltekit/src/server/load.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { trace } from '@sentry/core';
33
import { captureException } from '@sentry/node';
44
import type { TransactionContext } from '@sentry/types';
55
import { addExceptionMechanism, objectify } from '@sentry/utils';
6-
import type { HttpError, Load, ServerLoad } from '@sveltejs/kit';
6+
import type { HttpError, LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
77

88
import { getTracePropagationData } from './utils';
99

@@ -42,19 +42,18 @@ function sendErrorToSentry(e: unknown): unknown {
4242
}
4343

4444
/**
45-
* Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality
46-
*
47-
* Usage:
48-
* ```js
49-
* import { }
50-
* ```
51-
*
52-
* @param origLoad SvelteKit user defined load function
45+
* @inheritdoc
5346
*/
54-
export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {
47+
// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`.
48+
// This function needs to tell TS that it returns exactly the type that it was called with
49+
// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types
50+
// at build time for every route.
51+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
52+
export function wrapLoadWithSentry<T extends (...args: any) => any>(origLoad: T): T {
5553
return new Proxy(origLoad, {
5654
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
57-
const [event] = args;
55+
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
56+
const event = args[0] as LoadEvent;
5857
const routeId = event.route && event.route.id;
5958

6059
const traceLoadContext: TransactionContext = {
@@ -73,20 +72,33 @@ export function wrapLoadWithSentry<T extends Load>(origLoad: T): T {
7372

7473
/**
7574
* Wrap a server-only load function (e.g. +page.server.js or +layout.server.js) with Sentry functionality
76-
* TODO: usage
75+
*
76+
* Usage:
77+
*
78+
* ```js
79+
* // +page.serverjs
80+
*
81+
* import { wrapServerLoadWithSentry }
82+
*
83+
* export const load = wrapServerLoadWithSentry((event) => {
84+
* // your load code
85+
* });
86+
* ```
7787
*
7888
* @param origServerLoad SvelteKit user defined server-only load function
7989
*/
80-
export function wrapServerLoadWithSentry<T extends ServerLoad>(origServerLoad: T): T {
90+
// The liberal generic typing of `T` is necessary because we cannot let T extend `ServerLoad`.
91+
// This function needs to tell TS that it returns exactly the type that it was called with
92+
// because SvelteKit generates the narrowed down `PageServerLoad` or `LayoutServerLoad` types
93+
// at build time for every route.
94+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95+
export function wrapServerLoadWithSentry<T extends (...args: any) => any>(origServerLoad: T): T {
8196
return new Proxy(origServerLoad, {
8297
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
83-
const [event] = args;
98+
// Type casting here because `T` cannot extend `ServerLoad` (see comment above function signature)
99+
const event = args[0] as ServerLoadEvent;
84100
const routeId = event.route && event.route.id;
85101

86-
// Usually, the `handleWithSentry` hook handler should already create a transaction and store
87-
// traceparent and DSC on that transaction before the server-only load function is called.
88-
// However, since we have access to `event.request` we can still pass it to `trace`
89-
// in case our handler isn't called or for some reason the handle hook is bypassed.
90102
const { dynamicSamplingContext, traceparentData } = getTracePropagationData(event);
91103

92104
const traceLoadContext: TransactionContext = {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { DynamicSamplingContext, TraceparentData } from '@sentry/types';
2+
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils';
3+
import type { RequestEvent } from '@sveltejs/kit';
4+
5+
/**
6+
* Takes a request event and extracts traceparent and DSC data
7+
* from the `sentry-trace` and `baggage` DSC headers.
8+
*/
9+
export function getTracePropagationData(event: RequestEvent): {
10+
traceparentData?: TraceparentData;
11+
dynamicSamplingContext?: Partial<DynamicSamplingContext>;
12+
} {
13+
const sentryTraceHeader = event.request.headers.get('sentry-trace');
14+
const baggageHeader = event.request.headers.get('baggage');
15+
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
16+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
17+
18+
return { traceparentData, dynamicSamplingContext };
19+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getTracePropagationData } from '../../src/server/utils';
2+
3+
const MOCK_REQUEST_EVENT: any = {
4+
request: {
5+
headers: {
6+
get: (key: string) => {
7+
if (key === 'sentry-trace') {
8+
return '1234567890abcdef1234567890abcdef-1234567890abcdef-1';
9+
}
10+
11+
if (key === 'baggage') {
12+
return (
13+
'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' +
14+
'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' +
15+
'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1'
16+
);
17+
}
18+
19+
return null;
20+
},
21+
},
22+
},
23+
};
24+
25+
describe('getTracePropagationData', () => {
26+
it('returns traceParentData and DSC if both are available', () => {
27+
const event: any = MOCK_REQUEST_EVENT;
28+
29+
const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);
30+
31+
expect(traceparentData).toEqual({
32+
parentSampled: true,
33+
parentSpanId: '1234567890abcdef',
34+
traceId: '1234567890abcdef1234567890abcdef',
35+
});
36+
37+
expect(dynamicSamplingContext).toEqual({
38+
environment: 'production',
39+
public_key: 'dogsarebadatkeepingsecrets',
40+
release: '1.0.0',
41+
sample_rate: '1',
42+
trace_id: '1234567890abcdef1234567890abcdef',
43+
transaction: 'dogpark',
44+
user_segment: 'segmentA',
45+
});
46+
});
47+
48+
it('returns undefined if the necessary header is not avaolable', () => {
49+
const event: any = { request: { headers: { get: () => undefined } } };
50+
const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);
51+
52+
expect(traceparentData).toBeUndefined();
53+
expect(dynamicSamplingContext).toBeUndefined();
54+
});
55+
});

0 commit comments

Comments
 (0)