10000 ref(sveltekit): Split up universal and server load wrappers (#7652) · TrySound/sentry-javascript@dc0179c · GitHub
[go: up one dir, main page]

Skip to content

Commit dc0179c

Browse files
authored
ref(sveltekit): Split up universal and server load wrappers (getsentry#7652)
Split up the previously generic `handleLoadWithSentry` wrapper function into two functions on the server side: * `handleLoadWithSentry` to wrap universal load on server and client * `handleServerLoadWithSentry` to wrap server load on the server (duh) It further adjusts types on the client as well as on the server side to return a generic. The very liberal typing of the functions' generic is on purpose because any type that's more narrowed down (e.g. `T extends Load`) would create a type error with the users' actual load types. These types (e.g. `PageData` or `ServerLayoutData`) are generated at build time per route/page. Hence, we cannot access them.
1 parent 6678a4a commit dc0179c

File tree

8 files changed

+336
-190
lines changed

8 files changed

+336
-190
lines changed

packages/sveltekit/src/client/load.ts

Lines changed: 11 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,14 +27,18 @@ function sendErrorToSentry(e: unknown): unknown {
2727
}
2828

2929
/**
30-
* Wrap load function with Sentry
31-
*
32-
* @param origLoad SvelteKit user defined load function
30+
* @inheritdoc
3331
*/
34-
export function wrapLoadWithSentry(origLoad: Load): Load {
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 {
3538
return new Proxy(origLoad, {
36-
apply: (wrappingTarget, thisArg, args: Parameters<Load>) => {
37-
const [event] = args;
39+
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
40+
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
41+
const event = args[0] as LoadEvent;
3842

3943
const routeId = event.route.id;
4044
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, ServerLoad } 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<S extends ServerLoad | Load>(origLoad: S): S;
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/handle.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@
22
import type { Span } from '@sentry/core';
33
import { getActiveTransaction, getCurrentHub, trace } from '@sentry/core';
44
import { captureException } from '@sentry/node';
5-
import {
6-
addExceptionMechanism,
7-
baggageHeaderToDynamicSamplingContext,
8-
dynamicSamplingContextToSentryBaggageHeader,
9-
extractTraceparentData,
10-
objectify,
11-
} from '@sentry/utils';
5+
import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils';
126
import type { Handle, ResolveOptions } from '@sveltejs/kit';
137
import * as domain from 'domain';
148

9+
import { getTracePropagationData } from './utils';
10+
1511
function sendErrorToSentry(e: unknown): unknown {
1612
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
1713
// store a seen flag on it.
@@ -77,10 +73,7 @@ export const sentryHandle: Handle = input => {
7773
};
7874

7975
function instrumentHandle({ event, resolve }: Parameters<Handle>[0]): ReturnType<Handle> {
80-
const sentryTraceHeader = event.request.headers.get('sentry-trace');
81-
const baggageHeader = event.request.headers.get('baggage');
82-
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
83-
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
76+
const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);
8477

8578
return trace(
8679
{

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,5 @@ export * from '@sentry/node';
5252
// SvelteKit SDK exports:
5353
export { init } from './sdk';
5454
export { handleErrorWithSentry } from './handleError';
55-
export { wrapLoadWithSentry } from './load';
55+
export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load';
5656
export { sentryHandle } from './handle';

packages/sveltekit/src/server/load.ts

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
22
import { trace } from '@sentry/core';
33
import { captureException } from '@sentry/node';
4-
import type { DynamicSamplingContext, TraceparentData, TransactionContext } from '@sentry/types';
5-
import {
6-
addExceptionMechanism,
7-
baggageHeaderToDynamicSamplingContext,
8-
extractTraceparentData,
9-
objectify,
10-
} from '@sentry/utils';
11-
import type { HttpError, Load, LoadEvent, ServerLoad, ServerLoadEvent } from '@sveltejs/kit';
4+
import type { TransactionContext } from '@sentry/types';
5+
import { addExceptionMechanism, objectify } from '@sentry/utils';
6+
import type { HttpError, LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
7+
8+
import { getTracePropagationData } from './utils';
129

1310
function isHttpError(err: unknown): err is HttpError {
1411
return typeof err === 'object' && err !== null && 'status' in err && 'body' in err;
@@ -45,58 +42,77 @@ function sendErrorToSentry(e: unknown): unknown {
4542
}
4643

4744
/**
48-
* Wrap load function with Sentry
49-
*
50-
* @param origLoad SvelteKit user defined load function
45+
* @inheritdoc
5146
*/
52-
export function wrapLoadWithSentry<T extends ServerLoad | 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 {
5353
return new Proxy(origLoad, {
54-
apply: (wrappingTarget, thisArg, args: Parameters<ServerLoad | Load>) => {
55-
const [event] = args;
54+
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
55+
// Type casting here because `T` cannot extend `Load` (see comment above function signature)
56+
const event = args[0] as LoadEvent;
5657
const routeId = event.route && event.route.id;
5758

58-
const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event);
59-
6059
const traceLoadContext: TransactionContext = {
61-
op: `function.sveltekit${isServerOnlyLoad(event) ? '.server' : ''}.load`,
60+
op: 'function.sveltekit.load',
6261
name: routeId ? routeId : event.url.pathname,
6362
status: 'ok',
6463
metadata: {
6564
source: routeId ? 'route' : 'url',
66-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
6765
},
68-
...traceparentData,
6966
};
7067

7168
return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry);
7269
},
7370
});
7471
}
7572

76-
function getTracePropagationData(event: ServerLoadEvent | LoadEvent): {
77-
traceparentData?: TraceparentData;
78-
dynamicSamplingContext?: Partial<DynamicSamplingContext>;
79-
} {
80-
if (!isServerOnlyLoad(event)) {
81-
return {};
82-
}
83-
84-
const sentryTraceHeader = event.request.headers.get('sentry-trace');
85-
const baggageHeader = event.request.headers.get('baggage');
86-
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
87-
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
88-
89-
return { traceparentData, dynamicSamplingContext };
90-
}
91-
9273
/**
93-
* Our server-side wrapLoadWithSentry can be used to wrap two different kinds of `load` functions:
94-
* - load functions from `+(page|layout).ts`: These can be called both on client and on server
95-
* - load functions from `+(page|layout).server.ts`: These are only called on the server
74+
* Wrap a server-only load function (e.g. +page.server.js or +layout.server.js) with Sentry functionality
75+
*
76+
* Usage:
77+
*
78+
* ```js
79+
* // +page.serverjs
9680
*
97-
* In both cases, load events look differently. We can distinguish them by checking if the
98-
* event has a `request` field (which only the server-exclusive load event has).
81+
* import { wrapServerLoadWithSentry }
82+
*
83+
* export const load = wrapServerLoadWithSentry((event) => {
84+
* // your load code
85+
* });
86+
* ```
87+
*
88+
* @param origServerLoad SvelteKit user defined server-only load function
9989
*/
100-
function isServerOnlyLoad(event: ServerLoadEvent | LoadEvent): event is ServerLoadEvent {
101-
return 'request' in event;
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 {
96+
return new Proxy(origServerLoad, {
97+
apply: (wrappingTarget, thisArg, args: Parameters<T>) => {
98+
// Type casting here because `T` cannot extend `ServerLoad` (see comment above function signature)
99+
const event = args[0] as ServerLoadEvent;
100+
const routeId = event.route && event.route.id;
101+
102+
const { dynamicSamplingContext, traceparentData } = getTracePropagationData(event);
103+
104+
const traceLoadContext: TransactionContext = {
105+
op: 'function.sveltekit.server.load',
106+
name: routeId ? routeId : event.url.pathname,
107+
status: 'ok',
108+
metadata: {
109+
source: routeId ? 'route' : 'url',
110+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
111+
},
112+
...traceparentData,
113+
};
114+
115+
return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry);
116+
},
117+
});
102118
}
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+
}

0 commit comments

Comments
 (0)
0