Skip to content

Commit 36a85a0

Browse files
SG60Lms24
authored andcommitted
fix(sveltekit): Don't use node apis in cloudflare workers
Ideally this would use OTEL, but that requires integrating another library, as the first-party OTEL libaries do not support Cloudflare workers.
1 parent adca0f5 commit 36a85a0

File tree

10 files changed

+655
-1
lines changed

10 files changed

+655
-1
lines changed

packages/sveltekit/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
"./package.json": "./package.json",
2121
".": {
2222
"types": "./build/types/index.types.d.ts",
23+
"worker": {
24+
"import": "./build/esm/index.worker.js",
25+
"require": "./build/cjs/index.worker.js"
26+
},
2327
"browser": {
2428
"import": "./build/esm/index.client.js",
2529
"require": "./build/cjs/index.client.js"

packages/sveltekit/rollup.npm.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu
22

33
export default makeNPMConfigVariants(
44
makeBaseNPMConfig({
5-
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'],
5+
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/index.worker.ts', 'src/client/index.ts', 'src/server/index.ts', 'src/worker/index.ts'],
66
packageSpecificConfig: {
77
external: ['$app/stores'],
88
output: {

packages/sveltekit/src/index.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
export * from './client';
55
export * from './vite';
66
export * from './server';
7+
export * from './worker';
8+
9+
// Use the ./server version of some functions that are also exported from ./worker
10+
export { wrapServerLoadWithSentry, wrapServerRouteWithSentry, sentryHandle } from './server';
711

812
import type { Client, Integration, Options, StackParser } from '@sentry/core';
913
import type { HandleClientError, HandleServerError } from '@sveltejs/kit';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './worker';
2+
// export * from './vite';
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
continueTrace,
16+
} from '@sentry/core';
17+
import type { Handle, ResolveOptions } from '@sveltejs/kit';
18+
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+
}
92+
93+
/**
94+
* A SvelteKit handle function that wraps the request for Sentry error and
95+
* performance monitoring.
96+
*
97+
* This doesn't currently use OTEL, as it isn't available outside of Node
98+
*
99+
* Usage:
100+
* ```
101+
* // src/hooks.server.ts
102+
* import { sentryHandle } from '@sentry/sveltekit';
103+
*
104+
* export const handle = sentryHandle();
105+
*
106+
* // Optionally use the `sequence` function to add additional handlers.
107+
* // export const handle = sequence(sentryHandle(), yourCustomHandler);
108+
* ```
109+
*/
110+
export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
111+
const options = {
112+
handleUnknownRoutes: false,
113+
injectFetchProxyScript: true,
114+
...handlerOptions,
115+
};
116+
117+
const sentryRequestHandler: Handle = input => {
118+
// event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check
119+
// if we should create a new execution context or not.
120+
// In case of a same-origin `fetch` call within a server`load` function,
121+
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
122+
// to `true` so that no additional network call is made.
123+
// We want the `http.server` span of that nested call to be a child span of the
124+
// currently active span instead of a new root span to correctly reflect this
125+
// behavior.
126+
// As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none,
127+
// we create a new execution context.
128+
const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan();
129+
130+
if (isSubRequest) {
131+
return instrumentHandle(input, options);
132+
}
133+
134+
return withIsolationScope(isolationScope => {
135+
// We only call continueTrace in the initial top level request to avoid
136+
// creating a new root span for the sub request.
137+
isolationScope.setSDKProcessingMetadata({
138+
normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()),
139+
});
140+
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
141+
});
142+
};
143+
144+
return sentryRequestHandler;
145+
}
146+
147+
async function instrumentHandle(
148+
{ event, resolve }: Parameters<Handle>[0],
149+
options: SentryHandleOptions,
150+
): Promise<Response> {
151+
if (!event.route?.id && !options.handleUnknownRoutes) {
152+
return resolve(event);
153+
}
154+
155+
const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
156+
157+
if (getIsolationScope() !== getDefaultIsolationScope()) {
158+
getIsolationScope().setTransactionName(routeName);
159+
} else {
160+
DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName');
161+
}
162+
163+
try {
164+
const resolveResult = await startSpan(
165+
{
166+
op: 'http.server',
167+
attributes: {
168+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
169+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
170+
'http.method': event.request.method,
171+
},
172+
name: routeName,
173+
},
174+
async (span?: Span) => {
175+
getCurrentScope().setSDKProcessingMetadata({
176+
normalizedRequest: winterCGRequestToRequestData(event.request.clone()),
177+
});
178+
const res = await resolve(event, {
179+
transformPageChunk: addSentryCodeToPage(options),
180+
});
181+
if (span) {
182+
setHttpStatus(span, res.status);
183+
}
184+
return res;
185+
},
186+
);
187+
return resolveResult;
188+
} catch (e: unknown) {
189+
sendErrorToSentry(e, 'handle');
190+
throw e;
191+
} finally {
192+
await flushIfServerless();
193+
}
194+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { consoleSandbox, captureException } from '@sentry/core';
2+
import type { HandleServerError } from '@sveltejs/kit';
3+
4+
import { flushIfServerless } from './utils';
5+
6+
// The SvelteKit default error handler just logs the error's stack trace to the console
7+
// see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43
8+
function defaultErrorHandler({ error }: Parameters<HandleServerError>[0]): ReturnType<HandleServerError> {
9+
// @ts-expect-error this conforms to the default implementation (including this ts-expect-error)
10+
// eslint-disable-next-line no-console
11+
consoleSandbox(() => console.error(error && error.stack));
12+
}
13+
14+
type HandleServerErrorInput = Parameters<HandleServerError>[0];
15+
16+
/**
17+
* Backwards-compatible HandleServerError Input type for SvelteKit 1.x and 2.x
18+
* `message` and `status` were added in 2.x.
19+
* For backwards-compatibility, we make them optional
20+
*
21+
* @see https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling
22+
*/
23+
type SafeHandleServerErrorInput = Omit<HandleServerErrorInput, 'status' | 'message'> &
24+
Partial<Pick<HandleServerErrorInput, 'status' | 'message'>>;
25+
26+
/**
27+
* Wrapper for the SvelteKit error handler that sends the error to Sentry.
28+
*
29+
* @param handleError The original SvelteKit error handler.
30+
*/
31+
export function handleErrorWithSentry(handleError: HandleServerError = defaultErrorHandler): HandleServerError {
32+
return async (input: SafeHandleServerErrorInput): Promise<void | App.Error> => {
33+
if (isNotFoundError(input)) {
34+
// We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput
35+
// @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type
36+
return handleError(input);
37+
}
38+
39+
captureException(input.error, {
40+
mechanism: {
41+
type: 'sveltekit',
42+
handled: false,
43+
},
44+
});
45+
46+
await flushIfServerless();
47+
48+
// We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput
49+
// @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type
50+
return handleError(input);
51+
};
52+
}
53+
54+
/**
55+
* When a page request fails because the page is not found, SvelteKit throws a "Not found" error.
56+
*/
57+
function isNotFoundError(input: SafeHandleServerErrorInput): boolean {
58+
const { error, event, status } = input;
59+
60+
// SvelteKit 2.0 offers a reliable way to check for a Not Found error:
61+
if (status === 404) {
62+
return true;
63+
}
64+
65+
// SvelteKit 1.x doesn't offer a reliable way to check for a Not Found error.
66+
// So we check the route id (shouldn't exist) and the raw stack trace
67+
// We can delete all of this below whenever we drop Kit 1.x support
68+
const hasNoRouteId = !event.route || !event.route.id;
69+
70+
const rawStack: string =
71+
(error != null &&
72+
typeof error === 'object' &&
73+
'stack' in error &&
74+
typeof error.stack === 'string' &&
75+
error.stack) ||
76+
'';
77+
78+
return hasNoRouteId && rawStack.startsWith('Error: Not found:');
79+
}

0 commit comments

Comments
 (0)