diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9a5e056f3..4cba88cb7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -148,7 +148,8 @@ jobs:
- NextJS-15
- Remix
- React-Native
- - Sveltekit
+ - Sveltekit-Hooks
+ - Sveltekit-Tracing
- Help
- Cloudflare-Wrangler-Sourcemaps
os:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34f2fe56f..90180db33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+- feat(sveltekit): Add support for SDK setup with `instrumentation.server.ts` ([#1077](https://github.com/getsentry/sentry-wizard/pull/1077))
+
+ This release adds support for setting up the SvelteKit SDK in SvelteKit versions 2.31.0 or higher.
+
## 6.3.0
- feat(nextjs,remix,sveltekit,react-native,flutter,ios,angular,android,nuxt): add support to add MCP server during wizard based installlations ([#1063](https://github.com/getsentry/sentry-wizard/pull/1063))
diff --git a/e2e-tests/test-applications/sveltekit-test-app/.gitignore b/e2e-tests/test-applications/sveltekit-hooks-test-app/.gitignore
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/.gitignore
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/.gitignore
diff --git a/e2e-tests/test-applications/sveltekit-test-app/.npmrc b/e2e-tests/test-applications/sveltekit-hooks-test-app/.npmrc
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/.npmrc
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/.npmrc
diff --git a/e2e-tests/test-applications/sveltekit-test-app/package.json b/e2e-tests/test-applications/sveltekit-hooks-test-app/package.json
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/package.json
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/package.json
diff --git a/e2e-tests/test-applications/sveltekit-test-app/src/app.d.ts b/e2e-tests/test-applications/sveltekit-hooks-test-app/src/app.d.ts
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/src/app.d.ts
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/src/app.d.ts
diff --git a/e2e-tests/test-applications/sveltekit-test-app/src/app.html b/e2e-tests/test-applications/sveltekit-hooks-test-app/src/app.html
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/src/app.html
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/src/app.html
diff --git a/e2e-tests/test-applications/sveltekit-test-app/src/lib/index.ts b/e2e-tests/test-applications/sveltekit-hooks-test-app/src/lib/index.ts
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/src/lib/index.ts
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/src/lib/index.ts
diff --git a/e2e-tests/test-applications/sveltekit-test-app/src/routes/+page.svelte b/e2e-tests/test-applications/sveltekit-hooks-test-app/src/routes/+page.svelte
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/src/routes/+page.svelte
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/src/routes/+page.svelte
diff --git a/e2e-tests/test-applications/sveltekit-test-app/svelte.config.js b/e2e-tests/test-applications/sveltekit-hooks-test-app/svelte.config.js
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/svelte.config.js
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/svelte.config.js
diff --git a/e2e-tests/test-applications/sveltekit-test-app/tsconfig.json b/e2e-tests/test-applications/sveltekit-hooks-test-app/tsconfig.json
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/tsconfig.json
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/tsconfig.json
diff --git a/e2e-tests/test-applications/sveltekit-test-app/vite.config.ts b/e2e-tests/test-applications/sveltekit-hooks-test-app/vite.config.ts
similarity index 100%
rename from e2e-tests/test-applications/sveltekit-test-app/vite.config.ts
rename to e2e-tests/test-applications/sveltekit-hooks-test-app/vite.config.ts
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/.gitignore b/e2e-tests/test-applications/sveltekit-tracing-test-app/.gitignore
new file mode 100644
index 000000000..79518f716
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/.gitignore
@@ -0,0 +1,21 @@
+node_modules
+
+# Output
+.output
+.vercel
+/.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/e2e-tests/test-applications/sveltekit-tracing-test-app/.npmrc b/e2e-tests/test-applications/sveltekit-tracing-test-app/.npmrc
new file mode 100644
index 000000000..b6f27f135
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/.npmrc
@@ -0,0 +1 @@
+engine-strict=true
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/package.json b/e2e-tests/test-applications/sveltekit-tracing-test-app/package.json
new file mode 100644
index 000000000..d24686f24
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "sveltekit-test-app",
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-node": "^5.3.1",
+ "@sveltejs/kit": "2.31.0",
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
+ "svelte": "^5.0.0",
+ "svelte-check": "^4.0.0",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.3"
+ }
+}
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.d.ts b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.d.ts
new file mode 100644
index 000000000..c316018cf
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://svelte.dev/docs/kit/types#app
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.html b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.html
new file mode 100644
index 000000000..f273cc58f
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/app.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/src/hooks.server.ts b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/hooks.server.ts
new file mode 100644
index 000000000..0710a0993
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/hooks.server.ts
@@ -0,0 +1,4 @@
+export const handle = async ({ event, resolve }) => {
+ const response = await resolve(event);
+ return response;
+};
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/src/lib/index.ts b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/lib/index.ts
new file mode 100644
index 000000000..856f2b6c3
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/lib/index.ts
@@ -0,0 +1 @@
+// place files you want to import through the `$lib` alias in this folder.
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/src/routes/+page.svelte b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/routes/+page.svelte
new file mode 100644
index 000000000..cc88df0ea
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/src/routes/+page.svelte
@@ -0,0 +1,2 @@
+Welcome to SvelteKit
+Visit svelte.dev/docs/kit to read the documentation
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/svelte.config.js b/e2e-tests/test-applications/sveltekit-tracing-test-app/svelte.config.js
new file mode 100644
index 000000000..ddece5af1
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/svelte.config.js
@@ -0,0 +1,21 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://svelte.dev/docs/kit/integrations#preprocessors
+ // 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(),
+ experimental: {
+ remoteFunctions: true,
+ },
+ },
+};
+
+export default config;
diff --git a/e2e-tests/test-applications/sveltekit-tracing-test-app/tsconfig.json b/e2e-tests/test-applications/sveltekit-tracing-test-app/tsconfig.json
new file mode 100644
index 000000000..0b2d8865f
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/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/e2e-tests/test-applications/sveltekit-tracing-test-app/vite.config.ts b/e2e-tests/test-applications/sveltekit-tracing-test-app/vite.config.ts
new file mode 100644
index 000000000..bbf8c7da4
--- /dev/null
+++ b/e2e-tests/test-applications/sveltekit-tracing-test-app/vite.config.ts
@@ -0,0 +1,6 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit()]
+});
diff --git a/e2e-tests/tests/sveltekit.test.ts b/e2e-tests/tests/sveltekit-hooks.test.ts
similarity index 92%
rename from e2e-tests/tests/sveltekit.test.ts
rename to e2e-tests/tests/sveltekit-hooks.test.ts
index d69f0faee..8a01bdb7d 100644
--- a/e2e-tests/tests/sveltekit.test.ts
+++ b/e2e-tests/tests/sveltekit-hooks.test.ts
@@ -50,7 +50,7 @@ async function runWizardOnSvelteKitProject(
) => unknown,
) {
const wizardInstance = startWizardInstance(integration, projectDir);
- let packageManagerPrompted = false;
+ let kitVersionPrompted = false;
if (fileModificationFn) {
fileModificationFn(projectDir, integration);
@@ -58,16 +58,24 @@ async function runWizardOnSvelteKitProject(
// As we modified project, we have a warning prompt before we get the package manager prompt
await wizardInstance.waitForOutput('Do you want to continue anyway?');
- packageManagerPrompted = await wizardInstance.sendStdinAndWaitForOutput(
+ kitVersionPrompted = await wizardInstance.sendStdinAndWaitForOutput(
[KEYS.ENTER],
- 'Please select your package manager.',
+ "It seems you're using a SvelteKit version",
);
} else {
- packageManagerPrompted = await wizardInstance.waitForOutput(
- 'Please select your package manager',
+ kitVersionPrompted = await wizardInstance.waitForOutput(
+ "It seems you're using a SvelteKit version",
);
}
+ const packageManagerPrompted =
+ kitVersionPrompted &&
+ (await wizardInstance.sendStdinAndWaitForOutput(
+ // Select "Yes, Continue" to perform hooks-based SDK setup
+ [KEYS.DOWN, KEYS.DOWN, KEYS.ENTER],
+ 'Please select your package manager.',
+ ));
+
const tracingOptionPrompted =
packageManagerPrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
@@ -111,7 +119,7 @@ async function runWizardOnSvelteKitProject(
const mcpPrompted =
examplePagePrompted &&
(await wizardInstance.sendStdinAndWaitForOutput(
- [KEYS.ENTER], // This ENTER is for accepting the example page
+ [KEYS.ENTER], // This ENTER is for accepting the example page
'Optionally add a project-scoped MCP server configuration for the Sentry MCP?',
{
optional: true,
@@ -198,7 +206,7 @@ describe('Sveltekit', () => {
const integration = Integration.sveltekit;
const projectDir = path.resolve(
__dirname,
- '../test-applications/sveltekit-test-app',
+ '../test-applications/sveltekit-hooks-test-app',
);
beforeAll(async () => {
@@ -261,7 +269,7 @@ describe('Sveltekit', () => {
const integration = Integration.sveltekit;
const projectDir = path.resolve(
__dirname,
- '../test-applications/sveltekit-test-app',
+ '../test-applications/sveltekit-hooks-test-app',
);
beforeAll(async () => {
diff --git a/e2e-tests/tests/sveltekit-tracing.test.ts b/e2e-tests/tests/sveltekit-tracing.test.ts
new file mode 100644
index 000000000..f5469bc54
--- /dev/null
+++ b/e2e-tests/tests/sveltekit-tracing.test.ts
@@ -0,0 +1,235 @@
+import * as path from 'node:path';
+import * as fs from 'node:fs';
+import { Integration } from '../../lib/Constants';
+import {
+ checkEnvBuildPlugin,
+ checkFileExists,
+ checkIfBuilds,
+ checkIfRunsOnDevMode,
+ checkIfRunsOnProdMode,
+ checkPackageJson,
+ cleanupGit,
+ getWizardCommand,
+ initGit,
+ revertLocalChanges,
+ TEST_ARGS,
+} from '../utils';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+
+//@ts-expect-error - clifty is ESM only
+import { KEYS, withEnv } from 'clifty';
+
+describe('Sveltekit with instrumentation and tracing', () => {
+ describe('without existing files', () => {
+ const projectDir = path.resolve(
+ __dirname,
+ '../test-applications/sveltekit-tracing-test-app',
+ );
+
+ const integration = Integration.sveltekit;
+ let wizardExitCode: number;
+
+ beforeAll(async () => {
+ initGit(projectDir);
+ revertLocalChanges(projectDir);
+
+ wizardExitCode = await withEnv({
+ cwd: projectDir,
+ })
+ .defineInteraction()
+ .expectOutput(
+ 'The Sentry SvelteKit Wizard will help you set up Sentry for your application',
+ )
+ .step('package installation', ({ expectOutput, whenAsked }) => {
+ whenAsked('Please select your package manager.').respondWith(
+ KEYS.DOWN,
+ KEYS.ENTER,
+ );
+ expectOutput('Installing @sentry/sveltekit');
+ })
+ .step('SDK setup', ({ whenAsked }) => {
+ whenAsked('Do you want to enable Tracing', {
+ timeout: 90_000, // package installation can take a while in CI
+ }).respondWith(KEYS.ENTER);
+ whenAsked('Do you want to enable Session Replay').respondWith(
+ KEYS.ENTER,
+ );
+ whenAsked('Do you want to enable Logs').respondWith(KEYS.ENTER);
+ })
+ .whenAsked('Do you want to create an example page')
+ .respondWith(KEYS.ENTER)
+ .whenAsked(
+ 'Optionally add a project-scoped MCP server configuration for the Sentry MCP?',
+ )
+ .respondWith(KEYS.DOWN, KEYS.ENTER)
+ .expectOutput('Successfully installed the Sentry SvelteKit SDK!')
+ .run(getWizardCommand(integration));
+ });
+
+ afterAll(() => {
+ revertLocalChanges(projectDir);
+ cleanupGit(projectDir);
+ });
+
+ it('exits with exit code 0', () => {
+ expect(wizardExitCode).toBe(0);
+ });
+
+ it('adds the SDK dependency to package.json', () => {
+ checkPackageJson(projectDir, integration);
+ });
+
+ it('adds the .env.sentry-build-plugin', () => {
+ checkEnvBuildPlugin(projectDir);
+ });
+
+ it('adds the example page', () => {
+ checkFileExists(
+ path.resolve(projectDir, 'src/routes/sentry-example-page/+page.svelte'),
+ );
+ checkFileExists(
+ path.resolve(projectDir, 'src/routes/sentry-example-page/+server.js'),
+ );
+ });
+
+ it('adds the sentry plugin to vite.config.ts', () => {
+ const viteConfig = fs.readFileSync(
+ path.resolve(projectDir, 'vite.config.ts'),
+ );
+ expect(viteConfig.toString()).toMatchInlineSnapshot(`
+ "import { sentrySvelteKit } from "@sentry/sveltekit";
+ import { sveltekit } from '@sveltejs/kit/vite';
+ import { defineConfig } from 'vite';
+
+ export default defineConfig({
+ plugins: [sentrySvelteKit({
+ sourceMapsUploadOptions: {
+ org: "${TEST_ARGS.ORG_SLUG}",
+ project: "${TEST_ARGS.PROJECT_SLUG}"
+ }
+ }), sveltekit()]
+ });"
+ `);
+ });
+
+ it('creates the hook files', () => {
+ const clientHooks = fs.readFileSync(
+ path.resolve(projectDir, 'src/hooks.client.ts'),
+ );
+ const serverHooks = fs.readFileSync(
+ path.resolve(projectDir, 'src/hooks.server.ts'),
+ );
+
+ expect(clientHooks.toString()).toMatchInlineSnapshot(`
+ "import { handleErrorWithSentry, replayIntegration } from "@sentry/sveltekit";
+ import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: '${TEST_ARGS.PROJECT_DSN}',
+
+ tracesSampleRate: 1.0,
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // This sets the sample rate to be 10%. You may want this to be 100% while
+ // in development and sample at a lower rate in production
+ replaysSessionSampleRate: 0.1,
+
+ // If the entire session is not sampled, use the below sample rate to sample
+ // sessions when an error occurs.
+ replaysOnErrorSampleRate: 1.0,
+
+ // If you don't want to use Session Replay, just remove the line below:
+ integrations: [replayIntegration()],
+ });
+
+ // If you have a custom error handler, pass it to \`handleErrorWithSentry\`
+ export const handleError = handleErrorWithSentry();
+ "
+ `);
+
+ expect(serverHooks.toString()).toMatchInlineSnapshot(`
+ "import {sequence} from "@sveltejs/kit/hooks";
+ import * as Sentry from "@sentry/sveltekit";
+ export const handle = sequence(Sentry.sentryHandle(), async ({ event, resolve }) => {
+ const response = await resolve(event);
+ return response;
+ });
+ export const handleError = Sentry.handleErrorWithSentry();"
+ `);
+ });
+
+ it('creates the insturmentation.server file', () => {
+ const instrumentationServer = fs.readFileSync(
+ path.resolve(projectDir, 'src/instrumentation.server.ts'),
+ );
+
+ expect(instrumentationServer.toString()).toMatchInlineSnapshot(`
+ "import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: '${TEST_ARGS.PROJECT_DSN}',
+
+ tracesSampleRate: 1.0,
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+ });"
+ `);
+ });
+
+ it('enables tracing and instrumentation in svelte.config.js', () => {
+ const svelteConfig = fs.readFileSync(
+ path.resolve(projectDir, 'svelte.config.js'),
+ );
+ expect(svelteConfig.toString()).toMatchInlineSnapshot(`
+ "import adapter from '@sveltejs/adapter-node';
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+ /** @type {import('@sveltejs/kit').Config} */
+ const config = {
+ // Consult https://svelte.dev/docs/kit/integrations#preprocessors
+ // 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(),
+ experimental: {
+ remoteFunctions: true,
+
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };
+
+ export default config;"
+ `);
+ });
+
+ // checkSvelteKitProject(projectDir, integration);
+ it('builds successfully', async () => {
+ await checkIfBuilds(projectDir);
+ });
+
+ it('runs on dev mode correctly', async () => {
+ await checkIfRunsOnDevMode(projectDir, 'ready in');
+ });
+
+ it('runs on prod mode correctly', async () => {
+ await checkIfRunsOnProdMode(projectDir, 'to expose', 'preview');
+ });
+ });
+});
diff --git a/src/sveltekit/sdk-example.ts b/src/sveltekit/sdk-example.ts
index fc5d01dbe..1b309d5ef 100644
--- a/src/sveltekit/sdk-example.ts
+++ b/src/sveltekit/sdk-example.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
// @ts-expect-error - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
-import { PartialSvelteConfig } from './sdk-setup';
+import { PartialBackwardsForwardsCompatibleSvelteConfig } from './sdk-setup/svelte-config';
import {
getSentryExampleApiRoute,
getSentryExampleSveltePage,
@@ -13,7 +13,7 @@ import {
* Creates example page and API route to test Sentry
*/
export async function createExamplePage(
- svelteConfig: PartialSvelteConfig,
+ svelteConfig: PartialBackwardsForwardsCompatibleSvelteConfig,
projectProps: {
selfHosted: boolean;
url: string;
diff --git a/src/sveltekit/sdk-setup.ts b/src/sveltekit/sdk-setup/setup.ts
similarity index 69%
rename from src/sveltekit/sdk-setup.ts
rename to src/sveltekit/sdk-setup/setup.ts
index dacacabd9..ae3fac83d 100644
--- a/src/sveltekit/sdk-setup.ts
+++ b/src/sveltekit/sdk-setup/setup.ts
@@ -1,7 +1,6 @@
import type { ExportNamedDeclaration, Program } from '@babel/types';
import * as fs from 'fs';
import * as path from 'path';
-import * as url from 'url';
import chalk from 'chalk';
import * as Sentry from '@sentry/node';
@@ -11,48 +10,35 @@ import clack from '@clack/prompts';
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
import type { ProxifiedModule } from 'magicast';
// @ts-expect-error - magicast is ESM and TS complains about that. It works though
-import { builders, generateCode, loadFile, parseModule } from 'magicast';
-// @ts-expect-error - magicast is ESM and TS complains about that. It works though
-import { addVitePlugin } from 'magicast/helpers';
-import { getClientHooksTemplate, getServerHooksTemplate } from './templates';
+import { builders, generateCode, loadFile } from 'magicast';
+import {
+ getClientHooksTemplate,
+ getInstrumentationServerTemplate,
+ getServerHooksTemplate,
+} from '../templates';
import {
- abortIfCancelled,
featureSelectionPrompt,
isUsingTypeScript,
-} from '../utils/clack';
-import { debug } from '../utils/debug';
-import { findFile, hasSentryContent } from '../utils/ast-utils';
+ showCopyPasteInstructions,
+} from '../../utils/clack';
+import { findFile, hasSentryContent } from '../../utils/ast-utils';
import * as recast from 'recast';
import x = recast.types;
import t = x.namedTypes;
-import { traceStep } from '../telemetry';
-
-const SVELTE_CONFIG_FILE = 'svelte.config.js';
-
-export type PartialSvelteConfig = {
- kit?: {
- files?: {
- hooks?: {
- client?: string;
- server?: string;
- };
- routes?: string;
- };
- };
-};
-
-type ProjectInfo = {
- dsn: string;
- org: string;
- project: string;
- selfHosted: boolean;
- url: string;
-};
+import {
+ enableTracingAndInstrumentation,
+ type PartialBackwardsForwardsCompatibleSvelteConfig,
+} from './svelte-config';
+import { ProjectInfo } from './types';
+import { modifyViteConfig } from './vite';
+import { modifyAndRecordFail } from './utils';
+import { debug } from '../../utils/debug';
export async function createOrMergeSvelteKitFiles(
projectInfo: ProjectInfo,
- svelteConfig: PartialSvelteConfig,
+ svelteConfig: PartialBackwardsForwardsCompatibleSvelteConfig,
+ setupForSvelteKitTracing: boolean,
): Promise {
const selectedFeatures = await featureSelectionPrompt([
{
@@ -83,6 +69,9 @@ export async function createOrMergeSvelteKitFiles(
// full file paths with correct file ending (or undefined if not found)
const originalClientHooksFile = findFile(clientHooksPath);
const originalServerHooksFile = findFile(serverHooksPath);
+ const originalInstrumentationServerFile = findFile(
+ path.resolve(process.cwd(), 'src', 'instrumentation.server'),
+ );
const viteConfig = findFile(path.resolve(process.cwd(), 'vite.config'));
@@ -90,45 +79,85 @@ export async function createOrMergeSvelteKitFiles(
const { dsn } = projectInfo;
+ if (setupForSvelteKitTracing) {
+ await enableTracingAndInstrumentation(
+ svelteConfig,
+ selectedFeatures.performance,
+ );
+
+ try {
+ if (!originalInstrumentationServerFile) {
+ await createNewInstrumentationServerFile(dsn, selectedFeatures);
+ } else {
+ await mergeInstrumentationServerFile(
+ originalInstrumentationServerFile,
+ dsn,
+ selectedFeatures,
+ );
+ }
+ } catch (e) {
+ clack.log.warn(
+ `Failed to automatically set up ${chalk.cyan(
+ `instrumentation.server.${
+ fileEnding ?? isUsingTypeScript() ? 'ts' : 'js'
+ }`,
+ )}.`,
+ );
+ debug(e);
+
+ await showCopyPasteInstructions({
+ codeSnippet: getInstrumentationServerTemplate(dsn, selectedFeatures),
+ filename: `instrumentation.server.${
+ fileEnding ?? isUsingTypeScript() ? 'ts' : 'js'
+ }`,
+ });
+
+ Sentry.setTag('created-instrumentation-server', 'fail');
+ }
+ }
+
Sentry.setTag(
- 'client-hooks-file-strategy',
- originalClientHooksFile ? 'merge' : 'create',
+ 'server-hooks-file-strategy',
+ originalServerHooksFile ? 'merge' : 'create',
);
- if (!originalClientHooksFile) {
- clack.log.info('No client hooks file found, creating a new one.');
+
+ if (!originalServerHooksFile) {
await createNewHooksFile(
- `${clientHooksPath}.${fileEnding}`,
- 'client',
+ `${serverHooksPath}.${fileEnding}`,
+ 'server',
dsn,
selectedFeatures,
+ !setupForSvelteKitTracing,
);
} else {
await mergeHooksFile(
- originalClientHooksFile,
- 'client',
+ originalServerHooksFile,
+ 'server',
dsn,
selectedFeatures,
+ !setupForSvelteKitTracing,
);
}
Sentry.setTag(
- 'server-hooks-file-strategy',
- originalServerHooksFile ? 'merge' : 'create',
+ 'client-hooks-file-strategy',
+ originalClientHooksFile ? 'merge' : 'create',
);
- if (!originalServerHooksFile) {
- clack.log.info('No server hooks file found, creating a new one.');
+ if (!originalClientHooksFile) {
await createNewHooksFile(
- `${serverHooksPath}.${fileEnding}`,
- 'server',
+ `${clientHooksPath}.${fileEnding}`,
+ 'client',
dsn,
selectedFeatures,
+ true,
);
} else {
await mergeHooksFile(
- originalServerHooksFile,
- 'server',
+ originalClientHooksFile,
+ 'client',
dsn,
selectedFeatures,
+ true,
);
}
@@ -141,7 +170,9 @@ export async function createOrMergeSvelteKitFiles(
* Attempts to read the svelte.config.js file to find the location of the hooks files.
* If users specified a custom location, we'll use that. Otherwise, we'll use the default.
*/
-function getHooksConfigDirs(svelteConfig: PartialSvelteConfig): {
+function getHooksConfigDirs(
+ svelteConfig: PartialBackwardsForwardsCompatibleSvelteConfig,
+): {
clientHooksPath: string;
serverHooksPath: string;
} {
@@ -176,11 +207,12 @@ async function createNewHooksFile(
replay: boolean;
logs: boolean;
},
+ setupForSvelteKitTracing: boolean,
): Promise {
const filledTemplate =
hooktype === 'client'
? getClientHooksTemplate(dsn, selectedFeatures)
- : getServerHooksTemplate(dsn, selectedFeatures);
+ : getServerHooksTemplate(dsn, selectedFeatures, setupForSvelteKitTracing);
await fs.promises.mkdir(path.dirname(hooksFileDest), { recursive: true });
await fs.promises.writeFile(hooksFileDest, filledTemplate);
@@ -189,6 +221,38 @@ async function createNewHooksFile(
Sentry.setTag(`created-${hooktype}-hooks`, 'success');
}
+async function createNewInstrumentationServerFile(
+ dsn: string,
+ selectedFeatures: {
+ performance: boolean;
+ logs: boolean;
+ },
+): Promise {
+ const filledTemplate = getInstrumentationServerTemplate(
+ dsn,
+ selectedFeatures,
+ );
+
+ const fileEnding = isUsingTypeScript() ? 'ts' : 'js';
+
+ const instrumentationServerFile = path.resolve(
+ process.cwd(),
+ 'src',
+ `instrumentation.server.${fileEnding}`,
+ );
+
+ await fs.promises.mkdir(path.dirname(instrumentationServerFile), {
+ recursive: true,
+ });
+
+ await fs.promises.writeFile(instrumentationServerFile, filledTemplate);
+
+ clack.log.success(
+ `Created ${chalk.cyan(path.basename(instrumentationServerFile))}`,
+ );
+ Sentry.setTag('created-instrumentation-server', 'success');
+}
+
/**
* Merges the users' hooks file with Sentry-related code.
*
@@ -209,6 +273,7 @@ async function mergeHooksFile(
replay: boolean;
logs: boolean;
},
+ includeSentryInit: boolean,
): Promise {
const originalHooksMod = await loadFile(hooksFile);
@@ -239,17 +304,19 @@ Skipping adding Sentry functionality to.`,
file,
);
- await modifyAndRecordFail(
- () => {
- if (hookType === 'client') {
- insertClientInitCall(dsn, originalHooksMod, selectedFeatures);
- } else {
- insertServerInitCall(dsn, originalHooksMod, selectedFeatures);
- }
- },
- 'init-call-injection',
- file,
- );
+ if (hookType === 'client' || includeSentryInit) {
+ await modifyAndRecordFail(
+ () => {
+ if (hookType === 'client') {
+ insertClientInitCall(dsn, originalHooksMod, selectedFeatures);
+ } else {
+ insertServerInitCall(dsn, originalHooksMod, selectedFeatures);
+ }
+ },
+ 'init-call-injection',
+ file,
+ );
+ }
await modifyAndRecordFail(
() => wrapHandleError(originalHooksMod),
@@ -278,6 +345,79 @@ Skipping adding Sentry functionality to.`,
Sentry.setTag(`modified-${hookType}-hooks`, 'success');
}
+/**
+ * Merges the users' instrumentation.server file with Sentry-related code.
+ *
+ * Both hooks:
+ * - add import * as Sentry
+ * - add Sentry.init
+ * - add handleError hook wrapper
+ *
+ * Additionally in Server hook:
+ * - add handle hook handler
+ */
+async function mergeInstrumentationServerFile(
+ instrumentationServerFilePath: string,
+ dsn: string,
+ selectedFeatures: {
+ performance: boolean;
+ replay: boolean;
+ logs: boolean;
+ },
+): Promise {
+ const originalInstrumentationServerMod = await loadFile(
+ instrumentationServerFilePath,
+ );
+ const filename = path.basename(instrumentationServerFilePath);
+
+ if (hasSentryContent(originalInstrumentationServerMod.$ast as t.Program)) {
+ // We don't want to mess with files that already have Sentry content.
+ // Let's just bail out at this point.
+ clack.log.warn(
+ `File ${chalk.cyan(filename)} already contains Sentry code.
+Skipping adding Sentry functionality to it.`,
+ );
+ Sentry.setTag(`modified-instrumentation-server`, 'fail');
+ Sentry.setTag(`instrumentation-server-fail-reason`, 'has-sentry-content');
+ return;
+ }
+
+ await modifyAndRecordFail(
+ () =>
+ originalInstrumentationServerMod.imports.$add({
+ from: '@sentry/sveltekit',
+ imported: '*',
+ local: 'Sentry',
+ }),
+ 'import-injection',
+ 'instrumentation-server',
+ );
+
+ await modifyAndRecordFail(
+ () => {
+ insertServerInitCall(
+ dsn,
+ originalInstrumentationServerMod,
+ selectedFeatures,
+ );
+ },
+ 'init-call-injection',
+ 'instrumentation-server',
+ );
+
+ await modifyAndRecordFail(
+ async () => {
+ const modifiedCode = originalInstrumentationServerMod.generate().code;
+ await fs.promises.writeFile(instrumentationServerFilePath, modifiedCode);
+ },
+ 'write-file',
+ 'instrumentation-server',
+ );
+
+ clack.log.success(`Added Sentry.init code to ${chalk.cyan(filename)}`);
+ Sentry.setTag(`modified-instrumentation-server`, 'success');
+}
+
function insertClientInitCall(
dsn: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -343,7 +483,7 @@ function insertClientInitCall(
function insertServerInitCall(
dsn: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- originalHooksMod: ProxifiedModule,
+ originalMod: ProxifiedModule,
selectedFeatures: {
performance: boolean;
logs: boolean;
@@ -369,11 +509,11 @@ function insertServerInitCall(
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const initCall = builders.functionCall('Sentry.init', initArgs);
- const originalHooksModAST = originalHooksMod.$ast as Program;
+ const originalModAST = originalMod.$ast as Program;
- const initCallInsertionIndex = getInitCallInsertionIndex(originalHooksModAST);
+ const initCallInsertionIndex = getInitCallInsertionIndex(originalModAST);
- originalHooksModAST.body.splice(
+ originalModAST.body.splice(
initCallInsertionIndex,
0,
// @ts-expect-error - string works here because the AST is proxified by magicast
@@ -501,191 +641,18 @@ function wrapHandle(mod: ProxifiedModule): void {
}
}
-export async function loadSvelteConfig(): Promise {
- const configFilePath = path.join(process.cwd(), SVELTE_CONFIG_FILE);
-
- try {
- if (!fs.existsSync(configFilePath)) {
- return {};
- }
-
- const configUrl = url.pathToFileURL(configFilePath).href;
- const svelteConfigModule = (await import(configUrl)) as {
- default: PartialSvelteConfig;
- };
-
- return svelteConfigModule?.default || {};
- } catch (e: unknown) {
- clack.log.error(`Couldn't load ${SVELTE_CONFIG_FILE}.
-Please make sure, you're running this wizard with Node 16 or newer`);
- clack.log.info(
- chalk.dim(
- typeof e === 'object' && e != null && 'toString' in e
- ? e.toString()
- : typeof e === 'string'
- ? e
- : 'Unknown error',
- ),
- );
-
- return {};
- }
-}
-
-async function modifyViteConfig(
- viteConfigPath: string,
- projectInfo: ProjectInfo,
-): Promise {
- const viteConfigContent = (
- await fs.promises.readFile(viteConfigPath, 'utf-8')
- ).toString();
-
- const { org, project, url, selfHosted } = projectInfo;
-
- const prettyViteConfigFilename = chalk.cyan(path.basename(viteConfigPath));
-
- try {
- const viteModule = parseModule(viteConfigContent);
-
- if (hasSentryContent(viteModule.$ast as t.Program)) {
- clack.log.warn(
- `File ${prettyViteConfigFilename} already contains Sentry code.
-Skipping adding Sentry functionality to.`,
- );
- Sentry.setTag(`modified-vite-cfg`, 'fail');
- Sentry.setTag(`vite-cfg-fail-reason`, 'has-sentry-content');
- return;
- }
-
- await modifyAndRecordFail(
- () =>
- addVitePlugin(viteModule, {
- imported: 'sentrySvelteKit',
- from: '@sentry/sveltekit',
- constructor: 'sentrySvelteKit',
- options: {
- sourceMapsUploadOptions: {
- org,
- project,
- ...(selfHosted && { url }),
- },
- },
- index: 0,
- }),
- 'add-vite-plugin',
- 'vite-cfg',
- );
-
- await modifyAndRecordFail(
- async () => {
- const code = generateCode(viteModule.$ast).code;
- await fs.promises.writeFile(viteConfigPath, code);
- },
- 'write-file',
- 'vite-cfg',
- );
- } catch (e) {
- debug(e);
- await showFallbackViteCopyPasteSnippet(
- viteConfigPath,
- getViteConfigCodeSnippet(org, project, selfHosted, url),
- );
- Sentry.captureException('Sveltekit Vite Config Modification Fail');
- }
-
- clack.log.success(`Added Sentry code to ${prettyViteConfigFilename}`);
- Sentry.setTag(`modified-vite-cfg`, 'success');
-}
-
-async function showFallbackViteCopyPasteSnippet(
- viteConfigPath: string,
- codeSnippet: string,
-) {
- const viteConfigFilename = path.basename(viteConfigPath);
-
- clack.log.warning(
- `Couldn't automatically modify your ${chalk.cyan(viteConfigFilename)}
-${chalk.dim(`This sometimes happens when we encounter more complex vite configs.
-It may not seem like it but sometimes our magical powers are limited ;)`)}`,
- );
-
- clack.log.info("But don't worry - it's super easy to do this yourself!");
-
- clack.log.step(
- `Add the following code to your ${chalk.cyan(viteConfigFilename)}:`,
- );
-
- // Intentionally logging to console here for easier copy/pasting
- // eslint-disable-next-line no-console
- console.log(codeSnippet);
-
- await abortIfCancelled(
- clack.select({
- message: 'Did you copy the snippet above?',
- options: [
- { label: 'Yes!', value: true, hint: "Great, that's already it!" },
- ],
- initialValue: true,
- }),
- );
-}
-
-const getViteConfigCodeSnippet = (
- org: string,
- project: string,
- selfHosted: boolean,
- url: string,
-) =>
- chalk.gray(`
-import { sveltekit } from '@sveltejs/kit/vite';
-import { defineConfig } from 'vite';
-${chalk.greenBright("import { sentrySvelteKit } from '@sentry/sveltekit'")}
-
-export default defineConfig({
- plugins: [
- // Make sure \`sentrySvelteKit\` is registered before \`sveltekit\`
- ${chalk.greenBright(`sentrySvelteKit({
- sourceMapsUploadOptions: {
- org: '${org}',
- project: '${project}',${selfHosted ? `\n url: '${url}',` : ''}
- }
- }),`)}
- sveltekit(),
- ]
-});
-`);
-
/**
* We want to insert the init call on top of the file but after all import statements
*/
-function getInitCallInsertionIndex(originalHooksModAST: Program): number {
+function getInitCallInsertionIndex(originalModAST: Program): number {
// We need to deep-copy here because reverse mutates in place
- const copiedBodyNodes = [...originalHooksModAST.body];
+ const copiedBodyNodes = [...originalModAST.body];
const lastImportDeclaration = copiedBodyNodes
.reverse()
.find((node) => node.type === 'ImportDeclaration');
const initCallInsertionIndex = lastImportDeclaration
- ? originalHooksModAST.body.indexOf(lastImportDeclaration) + 1
+ ? originalModAST.body.indexOf(lastImportDeclaration) + 1
: 0;
return initCallInsertionIndex;
}
-
-/**
- * Applies the @param modifyCallback and records Sentry tags if the call failed.
- * In case of a failure, a tag is set with @param reason as a fail reason
- * and the error is rethrown.
- */
-async function modifyAndRecordFail(
- modifyCallback: () => T | Promise,
- reason: string,
- fileType: 'server-hooks' | 'client-hooks' | 'vite-cfg',
-): Promise {
- try {
- await traceStep(`${fileType}-${reason}`, modifyCallback);
- } catch (e) {
- Sentry.setTag(`modified-${fileType}`, 'fail');
- Sentry.setTag(`${fileType}-mod-fail-reason`, reason);
- throw e;
- }
-}
diff --git a/src/sveltekit/sdk-setup/svelte-config.ts b/src/sveltekit/sdk-setup/svelte-config.ts
new file mode 100644
index 000000000..1b539d872
--- /dev/null
+++ b/src/sveltekit/sdk-setup/svelte-config.ts
@@ -0,0 +1,397 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import * as url from 'url';
+import chalk from 'chalk';
+import * as Sentry from '@sentry/node';
+import * as recast from 'recast';
+import x = recast.types;
+import t = x.namedTypes;
+
+//@ts-expect-error - clack is ESM and TS complains about that. It works though
+import clack from '@clack/prompts';
+import { makeCodeSnippet, showCopyPasteInstructions } from '../../utils/clack';
+// @ts-expect-error - magicast is ESM and TS complains about that. It works though
+import { generateCode, parseModule, ProxifiedModule } from 'magicast';
+import { debug } from '../../utils/debug';
+
+const SVELTE_CONFIG_FILE = 'svelte.config.js';
+
+const b = recast.types.builders;
+
+export type PartialBackwardsForwardsCompatibleSvelteConfig = {
+ kit?: {
+ files?: {
+ hooks?: {
+ client?: string;
+ server?: string;
+ };
+ routes?: string;
+ };
+ experimental?: {
+ tracing?: {
+ server?: boolean;
+ };
+ instrumentation?: {
+ server?: boolean;
+ };
+ };
+ };
+};
+
+export async function loadSvelteConfig(): Promise {
+ const configFilePath = path.join(process.cwd(), SVELTE_CONFIG_FILE);
+
+ try {
+ if (!fs.existsSync(configFilePath)) {
+ return {};
+ }
+
+ const configUrl = url.pathToFileURL(configFilePath).href;
+ const svelteConfigModule = (await import(configUrl)) as {
+ default: PartialBackwardsForwardsCompatibleSvelteConfig;
+ };
+
+ return svelteConfigModule?.default || {};
+ } catch (e: unknown) {
+ clack.log.error(`Couldn't load ${chalk.cyan(SVELTE_CONFIG_FILE)}.
+Are you running this wizard from the root of your SvelteKit project?`);
+ clack.log.info(
+ chalk.dim(
+ typeof e === 'object' && e != null && 'toString' in e
+ ? e.toString()
+ : typeof e === 'string'
+ ? e
+ : 'Unknown error',
+ ),
+ );
+
+ return {};
+ }
+}
+
+export async function enableTracingAndInstrumentation(
+ originalSvelteConfig: PartialBackwardsForwardsCompatibleSvelteConfig,
+ enableTracing: boolean,
+) {
+ const hasTracingEnabled = originalSvelteConfig.kit?.experimental?.tracing;
+ const hasInstrumentationEnabled =
+ originalSvelteConfig.kit?.experimental?.instrumentation;
+
+ if (hasTracingEnabled && hasInstrumentationEnabled) {
+ clack.log.info('Tracing and instrumentation are already enabled.');
+ return;
+ }
+
+ if (hasTracingEnabled || hasInstrumentationEnabled) {
+ clack.log.info(
+ 'Tracing and instrumentation are partially enabled. Make sure both options are enabled.',
+ );
+ await showFallbackConfigSnippet();
+ return;
+ } else {
+ try {
+ const configPath = path.join(process.cwd(), SVELTE_CONFIG_FILE);
+ const svelteConfigContent = await fs.promises.readFile(
+ configPath,
+ 'utf-8',
+ );
+
+ const { error, result } = _enableTracingAndInstrumentationInConfig(
+ svelteConfigContent,
+ enableTracing,
+ );
+
+ if (error) {
+ clack.log.warning(
+ 'Failed to automatically enable SvelteKit tracing and instrumentation.',
+ );
+ debug(error);
+ Sentry.captureException(error);
+ await showFallbackConfigSnippet();
+ return;
+ }
+
+ if (result) {
+ await fs.promises.writeFile(configPath, result);
+ }
+
+ clack.log.success(
+ `Enabled tracing and instrumentation in ${chalk.cyan(
+ SVELTE_CONFIG_FILE,
+ )}`,
+ );
+ } catch (e) {
+ clack.log.error(
+ `Failed to enable tracing and instrumentation in ${chalk.cyan(
+ SVELTE_CONFIG_FILE,
+ )}.`,
+ );
+ debug(e);
+ Sentry.captureException(
+ `Failed to enable tracing and instrumentation in ${SVELTE_CONFIG_FILE}`,
+ );
+ await showFallbackConfigSnippet();
+ return;
+ }
+ }
+}
+
+export function _enableTracingAndInstrumentationInConfig(
+ config: string,
+ enableTracing: boolean,
+): {
+ result?: string;
+ error?: string;
+} {
+ let svelteConfig: ProxifiedModule;
+ try {
+ svelteConfig = parseModule(config);
+ } catch (e) {
+ return {
+ error: 'Failed to parse Svelte config',
+ };
+ }
+
+ let configObject: t.ObjectExpression | undefined = undefined;
+
+ // Cases to handle for finding the config object:
+ // 1. default export is named object
+ // 2. default export is in-place object
+ // 3. default export is an identifier, so look up the variable declaration
+ recast.visit(svelteConfig.$ast, {
+ visitExportDefaultDeclaration(path) {
+ const exportDeclarationNode = path.node;
+ if (
+ exportDeclarationNode.declaration.type === 'AssignmentExpression' &&
+ exportDeclarationNode.declaration.right.type === 'ObjectExpression'
+ ) {
+ configObject = exportDeclarationNode.declaration.right;
+ return false;
+ }
+
+ if (exportDeclarationNode.declaration.type === 'ObjectExpression') {
+ configObject = exportDeclarationNode.declaration;
+ return false;
+ }
+
+ if (exportDeclarationNode.declaration.type === 'Identifier') {
+ const identifierName = exportDeclarationNode.declaration.name;
+ recast.visit(svelteConfig.$ast, {
+ visitVariableDeclarator(path) {
+ if (
+ path.node.id?.type === 'Identifier' &&
+ path.node.id.name === identifierName &&
+ path.node.init?.type === 'ObjectExpression'
+ ) {
+ configObject = path.node.init;
+ return false;
+ }
+
+ this.traverse(path);
+ },
+ });
+ }
+
+ this.traverse(path);
+ },
+ });
+
+ if (!_isValidConfigObject(configObject)) {
+ return {
+ error: "Couldn't find the config object",
+ };
+ }
+
+ // This type cast is safe. For some reason, TS still assumes that `configObject`
+ // is `undefined` so we have to tell it that it's not (see check above)
+ const validatedConfigObject =
+ configObject as recast.types.namedTypes.ObjectExpression;
+
+ const kitProp = validatedConfigObject.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'kit',
+ );
+
+ if (!kitProp || kitProp.type !== 'ObjectProperty') {
+ return {
+ error: "Couldn't find the `kit` property",
+ };
+ }
+
+ if (kitProp.value.type !== 'ObjectExpression') {
+ return {
+ error: `\`kit\` property has unexpected type: ${kitProp.value.type}`,
+ };
+ }
+
+ // 1. find or add `kit.experimental` property
+ // type-cast because TS can't infer the type in `.find` :(
+ const kitExperimentalProp = kitProp.value.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'experimental',
+ ) as t.ObjectProperty | undefined;
+
+ let experimentalObject: t.ObjectExpression;
+
+ if (kitExperimentalProp) {
+ if (kitExperimentalProp.value.type !== 'ObjectExpression') {
+ return {
+ error: `Property \`kit.experimental\` has unexpected type: ${kitExperimentalProp.value.type}`,
+ };
+ }
+
+ experimentalObject = kitExperimentalProp.value;
+ } else {
+ experimentalObject = b.objectExpression([]);
+ kitProp.value.properties.push(
+ b.objectProperty(b.identifier('experimental'), experimentalObject),
+ );
+ }
+
+ // 2. find or add `kit.experimental.tracing` property
+ // find or add `kit.experimental.instrumentation` property
+ const kitExperimentalTraingProp = experimentalObject.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'tracing',
+ ) as t.ObjectProperty | undefined;
+
+ const kitExperimentalInstrumentationProp = experimentalObject.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'instrumentation',
+ ) as t.ObjectProperty | undefined;
+
+ let experimentalTracingObject: t.ObjectExpression;
+ let experimentalInstrumentationObject: t.ObjectExpression;
+
+ if (kitExperimentalTraingProp) {
+ if (kitExperimentalTraingProp.value.type !== 'ObjectExpression') {
+ return {
+ error: `Property \`kit.experimental.tracing\` has unexpected type: ${kitExperimentalTraingProp.value.type}`,
+ };
+ }
+
+ experimentalTracingObject = kitExperimentalTraingProp.value;
+ } else {
+ experimentalTracingObject = b.objectExpression([]);
+ experimentalObject.properties.push(
+ b.objectProperty(b.identifier('tracing'), experimentalTracingObject),
+ );
+ }
+
+ if (kitExperimentalInstrumentationProp) {
+ if (kitExperimentalInstrumentationProp.value.type !== 'ObjectExpression') {
+ return {
+ error: `Property \`kit.experimental.instrumentation\` has unexpected type: ${kitExperimentalInstrumentationProp.value.type}`,
+ };
+ }
+
+ experimentalInstrumentationObject =
+ kitExperimentalInstrumentationProp.value;
+ } else {
+ experimentalInstrumentationObject = b.objectExpression([]);
+ experimentalObject.properties.push(
+ b.objectProperty(
+ b.identifier('instrumentation'),
+ experimentalInstrumentationObject,
+ ),
+ );
+ }
+
+ // 3. find or add `kit.experimental.tracing.server` property
+ // find or add `kit.experimental.instrumentation.server` property
+ const kitExperimentalTracingSeverProp =
+ experimentalTracingObject.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'server',
+ ) as t.ObjectProperty | undefined;
+
+ const kitExperimentalInstrumentationSeverProp =
+ experimentalInstrumentationObject.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.key.name === 'server',
+ ) as t.ObjectProperty | undefined;
+
+ if (kitExperimentalTracingSeverProp) {
+ if (kitExperimentalTracingSeverProp.value.type !== 'BooleanLiteral') {
+ return {
+ error: `Property \`kit.experimental.tracing.server\` has unexpected type: ${kitExperimentalTracingSeverProp.value.type}`,
+ };
+ }
+
+ kitExperimentalTracingSeverProp.value = b.booleanLiteral(enableTracing);
+ } else {
+ experimentalTracingObject.properties.push(
+ b.objectProperty(b.identifier('server'), b.booleanLiteral(enableTracing)),
+ );
+ }
+
+ if (kitExperimentalInstrumentationSeverProp) {
+ if (
+ kitExperimentalInstrumentationSeverProp.value.type !== 'BooleanLiteral'
+ ) {
+ return {
+ error: `Property \`kit.experimental.instrumentation.server\` has unexpected type: ${kitExperimentalInstrumentationSeverProp.value.type}`,
+ };
+ }
+ kitExperimentalInstrumentationSeverProp.value = b.booleanLiteral(true);
+ } else {
+ experimentalInstrumentationObject.properties.push(
+ b.objectProperty(b.identifier('server'), b.booleanLiteral(true)),
+ );
+ }
+
+ try {
+ return {
+ result: generateCode(svelteConfig).code,
+ };
+ } catch (e) {
+ debug(e);
+ return {
+ error: 'Failed to generate code for Svelte config',
+ };
+ }
+}
+
+function _isValidConfigObject(
+ o: t.ObjectExpression | undefined,
+): o is t.ObjectExpression {
+ return !!o && o.type === 'ObjectExpression';
+}
+
+async function showFallbackConfigSnippet(): Promise {
+ const codeSnippet = makeCodeSnippet(true, (unchanged, plus) =>
+ unchanged(`const config = {
+preprocess: vitePreprocess(),
+
+kit: {
+ adapter: adapter(),
+ ${plus(`experimental: {
+ instrumentation: {
+ server: true,
+ },
+ tracing: {
+ server: true,
+ },
+ },`)}
+},
+};
+`),
+ );
+
+ await showCopyPasteInstructions({
+ filename: 'svelte.config.js',
+ codeSnippet,
+ });
+}
diff --git a/src/sveltekit/sdk-setup/types.ts b/src/sveltekit/sdk-setup/types.ts
new file mode 100644
index 000000000..cf606cbd9
--- /dev/null
+++ b/src/sveltekit/sdk-setup/types.ts
@@ -0,0 +1,7 @@
+export type ProjectInfo = {
+ dsn: string;
+ org: string;
+ project: string;
+ selfHosted: boolean;
+ url: string;
+};
diff --git a/src/sveltekit/sdk-setup/utils.ts b/src/sveltekit/sdk-setup/utils.ts
new file mode 100644
index 000000000..178b8fcba
--- /dev/null
+++ b/src/sveltekit/sdk-setup/utils.ts
@@ -0,0 +1,25 @@
+import { traceStep } from '../../telemetry';
+import * as Sentry from '@sentry/node';
+
+/**
+ * Applies the @param modifyCallback and records Sentry tags if the call failed.
+ * In case of a failure, a tag is set with @param reason as a fail reason
+ * and the error is rethrown.
+ */
+export async function modifyAndRecordFail(
+ modifyCallback: () => T | Promise,
+ reason: string,
+ fileType:
+ | 'server-hooks'
+ | 'client-hooks'
+ | 'vite-cfg'
+ | 'instrumentation-server',
+): Promise {
+ try {
+ await traceStep(`${fileType}-${reason}`, modifyCallback);
+ } catch (e) {
+ Sentry.setTag(`modified-${fileType}`, 'fail');
+ Sentry.setTag(`${fileType}-mod-fail-reason`, reason);
+ throw e;
+ }
+}
diff --git a/src/sveltekit/sdk-setup/vite.ts b/src/sveltekit/sdk-setup/vite.ts
new file mode 100644
index 000000000..ad0ce30f4
--- /dev/null
+++ b/src/sveltekit/sdk-setup/vite.ts
@@ -0,0 +1,144 @@
+import * as Sentry from '@sentry/node';
+import * as fs from 'fs';
+import * as path from 'path';
+import chalk from 'chalk';
+
+//@ts-expect-error - clack is ESM and TS complains about that. It works though
+import clack from '@clack/prompts';
+// @ts-expect-error - magicast is ESM and TS complains about that. It works though
+import { generateCode, parseModule } from 'magicast';
+// @ts-expect-error - magicast is ESM and TS complains about that. It works though
+import { addVitePlugin } from 'magicast/helpers';
+
+import * as recast from 'recast';
+import x = recast.types;
+import t = x.namedTypes;
+
+import { hasSentryContent } from '../../utils/ast-utils';
+import { debug } from '../../utils/debug';
+import { abortIfCancelled } from '../../utils/clack';
+import type { ProjectInfo } from './types';
+import { modifyAndRecordFail } from './utils';
+
+export async function modifyViteConfig(
+ viteConfigPath: string,
+ projectInfo: ProjectInfo,
+): Promise {
+ const viteConfigContent = (
+ await fs.promises.readFile(viteConfigPath, 'utf-8')
+ ).toString();
+
+ const { org, project, url, selfHosted } = projectInfo;
+
+ const prettyViteConfigFilename = chalk.cyan(path.basename(viteConfigPath));
+
+ try {
+ const viteModule = parseModule(viteConfigContent);
+
+ if (hasSentryContent(viteModule.$ast as t.Program)) {
+ clack.log.warn(
+ `File ${prettyViteConfigFilename} already contains Sentry code.
+Skipping adding Sentry functionality to.`,
+ );
+ Sentry.setTag(`modified-vite-cfg`, 'fail');
+ Sentry.setTag(`vite-cfg-fail-reason`, 'has-sentry-content');
+ return;
+ }
+
+ await modifyAndRecordFail(
+ () =>
+ addVitePlugin(viteModule, {
+ imported: 'sentrySvelteKit',
+ from: '@sentry/sveltekit',
+ constructor: 'sentrySvelteKit',
+ options: {
+ sourceMapsUploadOptions: {
+ org,
+ project,
+ ...(selfHosted && { url }),
+ },
+ },
+ index: 0,
+ }),
+ 'add-vite-plugin',
+ 'vite-cfg',
+ );
+
+ await modifyAndRecordFail(
+ async () => {
+ const code = generateCode(viteModule.$ast).code;
+ await fs.promises.writeFile(viteConfigPath, code);
+ },
+ 'write-file',
+ 'vite-cfg',
+ );
+ } catch (e) {
+ debug(e);
+ await showFallbackViteCopyPasteSnippet(
+ viteConfigPath,
+ getViteConfigCodeSnippet(org, project, selfHosted, url),
+ );
+ Sentry.captureException('Sveltekit Vite Config Modification Fail');
+ }
+
+ clack.log.success(`Added Sentry code to ${prettyViteConfigFilename}`);
+ Sentry.setTag(`modified-vite-cfg`, 'success');
+}
+
+async function showFallbackViteCopyPasteSnippet(
+ viteConfigPath: string,
+ codeSnippet: string,
+) {
+ const viteConfigFilename = path.basename(viteConfigPath);
+
+ clack.log.warning(
+ `Couldn't automatically modify your ${chalk.cyan(viteConfigFilename)}
+${chalk.dim(`This sometimes happens when we encounter more complex vite configs.
+It may not seem like it but sometimes our magical powers are limited ;)`)}`,
+ );
+
+ clack.log.info("But don't worry - it's super easy to do this yourself!");
+
+ clack.log.step(
+ `Add the following code to your ${chalk.cyan(viteConfigFilename)}:`,
+ );
+
+ // Intentionally logging to console here for easier copy/pasting
+ // eslint-disable-next-line no-console
+ console.log(codeSnippet);
+
+ await abortIfCancelled(
+ clack.select({
+ message: 'Did you copy the snippet above?',
+ options: [
+ { label: 'Yes!', value: true, hint: "Great, that's already it!" },
+ ],
+ initialValue: true,
+ }),
+ );
+}
+
+const getViteConfigCodeSnippet = (
+ org: string,
+ project: string,
+ selfHosted: boolean,
+ url: string,
+) =>
+ chalk.gray(`
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+${chalk.greenBright("import { sentrySvelteKit } from '@sentry/sveltekit'")}
+
+export default defineConfig({
+ plugins: [
+ // Make sure \`sentrySvelteKit\` is registered before \`sveltekit\`
+ ${chalk.greenBright(`sentrySvelteKit({
+ sourceMapsUploadOptions: {
+ org: '${org}',
+ project: '${project}',${selfHosted ? `\n url: '${url}',` : ''}
+ }
+ }),`)}
+ sveltekit(),
+ ]
+});
+`);
diff --git a/src/sveltekit/sveltekit-wizard.ts b/src/sveltekit/sveltekit-wizard.ts
index 95564afec..55cb488f0 100644
--- a/src/sveltekit/sveltekit-wizard.ts
+++ b/src/sveltekit/sveltekit-wizard.ts
@@ -24,7 +24,8 @@ import { NPM } from '../utils/package-manager';
import type { WizardOptions } from '../utils/types';
import { offerProjectScopedMcpConfig } from '../utils/clack/mcp-config';
import { createExamplePage } from './sdk-example';
-import { createOrMergeSvelteKitFiles, loadSvelteConfig } from './sdk-setup';
+import { createOrMergeSvelteKitFiles } from './sdk-setup/setup';
+import { loadSvelteConfig } from './sdk-setup/svelte-config';
import { getKitVersionBucket, getSvelteVersionBucket } from './utils';
export async function runSvelteKitWizard(
@@ -87,6 +88,58 @@ export async function runSvelteKitWizardWithTelemetry(
}
}
+ let setupForSvelteKitTracing = kitVersionBucket === '>=2.31.0';
+
+ if (kitVersionBucket !== '>=2.31.0') {
+ clack.log.warn(
+ `It seems you're using a SvelteKit version ${chalk.cyan(
+ '<2.31.0',
+ )} (detected ${chalk.cyan(kitVersion ?? 'unknown')}).
+
+We recommend upgrading SvelteKit to version ${chalk.cyan(
+ '>=2.31.0',
+ )} to use SvelteKit's builtin observability:
+${chalk.cyan('https://svelte.dev/docs/kit/observability')}
+
+Sentry works best with SvelteKit versions ${chalk.cyan('>=2.31.0')}.
+
+If you prefer, you can stay on your current version and use the Sentry SDK
+without SvelteKit's builtin observability.`,
+ );
+
+ const decision = await abortIfCancelled(
+ clack.select({
+ message: 'Do you want to continue anyway?',
+ options: [
+ {
+ label: "No, I'll upgrade SvelteKit first",
+ hint: 'Recommended',
+ value: 'exit-to-upgrade',
+ },
+ {
+ label: "I'm already on SvelteKit >=2.31.0",
+ hint: 'Sorry, my bad!',
+ value: 'install-with-kit-tracing',
+ },
+ {
+ label: 'Yes, continue',
+ hint: 'No Problem!',
+ value: 'install-without-kit-tracing',
+ },
+ ],
+ }),
+ );
+
+ if (decision === 'install-with-kit-tracing') {
+ setupForSvelteKitTracing = true;
+ }
+
+ if (decision === 'exit-to-upgrade') {
+ await abort('Exiting Wizard', 0);
+ return;
+ }
+ }
+
Sentry.setTag(
'svelte-version',
getSvelteVersionBucket(getPackageVersion('svelte', packageJson)),
@@ -123,6 +176,7 @@ export async function runSvelteKitWizardWithTelemetry(
url: sentryUrl,
},
svelteConfig,
+ setupForSvelteKitTracing,
),
);
} catch (e: unknown) {
@@ -190,7 +244,7 @@ async function buildOutroMessage(
): Promise {
const packageManager = await getPackageManager(NPM);
- let msg = chalk.green('\nSuccessfully installed the Sentry SvelteKit SDK!');
+ let msg = chalk.green('Successfully installed the Sentry SvelteKit SDK!');
if (shouldCreateExamplePage) {
msg += `\n\nYou can validate your setup by starting your dev environment (${chalk.cyan(
diff --git a/src/sveltekit/templates.ts b/src/sveltekit/templates.ts
index 5303a2a12..2f0c51c93 100644
--- a/src/sveltekit/templates.ts
+++ b/src/sveltekit/templates.ts
@@ -53,10 +53,10 @@ export function getServerHooksTemplate(
replay: boolean;
logs: boolean;
},
+ includeSentryInit: boolean,
) {
- return `import { sequence } from "@sveltejs/kit/hooks";
-import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit";
-import * as Sentry from '@sentry/sveltekit';
+ const sentryInit = includeSentryInit
+ ? `import * as Sentry from '@sentry/sveltekit';
Sentry.init({
dsn: '${dsn}',
@@ -76,7 +76,12 @@ ${
}
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: import.meta.env.DEV,
-});
+});`
+ : ``;
+
+ return `import { sequence } from "@sveltejs/kit/hooks";
+import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit";
+${sentryInit}
// If you have custom handlers, make sure to place them after \`sentryHandle()\` in the \`sequence\` function.
export const handle = sequence(sentryHandle());
@@ -86,6 +91,36 @@ export const handleError = handleErrorWithSentry();
`;
}
+export function getInstrumentationServerTemplate(
+ dsn: string,
+ selectedFeatures: {
+ performance: boolean;
+ logs: boolean;
+ },
+) {
+ return `import * as Sentry from '@sentry/sveltekit';
+
+Sentry.init({
+ dsn: '${dsn}',
+${
+ selectedFeatures.performance
+ ? `
+ tracesSampleRate: 1.0,
+`
+ : ''
+}
+${
+ selectedFeatures.logs
+ ? ` // Enable logs to be sent to Sentry
+ enableLogs: true,
+`
+ : ''
+}
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+});`;
+}
+
/**
* +page.svelte with Sentry example
*/
diff --git a/src/sveltekit/utils.ts b/src/sveltekit/utils.ts
index 54798dae7..1127dd9af 100644
--- a/src/sveltekit/utils.ts
+++ b/src/sveltekit/utils.ts
@@ -1,8 +1,16 @@
import { lt, minVersion } from 'semver';
+export type KitVersionBucket =
+ | 'none'
+ | 'invalid'
+ | '0.x'
+ | '>=1.0.0 <1.24.0'
+ | '>=1.24.0 <2.31.0'
+ | '>=2.31.0';
+
export function getKitVersionBucket(
version: string | undefined,
-): 'none' | 'invalid' | '0.x' | '>=1.0.0 <1.24.0' | '>=1.24.0' {
+): KitVersionBucket {
if (!version) {
return 'none';
}
@@ -16,11 +24,15 @@ export function getKitVersionBucket(
return '0.x';
} else if (lt(minVer, '1.24.0')) {
return '>=1.0.0 <1.24.0';
- } else {
+ } else if (lt(minVer, '2.31.0')) {
// This is the version when the client-side invalidation fix was released
// https://github.com/sveltejs/kit/releases/tag/%40sveltejs%2Fkit%401.24.0
// https://github.com/sveltejs/kit/pull/10576
- return '>=1.24.0';
+ return '>=1.24.0 <2.31.0';
+ } else {
+ // This is the version where sveltekit-native tracing and instrumentation was
+ // introduced as an experimental feature.
+ return '>=2.31.0';
}
}
diff --git a/test/sveltekit/sdk-setup/svelte-config.test.ts b/test/sveltekit/sdk-setup/svelte-config.test.ts
new file mode 100644
index 000000000..6c24c83b5
--- /dev/null
+++ b/test/sveltekit/sdk-setup/svelte-config.test.ts
@@ -0,0 +1,664 @@
+import { describe, it, expect } from 'vitest';
+import { _enableTracingAndInstrumentationInConfig } from '../../../src/sveltekit/sdk-setup/svelte-config';
+
+describe('_enableTracingAndInstrumentationInConfig', () => {
+ it('leaves already correct config unchanged', () => {
+ const originalConfig = `export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+};`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toBe(originalConfig);
+ });
+
+ describe('successfully handles', () => {
+ it('default config as variable declaration', () => {
+ const originalConfig = `/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ },
+};
+
+export default config;
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "/** @type {import('@sveltejs/kit').Config} */
+ const config = {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+
+ experimental: {
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };
+
+ export default config;"
+ `);
+ });
+
+ it('default config named declaration object', () => {
+ const originalConfig = `
+export default config = {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default config = {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+
+ experimental: {
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };"
+ `);
+ });
+
+ it('default config as in-place object', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+
+ experimental: {
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };"
+ `);
+ });
+
+ it('config with tracing disabled', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ false,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+
+ experimental: {
+ tracing: {
+ server: false,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing `kit.experimental` property', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing and empty `kit.experimental.tracing` property', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ },
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+
+ tracing: {
+ server: true,
+ },
+
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing and empty `kit.experimental.instrumentation` property', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ },
+ instrumentation: {
+ },
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing and filled `kit.experimental.(instrumentation|tracing).server` properties', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: false,
+ },
+ instrumentation: {
+ server: false,
+ },
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing and filled `kit.experimental.(instrumentation|tracing).server` properties with instrumentation disabled', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: false,
+ },
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+
+ it('config with pre-existing and filled `kit.experimental.(instrumentation|tracing).server` properties with tracing disabled', () => {
+ const originalConfig = `
+export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: false,
+ },
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+};
+`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.result).toMatchInlineSnapshot(`
+ "export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ remoteFunctions: true,
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: true,
+ },
+ }
+ },
+ };"
+ `);
+ });
+ });
+
+ describe('gracefully errors if', () => {
+ it('config object not found', () => {
+ const originalConfig = `console.log('hello')`;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe("Couldn't find the config object");
+ });
+
+ it('config is not an object', () => {
+ const originalConfig = `
+ export default getSvelteConfig();
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe("Couldn't find the config object");
+ });
+
+ it('`kit` property is missing', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe("Couldn't find the `kit` property");
+ });
+
+ it('`kit` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: getKitConfig(),
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ '`kit` property has unexpected type: CallExpression',
+ );
+ });
+
+ it('`kit.experimental` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: 'hello',
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ 'Property `kit.experimental` has unexpected type: StringLiteral',
+ );
+ });
+
+ it('`kit.experimental.tracing` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: true,
+ },
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ 'Property `kit.experimental.tracing` has unexpected type: BooleanLiteral',
+ );
+ });
+
+ it('`kit.experimental.instrumentation` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: {
+ server: true,
+ },
+ instrumentation: 'server',
+ },
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ 'Property `kit.experimental.instrumentation` has unexpected type: StringLiteral',
+ );
+ });
+
+ it('`kit.experimental.tracing.server` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: {
+ server: !!process.env['ENABLE_TRACING'],
+ },
+ instrumentation: {
+ server: true,
+ },
+ },
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ 'Property `kit.experimental.tracing.server` has unexpected type: UnaryExpression',
+ );
+ });
+
+ it('`kit.experimental.instrumentation.server` property has unexpected type', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: {
+ server: true,
+ },
+ instrumentation: {
+ server: 'hello',
+ },
+ },
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe(
+ 'Property `kit.experimental.instrumentation.server` has unexpected type: StringLiteral',
+ );
+ });
+
+ it('config parsing fails', () => {
+ const originalConfig = `
+ export default {
+ preprocess: vitePreprocess(),
+
+ kit: {
+ adapter: adapter(),
+ experimental: {
+ tracing: {
+ server: true,
+ }
+ instrumentation: {
+ server: 'hello',
+ },
+ },
+ },
+ };
+ `;
+
+ const modifiedConfig = _enableTracingAndInstrumentationInConfig(
+ originalConfig,
+ true,
+ );
+
+ expect(modifiedConfig.error).toBe('Failed to parse Svelte config');
+ });
+ });
+});
diff --git a/test/sveltekit/templates.test.ts b/test/sveltekit/templates.test.ts
index 4f5329d06..3d4b3bed2 100644
--- a/test/sveltekit/templates.test.ts
+++ b/test/sveltekit/templates.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import {
getClientHooksTemplate,
+ getInstrumentationServerTemplate,
getServerHooksTemplate,
} from '../../src/sveltekit/templates';
@@ -9,7 +10,7 @@ vi.mock('../../src/utils/clack/mcp-config', () => ({
}));
describe('getClientHooksTemplate', () => {
- it('should generate client hooks template with all features enabled', () => {
+ it('generates client hooks template with all features enabled', () => {
const result = getClientHooksTemplate('https://sentry.io/123', {
performance: true,
replay: true,
@@ -46,7 +47,7 @@ describe('getClientHooksTemplate', () => {
`);
});
- it('should generate client hooks template when performance disabled', () => {
+ it('generates client hooks template when performance disabled', () => {
const result = getClientHooksTemplate('https://sentry.io/123', {
performance: false,
replay: true,
@@ -79,7 +80,7 @@ describe('getClientHooksTemplate', () => {
`);
});
- it('should generate client hooks template when replay disabled', () => {
+ it('generates client hooks template when replay disabled', () => {
const result = getClientHooksTemplate('https://sentry.io/123', {
performance: true,
replay: false,
@@ -105,7 +106,7 @@ describe('getClientHooksTemplate', () => {
`);
});
- it('should generate client hooks template with only logs enabled', () => {
+ it('generates client hooks template with only logs enabled', () => {
const result = getClientHooksTemplate('https://sentry.io/123', {
performance: false,
replay: false,
@@ -133,12 +134,16 @@ describe('getClientHooksTemplate', () => {
});
describe('getServerHooksTemplate', () => {
- it('should generate server hooks template with all features enabled', () => {
- const result = getServerHooksTemplate('https://sentry.io/123', {
- performance: true,
- replay: true,
- logs: true,
- });
+ it('generates server hooks template with all features enabled', () => {
+ const result = getServerHooksTemplate(
+ 'https://sentry.io/123',
+ {
+ performance: true,
+ replay: true,
+ logs: true,
+ },
+ true,
+ );
expect(result).toMatchInlineSnapshot(`
"import { sequence } from "@sveltejs/kit/hooks";
@@ -166,12 +171,16 @@ describe('getServerHooksTemplate', () => {
`);
});
- it('should generate server hooks template when performance disabled', () => {
- const result = getServerHooksTemplate('https://sentry.io/123', {
- performance: false,
- replay: true,
- logs: false,
- });
+ it('generates server hooks template when performance disabled', () => {
+ const result = getServerHooksTemplate(
+ 'https://sentry.io/123',
+ {
+ performance: false,
+ replay: true,
+ logs: false,
+ },
+ true,
+ );
expect(result).toMatchInlineSnapshot(`
"import { sequence } from "@sveltejs/kit/hooks";
@@ -195,12 +204,16 @@ describe('getServerHooksTemplate', () => {
`);
});
- it('should generate server hooks template with only logs enabled', () => {
- const result = getServerHooksTemplate('https://sentry.io/123', {
- performance: false,
- replay: false,
- logs: true,
- });
+ it('generates server hooks template with only logs enabled', () => {
+ const result = getServerHooksTemplate(
+ 'https://sentry.io/123',
+ {
+ performance: false,
+ replay: false,
+ logs: true,
+ },
+ true,
+ );
expect(result).toMatchInlineSnapshot(`
"import { sequence } from "@sveltejs/kit/hooks";
@@ -225,4 +238,111 @@ describe('getServerHooksTemplate', () => {
"
`);
});
+
+ it('generates server hooks template without Sentry.init if includeSentryInit is false', () => {
+ const result = getServerHooksTemplate(
+ 'https://sentry.io/123',
+ {
+ performance: false,
+ replay: false,
+ logs: true,
+ },
+ false,
+ );
+
+ expect(result).toMatchInlineSnapshot(`
+ "import { sequence } from "@sveltejs/kit/hooks";
+ import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit";
+
+
+ // If you have custom handlers, make sure to place them after \`sentryHandle()\` in the \`sequence\` function.
+ export const handle = sequence(sentryHandle());
+
+ // If you have a custom error handler, pass it to \`handleErrorWithSentry\`
+ export const handleError = handleErrorWithSentry();
+ "
+ `);
+ });
+});
+
+describe('getInstrumentationServerTemplate', () => {
+ it('generates instrumentation.server template with all features enabled', () => {
+ const result = getInstrumentationServerTemplate('https://sentry.io/123', {
+ performance: true,
+ logs: true,
+ });
+
+ expect(result).toMatchInlineSnapshot(`
+ "import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: 'https://sentry.io/123',
+
+ tracesSampleRate: 1.0,
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+ });"`);
+ });
+
+ it('generates instrumentation.server template with only logs enabled', () => {
+ const result = getInstrumentationServerTemplate('https://sentry.io/123', {
+ performance: false,
+ logs: true,
+ });
+
+ expect(result).toMatchInlineSnapshot(`
+ "import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: 'https://sentry.io/123',
+
+ // Enable logs to be sent to Sentry
+ enableLogs: true,
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+ });"`);
+ });
+
+ it('generates instrumentation.server template with only tracesSampleRate enabled', () => {
+ const result = getInstrumentationServerTemplate('https://sentry.io/123', {
+ performance: true,
+ logs: false,
+ });
+
+ expect(result).toMatchInlineSnapshot(`
+ "import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: 'https://sentry.io/123',
+
+ tracesSampleRate: 1.0,
+
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+ });"`);
+ });
+
+ it('generates instrumentation.server template without any extra features enabled', () => {
+ const result = getInstrumentationServerTemplate('https://sentry.io/123', {
+ performance: false,
+ logs: false,
+ });
+
+ expect(result).toMatchInlineSnapshot(`
+ "import * as Sentry from '@sentry/sveltekit';
+
+ Sentry.init({
+ dsn: 'https://sentry.io/123',
+
+
+ // uncomment the line below to enable Spotlight (https://spotlightjs.com)
+ // spotlight: import.meta.env.DEV,
+ });"`);
+ });
});