diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore new file mode 100644 index 000000000000..bff793d5eae7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore @@ -0,0 +1,24 @@ +test-results +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc new file mode 100644 index 000000000000..0e94f06dacb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc @@ -0,0 +1,3 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 +engine-strict=true diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md new file mode 100644 index 000000000000..b5b295070b44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json new file mode 100644 index 000000000000..412f4281c256 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -0,0 +1,31 @@ +{ + "name": "sveltekit-cloudflare-pages", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "wrangler pages dev ./.svelte-kit/cloudflare --port 4173", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:e2e": "playwright test", + "test": "npm run test:e2e", + "test:build": "pnpm install && pnpm build", + "test:assert": "npm run test:e2e" + }, + "dependencies": { + "@sentry/sveltekit": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.45.3", + "@sveltejs/adapter-cloudflare": "^4.8.0", + "@sveltejs/kit": "^2.9.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.0.0", + "wrangler": "^3" + } +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts new file mode 100644 index 000000000000..43613d664655 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + + testDir: 'tests' +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts new file mode 100644 index 000000000000..da08e6da592d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html new file mode 100644 index 000000000000..77a5ff52c923 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts new file mode 100644 index 000000000000..4dc12acebc45 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts @@ -0,0 +1,8 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + dsn: env.PUBLIC_E2E_TEST_DSN, +}); + +export const handleError = Sentry.handleErrorWithSentry(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts new file mode 100644 index 000000000000..d9dbd4a356a0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts @@ -0,0 +1,12 @@ +import { E2E_TEST_DSN } from '$env/static/private'; +import { handleErrorWithSentry, initCloudflareSentryHandle, sentryHandle } from '@sentry/sveltekit'; +import { sequence } from '@sveltejs/kit/hooks'; + +export const handleError = handleErrorWithSentry(); + +export const handle = sequence( + initCloudflareSentryHandle({ + dsn: E2E_TEST_DSN, + }), + sentryHandle(), +); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/lib/index.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/lib/index.ts new file mode 100644 index 000000000000..856f2b6c38ae --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts new file mode 100644 index 000000000000..3cbde33753a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async function load() { + return { + message: 'From server load function.', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte new file mode 100644 index 000000000000..e17881ceaca9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte @@ -0,0 +1,10 @@ + + +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

+ +prerender test + +

{data.message}

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.server.ts new file mode 100644 index 000000000000..a3ede141fc59 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.server.ts @@ -0,0 +1,9 @@ +import type { PageServerLoad } from './$types'; + +export const prerender = true; + +export const load: PageServerLoad = async function load() { + return { + message: 'From server load function.', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.svelte new file mode 100644 index 000000000000..1aea39c3032b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/prerender-test/+page.svelte @@ -0,0 +1,6 @@ + + +

{data.message}

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png new file mode 100644 index 000000000000..825b9e65af7c Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png differ diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/svelte.config.js b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/svelte.config.js new file mode 100644 index 000000000000..581cd159f25d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from "@sveltejs/adapter-cloudflare"; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts new file mode 100644 index 000000000000..81eaa6f809f4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from '@playwright/test'; + +test('home page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); + +test('prerendered page has expected h1', async ({ page }) => { + await page.goto('/prerender-test'); + await expect(page.locator('h1')).toHaveText('From server load function.'); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json new file mode 100644 index 000000000000..0b2d8865f4ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts new file mode 100644 index 000000000000..e68fbfd2778d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts @@ -0,0 +1,7 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; + +export default defineConfig({ + plugins: [sentrySvelteKit({ autoUploadSourceMaps: false }), sveltekit()], +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml new file mode 100644 index 000000000000..d31d2fc7f225 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml @@ -0,0 +1,2 @@ +compatibility_date = "2024-12-17" +compatibility_flags = ["nodejs_compat"] diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 4961d2727696..0e69fdf45e77 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -20,6 +20,10 @@ "./package.json": "./package.json", ".": { "types": "./build/types/index.types.d.ts", + "worker": { + "import": "./build/esm/index.worker.js", + "require": "./build/cjs/index.worker.js" + }, "browser": { "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" @@ -40,6 +44,7 @@ } }, "dependencies": { + "@sentry/cloudflare": "8.45.0", "@sentry/core": "8.45.0", "@sentry/node": "8.45.0", "@sentry/opentelemetry": "8.45.0", diff --git a/packages/sveltekit/rollup.npm.config.mjs b/packages/sveltekit/rollup.npm.config.mjs index b0a19e091ad8..91a460933251 100644 --- a/packages/sveltekit/rollup.npm.config.mjs +++ b/packages/sveltekit/rollup.npm.config.mjs @@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'], + 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'], packageSpecificConfig: { external: ['$app/stores'], output: { diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 6f45425e33e0..62125a36a8e5 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -4,6 +4,12 @@ export * from './client'; export * from './vite'; export * from './server'; +export * from './worker'; + +// Use the ./server version of some functions that are also exported from ./worker +export { sentryHandle } from './server'; +// Use the ./worker version of some functions that are also exported from ./server +export { initCloudflareSentryHandle } from './worker'; import type { Client, Integration, Options, StackParser } from '@sentry/core'; import type { HandleClientError, HandleServerError } from '@sveltejs/kit'; diff --git a/packages/sveltekit/src/index.worker.ts b/packages/sveltekit/src/index.worker.ts new file mode 100644 index 000000000000..016e36c8a289 --- /dev/null +++ b/packages/sveltekit/src/index.worker.ts @@ -0,0 +1,2 @@ +export * from './worker'; +// export * from './vite'; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts new file mode 100644 index 000000000000..ebf2139d6260 --- /dev/null +++ b/packages/sveltekit/src/server-common/handle.ts @@ -0,0 +1,187 @@ +import type { Span } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + continueTrace, + getActiveSpan, + getCurrentScope, + getDefaultIsolationScope, + getIsolationScope, + getTraceMetaTags, + logger, + setHttpStatus, + startSpan, + winterCGRequestToRequestData, + withIsolationScope, +} from '@sentry/core'; +import type { Handle, ResolveOptions } from '@sveltejs/kit'; + +import { DEBUG_BUILD } from '../common/debug-build'; +import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; + +export type SentryHandleOptions = { + /** + * Controls whether the SDK should capture errors and traces in requests that don't belong to a + * route defined in your SvelteKit application. + * + * By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server). + * + * Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain + * scenarios, for instance if you registered other handlers that handle these requests. + * If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction` + * callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out + * transactions that you still don't want to be sent to Sentry. + * + * @default false + */ + handleUnknownRoutes?: boolean; + + /** + * Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation + * of `fetch` calls in `load` functions. + * + * @default true + */ + injectFetchProxyScript?: boolean; + + /** + * If this option is set, the `sentryHandle` handler will add a nonce attribute to the script + * tag it injects into the page. This script is used to enable instrumentation of `fetch` calls + * in `load` functions. + * + * Use this if your CSP policy blocks the fetch proxy script injected by `sentryHandle`. + */ + fetchProxyScriptNonce?: string; +}; + +export const FETCH_PROXY_SCRIPT = ` + const f = window.fetch; + if(f){ + window._sentryFetchProxy = function(...a){return f(...a)} + window.fetch = function(...a){return window._sentryFetchProxy(...a)} + } +`; + +/** + * Adds Sentry tracing tags to the returned html page. + * Adds Sentry fetch proxy script to the returned html page if enabled in options. + * Also adds a nonce attribute to the script tag if users specified one for CSP. + */ +export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { + const { fetchProxyScriptNonce, injectFetchProxyScript } = options; + // if injectFetchProxyScript is not set, we default to true + const shouldInjectScript = injectFetchProxyScript !== false; + const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : ''; + + return ({ html }) => { + const metaTags = getTraceMetaTags(); + const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; + + const headWithFetchScript = shouldInjectScript ? `\n` : ''; + + const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`; + + return html.replace('', modifiedHead); + }; +} + +export async function instrumentHandle( + { event, resolve }: Parameters[0], + options: SentryHandleOptions, +): Promise { + if (!event.route?.id && !options.handleUnknownRoutes) { + return resolve(event); + } + + const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`; + + if (getIsolationScope() !== getDefaultIsolationScope()) { + getIsolationScope().setTransactionName(routeName); + } else { + DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + } + + try { + const resolveResult = await startSpan( + { + op: 'http.server', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url', + 'http.method': event.request.method, + }, + name: routeName, + }, + async (span?: Span) => { + getCurrentScope().setSDKProcessingMetadata({ + normalizedRequest: winterCGRequestToRequestData(event.request.clone()), + }); + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage(options), + }); + if (span) { + setHttpStatus(span, res.status); + } + return res; + }, + ); + return resolveResult; + } catch (e: unknown) { + sendErrorToSentry(e, 'handle'); + throw e; + } finally { + await flushIfServerless(); + } +} + +/** + * A SvelteKit handle function that wraps the request for Sentry error and + * performance monitoring. + * + * Usage: + * ``` + * // src/hooks.server.ts + * import { sentryHandle } from '@sentry/sveltekit'; + * + * export const handle = sentryHandle(); + * + * // Optionally use the `sequence` function to add additional handlers. + * // export const handle = sequence(sentryHandle(), yourCustomHandler); + * ``` + */ +export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { + const options = { + handleUnknownRoutes: false, + injectFetchProxyScript: true, + ...handlerOptions, + }; + + const sentryRequestHandler: Handle = input => { + // event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check + // if we should create a new execution context or not. + // In case of a same-origin `fetch` call within a server`load` function, + // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` + // to `true` so that no additional network call is made. + // We want the `http.server` span of that nested call to be a child span of the + // currently active span instead of a new root span to correctly reflect this + // behavior. + // As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none, + // we create a new execution context. + const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan(); + + if (isSubRequest) { + return instrumentHandle(input, options); + } + + return withIsolationScope(isolationScope => { + // We only call continueTrace in the initial top level request to avoid + // creating a new root span for the sub request. + isolationScope.setSDKProcessingMetadata({ + normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()), + }); + return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); + }); + }; + + return sentryRequestHandler; +} diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server-common/handleError.ts similarity index 95% rename from packages/sveltekit/src/server/handleError.ts rename to packages/sveltekit/src/server-common/handleError.ts index 7f6a8cd0b0cb..992d71d014a4 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server-common/handleError.ts @@ -1,8 +1,7 @@ -import { consoleSandbox } from '@sentry/core'; -import { captureException } from '@sentry/node'; +import { captureException, consoleSandbox } from '@sentry/core'; import type { HandleServerError } from '@sveltejs/kit'; -import { flushIfServerless } from './utils'; +import { flushIfServerless } from '../server-common/utils'; // The SvelteKit default error handler just logs the error's stack trace to the console // see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43 diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server-common/load.ts similarity index 96% rename from packages/sveltekit/src/server/load.ts rename to packages/sveltekit/src/server-common/load.ts index c4a67eaf3482..1c65dacf1254 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server-common/load.ts @@ -1,5 +1,9 @@ -import { addNonEnumerableProperty } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node'; +import { + addNonEnumerableProperty, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + startSpan, +} from '@sentry/core'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; diff --git a/packages/sveltekit/src/server/rewriteFramesIntegration.ts b/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts similarity index 93% rename from packages/sveltekit/src/server/rewriteFramesIntegration.ts rename to packages/sveltekit/src/server-common/rewriteFramesIntegration.ts index 44afbca2d6df..3ed3e72c1f49 100644 --- a/packages/sveltekit/src/server/rewriteFramesIntegration.ts +++ b/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts @@ -7,7 +7,7 @@ import { join, rewriteFramesIntegration as originalRewriteFramesIntegration, } from '@sentry/core'; -import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; +import { WRAPPED_MODULE_SUFFIX } from '../vite/constants'; import type { GlobalWithSentryValues } from '../vite/injectGlobalValues'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; @@ -53,8 +53,8 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { if (isWindowsFrame || startsWithSlash) { const filename = isWindowsFrame ? frame.filename - .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix - .replace(/\\/g, '/') // replace all `\\` instances with `/` + .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\\` instances with `/` : frame.filename; let strippedFilename; diff --git a/packages/sveltekit/src/server/serverRoute.ts b/packages/sveltekit/src/server-common/serverRoute.ts similarity index 92% rename from packages/sveltekit/src/server/serverRoute.ts rename to packages/sveltekit/src/server-common/serverRoute.ts index dbf930ce1180..ec0ebda529e6 100644 --- a/packages/sveltekit/src/server/serverRoute.ts +++ b/packages/sveltekit/src/server-common/serverRoute.ts @@ -1,5 +1,9 @@ -import { addNonEnumerableProperty } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node'; +import { + addNonEnumerableProperty, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + startSpan, +} from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; import { flushIfServerless, sendErrorToSentry } from './utils'; diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server-common/utils.ts similarity index 95% rename from packages/sveltekit/src/server/utils.ts rename to packages/sveltekit/src/server-common/utils.ts index 8eae93d531ab..d6f09093b74d 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -1,5 +1,4 @@ -import { logger, objectify } from '@sentry/core'; -import { captureException, flush } from '@sentry/node'; +import { captureException, flush, logger, objectify } from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; import { DEBUG_BUILD } from '../common/debug-build'; diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 8d5fe21de1c1..cb8166c3d39b 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,192 +1,6 @@ -import type { Span } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - continueTrace, - getActiveSpan, - getCurrentScope, - getDefaultIsolationScope, - getIsolationScope, - getTraceMetaTags, - logger, - setHttpStatus, - startSpan, - winterCGRequestToRequestData, - withIsolationScope, -} from '@sentry/core'; -import type { Handle, ResolveOptions } from '@sveltejs/kit'; +import type { Handle } from '@sveltejs/kit'; -import { DEBUG_BUILD } from '../common/debug-build'; -import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; - -export type SentryHandleOptions = { - /** - * Controls whether the SDK should capture errors and traces in requests that don't belong to a - * route defined in your SvelteKit application. - * - * By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server). - * - * Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain - * scenarios, for instance if you registered other handlers that handle these requests. - * If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction` - * callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out - * transactions that you still don't want to be sent to Sentry. - * - * @default false - */ - handleUnknownRoutes?: boolean; - - /** - * Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation - * of `fetch` calls in `load` functions. - * - * @default true - */ - injectFetchProxyScript?: boolean; - - /** - * If this option is set, the `sentryHandle` handler will add a nonce attribute to the script - * tag it injects into the page. This script is used to enable instrumentation of `fetch` calls - * in `load` functions. - * - * Use this if your CSP policy blocks the fetch proxy script injected by `sentryHandle`. - */ - fetchProxyScriptNonce?: string; -}; - -/** - * Exported only for testing - */ -export const FETCH_PROXY_SCRIPT = ` - const f = window.fetch; - if(f){ - window._sentryFetchProxy = function(...a){return f(...a)} - window.fetch = function(...a){return window._sentryFetchProxy(...a)} - } -`; - -/** - * Adds Sentry tracing tags to the returned html page. - * Adds Sentry fetch proxy script to the returned html page if enabled in options. - * Also adds a nonce attribute to the script tag if users specified one for CSP. - * - * Exported only for testing - */ -export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { - const { fetchProxyScriptNonce, injectFetchProxyScript } = options; - // if injectFetchProxyScript is not set, we default to true - const shouldInjectScript = injectFetchProxyScript !== false; - const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : ''; - - return ({ html }) => { - const metaTags = getTraceMetaTags(); - const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; - - const headWithFetchScript = shouldInjectScript ? `\n` : ''; - - const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`; - - return html.replace('', modifiedHead); - }; -} - -/** - * A SvelteKit handle function that wraps the request for Sentry error and - * performance monitoring. - * - * Usage: - * ``` - * // src/hooks.server.ts - * import { sentryHandle } from '@sentry/sveltekit'; - * - * export const handle = sentryHandle(); - * - * // Optionally use the `sequence` function to add additional handlers. - * // export const handle = sequence(sentryHandle(), yourCustomHandler); - * ``` - */ -export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { - const options = { - handleUnknownRoutes: false, - injectFetchProxyScript: true, - ...handlerOptions, - }; - - const sentryRequestHandler: Handle = input => { - // event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check - // if we should create a new execution context or not. - // In case of a same-origin `fetch` call within a server`load` function, - // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` - // to `true` so that no additional network call is made. - // We want the `http.server` span of that nested call to be a child span of the - // currently active span instead of a new root span to correctly reflect this - // behavior. - // As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none, - // we create a new execution context. - const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan(); - - if (isSubRequest) { - return instrumentHandle(input, options); - } - - return withIsolationScope(isolationScope => { - // We only call continueTrace in the initial top level request to avoid - // creating a new root span for the sub request. - isolationScope.setSDKProcessingMetadata({ - normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()), - }); - return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); - }); - }; - - return sentryRequestHandler; -} - -async function instrumentHandle( - { event, resolve }: Parameters[0], - options: SentryHandleOptions, -): Promise { - if (!event.route?.id && !options.handleUnknownRoutes) { - return resolve(event); - } - - const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`; - - if (getIsolationScope() !== getDefaultIsolationScope()) { - getIsolationScope().setTransactionName(routeName); - } else { - DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - } - - try { - const resolveResult = await startSpan( - { - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url', - 'http.method': event.request.method, - }, - name: routeName, - }, - async (span?: Span) => { - getCurrentScope().setSDKProcessingMetadata({ - normalizedRequest: winterCGRequestToRequestData(event.request.clone()), - }); - const res = await resolve(event, { - transformPageChunk: addSentryCodeToPage(options), - }); - if (span) { - setHttpStatus(span, res.status); - } - return res; - }, - ); - return resolveResult; - } catch (e: unknown) { - sendErrorToSentry(e, 'handle'); - throw e; - } finally { - await flushIfServerless(); - } +/** Documented in `worker/handle.ts` */ +export function initCloudflareSentryHandle(_options: any): Handle { + return ({ event, resolve }) => resolve(event); } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index cbd4934744bf..ad98f8e77cc6 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -130,10 +130,11 @@ export * from '@sentry/node'; // ------------------------- // SvelteKit SDK exports: export { init } from './sdk'; -export { handleErrorWithSentry } from './handleError'; -export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load'; -export { sentryHandle } from './handle'; -export { wrapServerRouteWithSentry } from './serverRoute'; +export { handleErrorWithSentry } from '../server-common/handleError'; +export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/load'; +export { sentryHandle } from '../server-common/handle'; +export { initCloudflareSentryHandle } from './handle'; +export { wrapServerRouteWithSentry } from '../server-common/serverRoute'; /** * Tracks the Svelte component's initialization and mounting operation as well as diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 7f3acbf57fbd..60e6d8e9824c 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -3,7 +3,7 @@ import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getDefaultNodeIntegrations } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; -import { rewriteFramesIntegration } from './rewriteFramesIntegration'; +import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; /** * diff --git a/packages/sveltekit/src/vite/autoInstrument.ts b/packages/sveltekit/src/vite/autoInstrument.ts index 7194a3ae3ac8..d353f8c8bef2 100644 --- a/packages/sveltekit/src/vite/autoInstrument.ts +++ b/packages/sveltekit/src/vite/autoInstrument.ts @@ -4,7 +4,7 @@ import type { ExportNamedDeclaration } from '@babel/types'; import { parseModule } from 'magicast'; import type { Plugin } from 'vite'; -export const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap'; +import { WRAPPED_MODULE_SUFFIX } from './constants'; export type AutoInstrumentSelection = { /** diff --git a/packages/sveltekit/src/vite/constants.ts b/packages/sveltekit/src/vite/constants.ts new file mode 100644 index 000000000000..a0e160fdd272 --- /dev/null +++ b/packages/sveltekit/src/vite/constants.ts @@ -0,0 +1 @@ +export const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap'; diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index b664e2d23db5..40adeab0e3cc 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -8,7 +8,7 @@ import { sentryVitePlugin } from '@sentry/vite-plugin'; import type { Plugin } from 'vite'; import MagicString from 'magic-string'; -import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; +import { WRAPPED_MODULE_SUFFIX } from './constants'; import type { GlobalSentryValues } from './injectGlobalValues'; import { VIRTUAL_GLOBAL_VALUES_FILE, getGlobalValueInjectionCode } from './injectGlobalValues'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; diff --git a/packages/sveltekit/src/worker/handle.ts b/packages/sveltekit/src/worker/handle.ts new file mode 100644 index 000000000000..be9609f11d7e --- /dev/null +++ b/packages/sveltekit/src/worker/handle.ts @@ -0,0 +1,34 @@ +import { CloudflareOptions, wrapRequestHandler } from '@sentry/cloudflare'; +import { getDefaultIntegrations as getDefaultCloudflareIntegrations } from '@sentry/cloudflare'; +import type { Handle } from '@sveltejs/kit'; + +import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; + +/** Initializes Sentry SvelteKit Cloudflare SDK + * This should be before the sentryHandle() call. + * + * In Node.js, this is a stub that does nothing. + * */ +export function initCloudflareSentryHandle(options: CloudflareOptions): Handle { + const opts: CloudflareOptions = { + defaultIntegrations: [...getDefaultCloudflareIntegrations(options), rewriteFramesIntegration()], + ...options, + }; + + const handleInitSentry: Handle = ({ event, resolve }) => { + // if event.platform exists (should be there in a cloudflare worker), then do the cloudflare sentry init + return event.platform + ? wrapRequestHandler( + { + options: opts, + request: event.request, + // @ts-expect-error This will exist in Cloudflare + context: event.platform.context, + }, + () => resolve(event), + ) + : resolve(event); + }; + + return handleInitSentry; +} diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts new file mode 100644 index 000000000000..79f206097af6 --- /dev/null +++ b/packages/sveltekit/src/worker/index.ts @@ -0,0 +1,90 @@ +// For use in cloudflare workers and other edge environments +// +// These are essentially the same as the node server exports, but using imports from @sentry/core +// instead of @sentry/node. +// +// This is expected to be used together with something like the @sentry/cloudflare package, to initialize Sentry +// in the worker. +// +// ------------------------- +// SvelteKit SDK exports: +export { handleErrorWithSentry } from '../server-common/handleError'; +export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/load'; +export { sentryHandle } from '../server-common/handle'; +export { initCloudflareSentryHandle } from './handle'; +export { wrapServerRouteWithSentry } from '../server-common/serverRoute'; + +// Re-export some functions from Cloudflare SDK +export { + addBreadcrumb, + addEventProcessor, + addIntegration, + captureCheckIn, + captureConsoleIntegration, + captureEvent, + captureException, + captureFeedback, + captureMessage, + close, + continueTrace, + createTransport, + dedupeIntegration, + extraErrorDataIntegration, + flush, + functionToStringIntegration, + getActiveSpan, + getClient, + getCurrentScope, + getDefaultIntegrations, + getGlobalScope, + getIsolationScope, + getRootSpan, + getSpanDescendants, + getSpanStatusFromHttpCode, + getTraceData, + getTraceMetaTags, + inboundFiltersIntegration, + isInitialized, + lastEventId, + linkedErrorsIntegration, + requestDataIntegration, + rewriteFramesIntegration, + Scope, + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + setContext, + setCurrentClient, + setExtra, + setExtras, + setHttpStatus, + setMeasurement, + setTag, + setTags, + setUser, + spanToBaggageHeader, + spanToJSON, + spanToTraceHeader, + startInactiveSpan, + startNewTrace, + suppressTracing, + startSpan, + startSpanManual, + trpcMiddleware, + withActiveSpan, + withIsolationScope, + withMonitor, + withScope, + zodErrorsIntegration, +} from '@sentry/cloudflare'; + +/** + * Tracks the Svelte component's initialization and mounting operation as well as + * updates and records them as spans. These spans are only recorded on the client-side. + * Sever-side, during SSR, this function will not record any spans. + */ +export function trackComponent(_options?: unknown): void { + // no-op on the server side +} diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index c5225124ace3..e66d135a7813 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -14,7 +14,8 @@ import type { Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; -import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, sentryHandle } from '../../src/server/handle'; +import { FETCH_PROXY_SCRIPT, addSentryCodeToPage } from '../../src/server-common/handle'; +import { sentryHandle } from '../../src/server/handle'; import { getDefaultNodeClientOptions } from '../utils'; const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); diff --git a/packages/sveltekit/test/server/handleError.test.ts b/packages/sveltekit/test/server/handleError.test.ts index b9a91a0b0e1d..ee62c2fd145b 100644 --- a/packages/sveltekit/test/server/handleError.test.ts +++ b/packages/sveltekit/test/server/handleError.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as SentryNode from '@sentry/node'; import type { HandleServerError, RequestEvent } from '@sveltejs/kit'; -import { handleErrorWithSentry } from '../../src/server/handleError'; +import { handleErrorWithSentry } from '../../src/server-common/handleError'; const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 1001d8464ad4..b1722cd21cee 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -11,7 +11,7 @@ import * as SentryNode from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; -import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server/load'; +import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server-common/load'; import { getDefaultNodeClientOptions } from '../utils'; const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); diff --git a/packages/sveltekit/test/server/rewriteFramesIntegration.ts b/packages/sveltekit/test/server/rewriteFramesIntegration.ts index 3dfd5d3e460e..1d5ca8d4d695 100644 --- a/packages/sveltekit/test/server/rewriteFramesIntegration.ts +++ b/packages/sveltekit/test/server/rewriteFramesIntegration.ts @@ -2,7 +2,7 @@ import { rewriteFramesIntegration } from '@sentry/browser'; import { basename } from '@sentry/core'; import type { Event, StackFrame } from '@sentry/core'; -import { rewriteFramesIteratee } from '../../src/server/rewriteFramesIntegration'; +import { rewriteFramesIteratee } from '../../src/server-common/rewriteFramesIntegration'; import type { GlobalWithSentryValues } from '../../src/vite/injectGlobalValues'; describe('rewriteFramesIteratee', () => { diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 5e8b9b2b99a3..53e588d683ec 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getTracePropagationData } from '../../src/server/utils'; +import { getTracePropagationData } from '../../src/server-common/utils'; const MOCK_REQUEST_EVENT: any = { request: {