Skip to content

Commit 9eec3cf

Browse files
SG60Lms24
authored andcommitted
chore(sveltekit): refactor some common server-side code
Handle functions and utils.ts There is more left to refactor as well
1 parent ce93824 commit 9eec3cf

File tree

11 files changed

+209
-310
lines changed

11 files changed

+209
-310
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import type { continueTrace, Span } from '@sentry/core';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
4+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
5+
getActiveSpan,
6+
getCurrentScope,
7+
getDefaultIsolationScope,
8+
getIsolationScope,
9+
getTraceMetaTags,
10+
logger,
11+
setHttpStatus,
12+
startSpan,
13+
winterCGRequestToRequestData,
14+
withIsolationScope,
15+
} from '@sentry/core';
16+
import type { Handle, ResolveOptions } from '@sveltejs/kit';
17+
18+
import { DEBUG_BUILD } from '../common/debug-build';
19+
import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils';
20+
21+
export type SentryHandleOptions = {
22+
/**
23+
* Controls whether the SDK should capture errors and traces in requests that don't belong to a
24+
* route defined in your SvelteKit application.
25+
*
26+
* By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server).
27+
*
28+
* Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain
29+
* scenarios, for instance if you registered other handlers that handle these requests.
30+
* If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction`
31+
* callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out
32+
* transactions that you still don't want to be sent to Sentry.
33+
*
34+
* @default false
35+
*/
36+
handleUnknownRoutes?: boolean;
37+
38+
/**
39+
* Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation
40+
* of `fetch` calls in `load` functions.
41+
*
42+
* @default true
43+
*/
44+
injectFetchProxyScript?: boolean;
45+
};
46+
47+
export const FETCH_PROXY_SCRIPT = `
48+
const f = window.fetch;
49+
if(f){
50+
window._sentryFetchProxy = function(...a){return f(...a)}
51+
window.fetch = function(...a){return window._sentryFetchProxy(...a)}
52+
}
53+
`;
54+
/**
55+
* Adds Sentry tracing <meta> tags to the returned html page.
56+
* Adds Sentry fetch proxy script to the returned html page if enabled in options.
57+
*
58+
* Exported only for testing
59+
*/
60+
export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean }): NonNullable<
61+
ResolveOptions['transformPageChunk']
62+
> {
63+
return ({ html }) => {
64+
const metaTags = getTraceMetaTags();
65+
const headWithMetaTags = metaTags ? `<head>\n${metaTags}` : '<head>';
66+
67+
const headWithFetchScript = options.injectFetchProxyScript ? `\n<script>${FETCH_PROXY_SCRIPT}</script>` : '';
68+
69+
const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`;
70+
71+
return html.replace('<head>', modifiedHead);
72+
};
73+
}
74+
75+
async function instrumentHandle(
76+
{ event, resolve }: Parameters<Handle>[0],
77+
options: SentryHandleOptions,
78+
): Promise<Response> {
79+
if (!event.route?.id && !options.handleUnknownRoutes) {
80+
return resolve(event);
81+
}
82+
83+
// caching the result of the version check in `options.injectFetchProxyScript`
84+
// to avoid doing the dynamic import on every request
85+
if (options.injectFetchProxyScript == null) {
86+
try {
87+
// @ts-expect-error - the dynamic import is fine here
88+
const { VERSION } = await import('@sveltejs/kit');
89+
options.injectFetchProxyScript = isFetchProxyRequired(VERSION);
90+
} catch {
91+
options.injectFetchProxyScript = true;
92+
}
93+
}
94+
95+
const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
96+
97+
if (getIsolationScope() !== getDefaultIsolationScope()) {
98+
getIsolationScope().setTransactionName(routeName);
99+
} else {
100+
DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName');
101+
}
102+
103+
try {
104+
const resolveResult = await startSpan(
105+
{
106+
op: 'http.server',
107+
attributes: {
108+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
109+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
110+
'http.method': event.request.method,
111+
},
112+
name: routeName,
113+
},
114+
async (span?: Span) => {
115+
getCurrentScope().setSDKProcessingMetadata({
116+
normalizedRequest: winterCGRequestToRequestData(event.request.clone()),
117+
});
118+
const res = await resolve(event, {
119+
transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }),
120+
});
121+
if (span) {
122+
setHttpStatus(span, res.status);
123+
}
124+
return res;
125+
},
126+
);
127+
return resolveResult;
128+
} catch (e: unknown) {
129+
sendErrorToSentry(e, 'handle');
130+
throw e;
131+
} finally {
132+
await flushIfServerless();
133+
}
134+
}
135+
136+
/**
137+
* We only need to inject the fetch proxy script for SvelteKit versions < 2.16.0.
138+
* Exported only for testing.
139+
*/
140+
export function isFetchProxyRequired(version: string): boolean {
141+
try {
142+
const [major, minor] = version.trim().replace(/-.*/, '').split('.').map(Number);
143+
if (major != null && minor != null && (major > 2 || (major === 2 && minor >= 16))) {
144+
return false;
145+
}
146+
} catch {
147+
// ignore
148+
}
149+
return true;
150+
}
151+
152+
/**
153+
* A SvelteKit handle function that wraps the request for Sentry error and
154+
* performance monitoring.
155+
*
156+
* Some environments require a different continueTrace function. E.g. Node can use
157+
* the Opentelemetry SDK, whereas Cloudflare cannot.
158+
*/
159+
export function sentryHandleGeneric(
160+
continueTraceFunction: typeof continueTrace,
161+
handlerOptions?: SentryHandleOptions,
162+
): Handle {
163+
const options = {
164+
handleUnknownRoutes: false,
165+
injectFetchProxyScript: true,
166+
...handlerOptions,
167+
};
168+
169+
const sentryRequestHandler: Handle = input => {
170+
// event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check
171+
// if we should create a new execution context or not.
172+
// In case of a same-origin `fetch` call within a server`load` function,
173+
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
174+
// to `true` so that no additional network call is made.
175+
// We want the `http.server` span of that nested call to be a child span of the
176+
// currently active span instead of a new root span to correctly reflect this
177+
// behavior.
178+
// As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none,
179+
// we create a new execution context.
180+
const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan();
181+
182+
if (isSubRequest) {
183+
return instrumentHandle(input, options);
184+
}
185+
186+
return withIsolationScope(isolationScope => {
187+
// We only call continueTrace in the initial top level request to avoid
188+
// creating a new root span for the sub request.
189+
isolationScope.setSDKProcessingMetadata({
190+
normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()),
191+
});
192+
return continueTraceFunction(getTracePropagationData(input.event), () => instrumentHandle(input, options));
193+
});
194+
};
195+
196+
return sentryRequestHandler;
197+
}

packages/sveltekit/src/server/utils.ts renamed to packages/sveltekit/src/server-common/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { logger, objectify } from '@sentry/core';
2-
import { captureException, flush } from '@sentry/node';
1+
import { captureException, flush, logger, objectify } from '@sentry/core';
32
import type { RequestEvent } from '@sveltejs/kit';
43

54
import { DEBUG_BUILD } from '../common/debug-build';

packages/sveltekit/src/server/handle.ts

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,66 +15,8 @@ import {
1515
} from '@sentry/core';
1616
import type { Handle, ResolveOptions } from '@sveltejs/kit';
1717

18-
import { DEBUG_BUILD } from '../common/debug-build';
19-
import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils';
20-
21-
export type SentryHandleOptions = {
22-
/**
23-
* Controls whether the SDK should capture errors and traces in requests that don't belong to a
24-
* route defined in your SvelteKit application.
25-
*
26-
* By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server).
27-
*
28-
* Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain
29-
* scenarios, for instance if you registered other handlers that handle these requests.
30-
* If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction`
31-
* callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out
32-
* transactions that you still don't want to be sent to Sentry.
33-
*
34-
* @default false
35-
*/
36-
handleUnknownRoutes?: boolean;
37-
38-
/**
39-
* Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation
40-
* of `fetch` calls in `load` functions.
41-
*
42-
* @default true
43-
*/
44-
injectFetchProxyScript?: boolean;
45-
};
46-
47-
/**
48-
* Exported only for testing
49-
*/
50-
export const FETCH_PROXY_SCRIPT = `
51-
const f = window.fetch;
52-
if(f){
53-
window._sentryFetchProxy = function(...a){return f(...a)}
54-
window.fetch = function(...a){return window._sentryFetchProxy(...a)}
55-
}
56-
`;
57-
58-
/**
59-
* Adds Sentry tracing <meta> tags to the returned html page.
60-
* Adds Sentry fetch proxy script to the returned html page if enabled in options.
61-
*
62-
* Exported only for testing
63-
*/
64-
export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean }): NonNullable<
65-
ResolveOptions['transformPageChunk']
66-
> {
67-
return ({ html }) => {
68-
const metaTags = getTraceMetaTags();
69-
const headWithMetaTags = metaTags ? `<head>\n${metaTags}` : '<head>';
70-
71-
const headWithFetchScript = options.injectFetchProxyScript ? `\n<script>${FETCH_PROXY_SCRIPT}</script>` : '';
72-
73-
const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`;
74-
75-
return html.replace('<head>', modifiedHead);
76-
};
77-
}
18+
import type { SentryHandleOptions } from '../server-common/handle';
19+
import { sentryHandleGeneric } from '../server-common/handle';
7820

7921
/**
8022
* A SvelteKit handle function that wraps the request for Sentry error and

packages/sveltekit/src/server/handleError.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { consoleSandbox } from '@sentry/core';
22
import { captureException } from '@sentry/node';
33
import type { HandleServerError } from '@sveltejs/kit';
44

5-
import { flushIfServerless } from './utils';
5+
import { flushIfServerless } from '../server-common/utils';
66

77
// The SvelteKit default error handler just logs the error's stack trace to the console
88
// see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43

packages/sveltekit/src/server/load.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, sta
33
import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit';
44

55
import type { SentryWrappedFlag } from '../common/utils';
6-
import { flushIfServerless, sendErrorToSentry } from './utils';
6+
import { flushIfServerless, sendErrorToSentry } from '../server-common/utils';
77

88
type PatchedLoadEvent = LoadEvent & SentryWrappedFlag;
99
type PatchedServerLoadEvent = ServerLoadEvent & SentryWrappedFlag;

packages/sveltekit/src/server/serverRoute.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { addNonEnumerableProperty } from '@sentry/core';
22
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node';
33
import type { RequestEvent } from '@sveltejs/kit';
4-
import { flushIfServerless, sendErrorToSentry } from './utils';
4+
import { flushIfServerless, sendErrorToSentry } from '../server-common/utils';
55

66
type PatchedServerRouteEvent = RequestEvent & { __sentry_wrapped__?: boolean };
77

0 commit comments

Comments
 (0)