Skip to content

Commit dc969b9

Browse files
committed
chore(sveltekit): refactor some common server-side code
Handle functions and utils.ts There is more left to refactor as well
1 parent 31aa1d4 commit dc969b9

File tree

11 files changed

+194
-420
lines changed

11 files changed

+194
-420
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
* If this option is set, the `sentryHandle` handler will add a nonce attribute to the script
48+
* tag it injects into the page. This script is used to enable instrumentation of `fetch` calls
49+
* in `load` functions.
50+
*
51+
* Use this if your CSP policy blocks the fetch proxy script injected by `sentryHandle`.
52+
*/
53+
fetchProxyScriptNonce?: string;
54+
};
55+
56+
export const FETCH_PROXY_SCRIPT = `
57+
const f = window.fetch;
58+
if(f){
59+
window._sentryFetchProxy = function(...a){return f(...a)}
60+
window.fetch = function(...a){return window._sentryFetchProxy(...a)}
61+
}
62+
`;
63+
64+
/**
65+
* Adds Sentry tracing <meta> tags to the returned html page.
66+
* Adds Sentry fetch proxy script to the returned html page if enabled in options.
67+
* Also adds a nonce attribute to the script tag if users specified one for CSP.
68+
*/
69+
export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable<ResolveOptions['transformPageChunk']> {
70+
const { fetchProxyScriptNonce, injectFetchProxyScript } = options;
71+
// if injectFetchProxyScript is not set, we default to true
72+
const shouldInjectScript = injectFetchProxyScript !== false;
73+
const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : '';
74+
75+
return ({ html }) => {
76+
const metaTags = getTraceMetaTags();
77+
const headWithMetaTags = metaTags ? `<head>\n${metaTags}` : '<head>';
78+
79+
const headWithFetchScript = shouldInjectScript ? `\n<script ${nonce}>${FETCH_PROXY_SCRIPT}</script>` : '';
80+
81+
const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`;
82+
83+
return html.replace('<head>', modifiedHead);
84+
};
85+
}
86+
87+
export async function instrumentHandle(
88+
{ event, resolve }: Parameters<Handle>[0],
89+
options: SentryHandleOptions,
90+
): Promise<Response> {
91+
if (!event.route?.id && !options.handleUnknownRoutes) {
92+
return resolve(event);
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(options),
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+
* A SvelteKit handle function that wraps the request for Sentry error and
138+
* performance monitoring.
139+
*
140+
* Some environments require a different continueTrace function. E.g. Node can use
141+
* the Opentelemetry SDK, whereas Cloudflare cannot.
142+
*/
143+
export function sentryHandleGeneric(
144+
continueTraceFunction: typeof continueTrace,
145+
handlerOptions?: SentryHandleOptions,
146+
): Handle {
147+
const options = {
148+
handleUnknownRoutes: false,
149+
injectFetchProxyScript: true,
150+
...handlerOptions,
151+
};
152+
153+
const sentryRequestHandler: Handle = input => {
154+
// event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check
155+
// if we should create a new execution context or not.
156+
// In case of a same-origin `fetch` call within a server`load` function,
157+
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
158+
// to `true` so that no additional network call is made.
159+
// We want the `http.server` span of that nested call to be a child span of the
160+
// currently active span instead of a new root span to correctly reflect this
161+
// behavior.
162+
// As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none,
163+
// we create a new execution context.
164+
const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan();
165+
166+
if (isSubRequest) {
167+
return instrumentHandle(input, options);
168+
}
169+
170+
return withIsolationScope(isolationScope => {
171+
// We only call continueTrace in the initial top level request to avoid
172+
// creating a new root span for the sub request.
173+
isolationScope.setSDKProcessingMetadata({
174+
normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()),
175+
});
176+
return continueTraceFunction(getTracePropagationData(input.event), () => instrumentHandle(input, options));
177+
});
178+
};
179+
180+
return sentryRequestHandler;
181+
}

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';
Lines changed: 3 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,7 @@
1-
import type { 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';
161
import { continueTrace } from '@sentry/node';
17-
import type { Handle, ResolveOptions } from '@sveltejs/kit';
2+
import type { Handle } from '@sveltejs/kit';
183

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

936
/**
947
* A SvelteKit handle function that wraps the request for Sentry error and
@@ -106,87 +19,7 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable<R
10619
* ```
10720
*/
10821
export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
109-
const options = {
110-
handleUnknownRoutes: false,
111-
injectFetchProxyScript: true,
112-
...handlerOptions,
113-
};
114-
115-
const sentryRequestHandler: Handle = input => {
116-
// event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check
117-
// if we should create a new execution context or not.
118-
// In case of a same-origin `fetch` call within a server`load` function,
119-
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
120-
// to `true` so that no additional network call is made.
121-
// We want the `http.server` span of that nested call to be a child span of the
122-
// currently active span instead of a new root span to correctly reflect this
123-
// behavior.
124-
// As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none,
125-
// we create a new execution context.
126-
const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan();
127-
128-
if (isSubRequest) {
129-
return instrumentHandle(input, options);
130-
}
131-
132-
return withIsolationScope(isolationScope => {
133-
// We only call continueTrace in the initial top level request to avoid
134-
// creating a new root span for the sub request.
135-
isolationScope.setSDKProcessingMetadata({
136-
normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()),
137-
});
138-
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
139-
});
140-
};
22+
const sentryRequestHandler = sentryHandleGeneric(continueTrace, handlerOptions);
14123

14224
return sentryRequestHandler;
14325
}
144-
145-
async function instrumentHandle(
146-
{ event, resolve }: Parameters<Handle>[0],
147-
options: SentryHandleOptions,
148-
): Promise<Response> {
149-
if (!event.route?.id && !options.handleUnknownRoutes) {
150-
return resolve(event);
151-
}
152-
153-
const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
154-
155-
if (getIsolationScope() !== getDefaultIsolationScope()) {
156-
getIsolationScope().setTransactionName(routeName);
157-
} else {
158-
DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName');
159-
}
160-
161-
try {
162-
const resolveResult = await startSpan(
163-
{
164-
op: 'http.server',
165-
attributes: {
166-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
167-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
168-
'http.method': event.request.method,
169-
},
170-
name: routeName,
171-
},
172-
async (span?: Span) => {
173-
getCurrentScope().setSDKProcessingMetadata({
174-
normalizedRequest: winterCGRequestToRequestData(event.request.clone()),
175-
});
176-
const res = await resolve(event, {
177-
transformPageChunk: addSentryCodeToPage(options),
178-
});
179-
if (span) {
180-
setHttpStatus(span, res.status);
181-
}
182-
return res;
183-
},
184-
);
185-
return resolveResult;
186-
} catch (e: unknown) {
187-
sendErrorToSentry(e, 'handle');
188-
throw e;
189-
} finally {
190-
await flushIfServerless();
191-
}
192-
}

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;

0 commit comments

Comments
 (0)