Skip to content

Commit 79662d9

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 1e8229a commit 79662d9

File tree

11 files changed

+212
-436
lines changed

11 files changed

+212
-436
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';

0 commit comments

Comments
 (0)