diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/.gitignore
new file mode 100644
index 000000000000..4096cd11d279
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+
+# Mock Sentry server artifacts
+.tmp_*
+/tmp_mock_chunks
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/assert-build.ts
new file mode 100644
index 000000000000..697c4437532d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/assert-build.ts
@@ -0,0 +1,100 @@
+import * as assert from 'node:assert/strict';
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import {
+ findInjectedDebugIds,
+ findSourceMapFiles,
+ findSourceMappingUrlComments,
+ getArtifactBundles,
+ getAssembleRequests,
+ getChunkUploadPosts,
+ getDebugIdPairs,
+ getSourcemaps,
+ loadMockServerResults,
+} from '@sentry-internal/test-utils';
+
+const BUILD_OUTPUT = 'build';
+const CLIENT_OUTPUT = path.join(BUILD_OUTPUT, 'client');
+const SERVER_OUTPUT = path.join(BUILD_OUTPUT, 'server');
+
+/** Both markers sit in comments, so bundlers strip them from the code but keep them in `sourcesContent`. */
+const CLIENT_MARKER = 'SOURCEMAP_MARKER_CLIENT';
+const SERVER_MARKER = 'SOURCEMAP_MARKER_SERVER';
+
+/** Written by the build, see `recordChunksWithoutSourceMap` in `vite.config.js`. */
+const debugIdsWithoutSourceMap = new Set(
+ fs.existsSync('.tmp_debug_ids_without_source_map')
+ ? fs.readFileSync('.tmp_debug_ids_without_source_map', 'utf8').split('\n').filter(Boolean)
+ : [],
+);
+
+const requests = loadMockServerResults();
+
+console.log(`Captured ${requests.length} requests to mock Sentry server:\n`);
+for (const request of requests) {
+ console.log(` ${request.method} ${request.url} (${request.bodySize} bytes)`);
+}
+console.log('');
+
+assert.ok(
+ requests.some(r => r.authorization.includes('fake-auth-token')),
+ 'Expected requests with the configured auth token',
+);
+
+assert.ok(
+ requests.some(r => r.url?.includes('/releases') && r.method === 'POST'),
+ 'Expected a POST to create the release',
+);
+
+assert.ok(
+ getChunkUploadPosts(requests).some(r => r.bodySize > 0),
+ 'Expected at least one chunk upload POST with a non-empty body',
+);
+
+const assembleRequests = getAssembleRequests(requests);
+assert.ok(assembleRequests.length > 0, 'Expected at least one assemble request');
+for (const request of assembleRequests) {
+ assert.ok(request.assembleBody?.projects?.includes('test-project'), 'Expected assemble request for test-project');
+ assert.equal(request.assembleBody?.version, 'test-release', 'Expected assemble request to reference the release');
+}
+
+const bundles = getArtifactBundles(requests);
+assert.ok(bundles.length > 0, 'Expected at least one artifact bundle with a manifest');
+
+const sourcemaps = getSourcemaps(bundles);
+
+const containsMarker = (marker: string): boolean =>
+ sourcemaps.some(map => map.sourcemap.sourcesContent?.some(source => source?.includes(marker)));
+
+assert.ok(containsMarker(CLIENT_MARKER), 'Expected an uploaded sourcemap carrying the client source');
+assert.ok(containsMarker(SERVER_MARKER), 'Expected an uploaded sourcemap carrying the server source');
+
+// The auto-instrument plugin loads wrapped modules under this suffix. It must be stripped before
+// upload, or the uploaded sources don't match the frames Sentry receives.
+const wrappedSources = sourcemaps
+ .flatMap(map => map.sourcemap.sources ?? [])
+ .filter(source => source.includes('?sentry-auto-wrap'));
+assert.deepEqual(wrappedSources, [], 'Expected no uploaded source to carry the auto-wrap query suffix');
+
+const uploadedDebugIds = new Set(getDebugIdPairs(bundles).map(pair => pair.debugId.toLowerCase()));
+assert.ok(uploadedDebugIds.size > 0, 'Expected at least one JS/sourcemap pair with matching debug IDs');
+
+for (const outputDir of [CLIENT_OUTPUT, SERVER_OUTPUT]) {
+ const injectedDebugIds = findInjectedDebugIds({ outputDir });
+ assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`);
+
+ const unuploaded = injectedDebugIds.filter(
+ debugId => !uploadedDebugIds.has(debugId) && !debugIdsWithoutSourceMap.has(debugId),
+ );
+ assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`);
+}
+
+// The app does not set `build.sourcemap`, so the SDK enables hidden maps and must delete them after upload.
+assert.deepEqual(findSourceMapFiles({ outputDir: BUILD_OUTPUT }), [], `Expected no source maps in ${BUILD_OUTPUT}`);
+assert.deepEqual(
+ findSourceMappingUrlComments({ outputDir: CLIENT_OUTPUT }),
+ [],
+ `Expected no sourceMappingURL comments in ${CLIENT_OUTPUT}`,
+);
+
+console.log('All sourcemap assertions passed!');
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/package.json
new file mode 100644
index 000000000000..93da7029d773
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "sveltekit-3-sourcemaps",
+ "description": "E2E test app asserting what the SvelteKit SDK uploads to Sentry and what it leaves behind in `build`.",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "node start-mock-sentry-server.mjs & vite build > .tmp_build_stdout 2> .tmp_build_stderr; BUILD_EXIT=$?; kill %1 2>/dev/null; if [ $BUILD_EXIT -ne 0 ]; then cat .tmp_build_stdout; cat .tmp_build_stderr >&2; fi; exit $BUILD_EXIT",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "node assert-build.ts"
+ },
+ "dependencies": {
+ "@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
+ },
+ "devDependencies": {
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sveltejs/adapter-node": "^6.0.0",
+ "@sveltejs/kit": "^3.0.0",
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
+ "@types/node": "^22.20.0",
+ "svelte": "^5.57.1",
+ "typescript": "^6.0.0",
+ "vite": "^8.0.12"
+ },
+ "volta": {
+ "node": "22.20.0",
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.d.ts
new file mode 100644
index 000000000000..ede601ab93e2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://kit.svelte.dev/docs/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/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.html
new file mode 100644
index 000000000000..023060c2d49d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+
%sveltekit.body%
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.client.ts
new file mode 100644
index 000000000000..f3bbf5d7b179
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.client.ts
@@ -0,0 +1,7 @@
+import * as Sentry from '@sentry/sveltekit';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
+
+export const handleError = Sentry.handleErrorWithSentry();
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.server.ts
new file mode 100644
index 000000000000..0865342abcf1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/hooks.server.ts
@@ -0,0 +1,5 @@
+import * as Sentry from '@sentry/sveltekit';
+
+export const handleError = Sentry.handleErrorWithSentry();
+
+export const handle = Sentry.sentryHandle();
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/instrumentation.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/instrumentation.server.ts
new file mode 100644
index 000000000000..372d3cceff73
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/instrumentation.server.ts
@@ -0,0 +1,6 @@
+import * as Sentry from '@sentry/sveltekit';
+
+// SvelteKit 3 loads this file before the app, which is where `Sentry.init` belongs.
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/+page.svelte
new file mode 100644
index 000000000000..dec5cf04b490
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.server.ts
new file mode 100644
index 000000000000..131d44a2f2a0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.server.ts
@@ -0,0 +1,4 @@
+// SOURCEMAP_MARKER_SERVER
+export const load = async () => {
+ throw new Error('Server load error');
+};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.svelte
new file mode 100644
index 000000000000..089965d0419f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/src/routes/server-error/+page.svelte
@@ -0,0 +1 @@
+
Server error
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/start-mock-sentry-server.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/start-mock-sentry-server.mjs
new file mode 100644
index 000000000000..7d2ade5cb226
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/start-mock-sentry-server.mjs
@@ -0,0 +1,4 @@
+import { startMockSentryServer } from '@sentry-internal/test-utils';
+
+// Not a hidden dir, which the SDK's default `./.*/**/*.map` cleanup would empty after the upload
+startMockSentryServer({ org: 'test-org', outputDir: 'tmp_mock_chunks' });
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/static/favicon.png
new file mode 100644
index 000000000000..825b9e65af7c
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/static/favicon.png differ
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/tsconfig.json
new file mode 100644
index 000000000000..fb327998a0ba
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "$app/tsconfig",
+ "include": ["src"],
+ "compilerOptions": {
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/vite.config.js b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/vite.config.js
new file mode 100644
index 000000000000..c826e74cce72
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-3-sourcemaps/vite.config.js
@@ -0,0 +1,44 @@
+import adapter from '@sveltejs/adapter-node';
+import { sentrySvelteKit } from '@sentry/sveltekit/vite';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+import * as fs from 'node:fs';
+import { defineConfig } from 'vite';
+
+// Rolldown emits no map for chunks without mappable source (e.g. Vite's preload helper), so their debug
+// IDs can't be uploaded. Recorded here because the SDK deletes all maps after the upload.
+function recordChunksWithoutSourceMap() {
+ return {
+ name: 'record-chunks-without-source-map',
+ apply: 'build',
+ enforce: 'post',
+ writeBundle(_options, bundle) {
+ for (const output of Object.values(bundle)) {
+ if (output.type !== 'chunk' || bundle[`${output.fileName}.map`]) {
+ continue;
+ }
+ for (const match of output.code.matchAll(/sentry-dbid-([\da-f-]{36})/gi)) {
+ fs.appendFileSync('.tmp_debug_ids_without_source_map', `${match[1].toLowerCase()}\n`);
+ }
+ }
+ },
+ };
+}
+
+export default defineConfig({
+ plugins: [
+ sentrySvelteKit({
+ sentryUrl: 'http://localhost:3032',
+ authToken: 'fake-auth-token',
+ org: 'test-org',
+ project: 'test-project',
+ release: { name: 'test-release' },
+ debug: true,
+ }),
+ sveltekit({
+ preprocess: vitePreprocess(),
+ adapter: adapter(),
+ }),
+ recordChunksWithoutSourceMap(),
+ ],
+});
diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts
index 4f8c50e1a14c..7cfa82ed6f6d 100644
--- a/packages/sveltekit/src/vite/sourceMaps.ts
+++ b/packages/sveltekit/src/vite/sourceMaps.ts
@@ -111,10 +111,14 @@ export async function makeCustomSentryVitePlugins(
let isSSRBuild = true;
+ // Recorded before `sourceMapSettingsPlugin` overrides `build.sourcemap`
+ let sourcemapSettingAlreadySet = false;
+
const sourceMapSettingsPlugin: Plugin = {
name: 'sentry-sveltekit-update-source-map-setting-plugin',
apply: 'build', // only apply this plugin at build time
config: async (config: UserConfig) => {
+ sourcemapSettingAlreadySet = typeof config.build?.sourcemap !== 'undefined';
return {
...config,
build: {
@@ -125,18 +129,9 @@ export async function makeCustomSentryVitePlugins(
},
};
- // Whether `build.sourcemap` is already set when this `config` hook runs. Read here rather than in
- // `configResolved`, where it is always set - but note that nothing here may *await* the SvelteKit
- // config from a `config` hook, see the note in `kitConfig.ts`.
- let sourcemapSettingAlreadySet = false;
-
const filesToDeleteAfterUploadConfigPlugin: Plugin = {
name: 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
apply: 'build', // only apply this plugin at build time
- config: (config: UserConfig) => {
- sourcemapSettingAlreadySet = typeof config.build?.sourcemap !== 'undefined';
- return config;
- },
configResolved: async () => {
// Resolved here (not in `closeBundle`) so the adapter is invoked before the build writes its
// output - see the note on the adapter output dir in `sentrySvelteKit()`.
@@ -165,6 +160,86 @@ export async function makeCustomSentryVitePlugins(
},
};
+ let sourceMapsUploaded = false;
+
+ // We need to start uploading source maps later than in the original plugin
+ // because SvelteKit is invoking the adapter after its builds.
+ // This means that we need to wait until the adapter is done before we start uploading.
+ const uploadSourceMaps = async (): Promise => {
+ sourceMapsUploaded = true;
+
+ const outDir = path.resolve(process.cwd(), await getAdapterOutputDir());
+ // eslint-disable-next-line no-console
+ debug && console.log('[Source Maps Plugin] Looking up source maps in', outDir);
+
+ const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js'));
+ // eslint-disable-next-line no-console
+ debug && console.log('[Source Maps Plugin] Flattening source maps');
+
+ // @ts-expect-error - we're using dynamic import here and TS complains about that. It works though.
+ const sorcery = await import('sorcery');
+
+ for (const file of jsFiles) {
+ try {
+ await (sorcery as Sorcery).load(file).then(async chain => {
+ if (!chain) {
+ // We end up here, if we don't have a source map for the file.
+ // This is fine, as we're not interested in files w/o source maps.
+ return;
+ }
+ // This flattens the source map
+ await chain.apply();
+ // Write it back to the original file
+ await chain.write();
+ });
+ } catch (e) {
+ // Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly
+ // happening in Kit-internal files which is fine as they're not in-app.
+ // This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist.
+ const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open');
+ if (debug && !isKnownError) {
+ // eslint-disable-next-line no-console
+ console.error('[Source Maps Plugin] error while flattening', file, e);
+ }
+ }
+
+ // We need to remove the query string from the source map files that our auto-instrument plugin added
+ // to proxy the load functions during building.
+ const mapFile = `${file}.map`;
+ try {
+ const mapContent = (await fs.promises.readFile(mapFile, 'utf-8')).toString();
+ const cleanedMapContent = mapContent.replace(
+ // oxlint-disable-next-line sdk/no-regexp-constructor -- no user input + escaped anyway
+ new RegExp(escapeStringForRegex(WRAPPED_MODULE_SUFFIX), 'gm'),
+ '',
+ );
+ await fs.promises.writeFile(mapFile, cleanedMapContent);
+ } catch {
+ // Map file doesn't exist, nothing to clean
+ }
+ }
+
+ try {
+ // Call the original plugin's writeBundle to upload source maps, manage releases, and delete files.
+ // We pass in the `outDir` we determined earlier as output options.
+ if (typeof originalWriteBundle === 'function') {
+ // @ts-expect-error - calling writeBundle with only outputOptions, without the full bundle arg
+ await originalWriteBundle({ dir: outDir });
+ }
+ } catch {
+ // eslint-disable-next-line no-console
+ console.warn('[Source Maps Plugin] Failed to upload source maps!');
+ // eslint-disable-next-line no-console
+ console.log(
+ '[Source Maps Plugin] Please make sure you specified a valid Sentry auth token, as well as your org and project slugs.',
+ );
+ // eslint-disable-next-line no-console
+ console.log(
+ '[Source Maps Plugin] Further information: https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md#uploading-source-maps',
+ );
+ }
+ };
+
const customDebugIdUploadPlugin: Plugin = {
name: 'sentry-sveltekit-debug-id-upload-plugin',
apply: 'build', // only apply this plugin at build time
@@ -179,84 +254,24 @@ export async function makeCustomSentryVitePlugins(
}
},
- // We need to start uploading source maps later than in the original plugin
- // because SvelteKit is invoking the adapter at closeBundle.
- // This means that we need to wait until the adapter is done before we start uploading.
+ // SvelteKit 2 invokes the adapter in `closeBundle` of the SSR build.
closeBundle: async () => {
if (!isSSRBuild) {
return;
}
+ await uploadSourceMaps();
+ },
- const outDir = path.resolve(process.cwd(), await getAdapterOutputDir());
- // eslint-disable-next-line no-console
- debug && console.log('[Source Maps Plugin] Looking up source maps in', outDir);
-
- const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js'));
- // eslint-disable-next-line no-console
- debug && console.log('[Source Maps Plugin] Flattening source maps');
-
- // @ts-expect-error - we're using dynamic import here and TS complains about that. It works though.
- const sorcery = await import('sorcery');
-
- for (const file of jsFiles) {
- try {
- await (sorcery as Sorcery).load(file).then(async chain => {
- if (!chain) {
- // We end up here, if we don't have a source map for the file.
- // This is fine, as we're not interested in files w/o source maps.
- return;
- }
- // This flattens the source map
- await chain.apply();
- // Write it back to the original file
- await chain.write();
- });
- } catch (e) {
- // Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly
- // happening in Kit-internal files which is fine as they're not in-app.
- // This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist.
- const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open');
- if (debug && !isKnownError) {
- // eslint-disable-next-line no-console
- console.error('[Source Maps Plugin] error while flattening', file, e);
- }
- }
-
- // We need to remove the query string from the source map files that our auto-instrument plugin added
- // to proxy the load functions during building.
- const mapFile = `${file}.map`;
- try {
- const mapContent = (await fs.promises.readFile(mapFile, 'utf-8')).toString();
- const cleanedMapContent = mapContent.replace(
- // oxlint-disable-next-line sdk/no-regexp-constructor -- no user input + escaped anyway
- new RegExp(escapeStringForRegex(WRAPPED_MODULE_SUFFIX), 'gm'),
- '',
- );
- await fs.promises.writeFile(mapFile, cleanedMapContent);
- } catch {
- // Map file doesn't exist, nothing to clean
- }
- }
-
- try {
- // Call the original plugin's writeBundle to upload source maps, manage releases, and delete files.
- // We pass in the `outDir` we determined earlier as output options.
- if (typeof originalWriteBundle === 'function') {
- // @ts-expect-error - calling writeBundle with only outputOptions, without the full bundle arg
- await originalWriteBundle({ dir: outDir });
+ // SvelteKit 3 invokes the adapter in its post `buildApp` hook, which runs before ours (`enforce: 'post'`).
+ // With SvelteKit 2, nothing is built yet when this runs, so `closeBundle` uploads instead.
+ buildApp: {
+ order: 'post',
+ handler: async builder => {
+ if (sourceMapsUploaded || !Object.values(builder.environments).some(environment => environment.isBuilt)) {
+ return;
}
- } catch {
- // eslint-disable-next-line no-console
- console.warn('[Source Maps Plugin] Failed to upload source maps!');
- // eslint-disable-next-line no-console
- console.log(
- '[Source Maps Plugin] Please make sure you specified a valid Sentry auth token, as well as your org and project slugs.',
- );
- // eslint-disable-next-line no-console
- console.log(
- '[Source Maps Plugin] Further information: https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md#uploading-source-maps',
- );
- }
+ await uploadSourceMaps();
+ },
},
};
diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
index 10b81dadf3fc..4e4226e5368b 100644
--- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts
+++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
@@ -145,6 +145,9 @@ describe('adapter output dir resolution', () => {
],
});
+ const sourceMapSettingPlugin = plugins.find(
+ plugin => plugin.name === 'sentry-sveltekit-update-source-map-setting-plugin',
+ )!;
const filesToDeletePlugin = plugins.find(
plugin => plugin.name === 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
)!;
@@ -153,7 +156,7 @@ describe('adapter output dir resolution', () => {
// This takes the branch that leaves `filesToDeleteAfterUpload` untouched - the adapter still
// has to be resolved by `configResolved`, not later in `closeBundle`
// @ts-expect-error these hooks exist and are callable
- filesToDeletePlugin.config({ build: { sourcemap: true } });
+ await sourceMapSettingPlugin.config({ build: { sourcemap: true } });
// @ts-expect-error these hooks exist and are callable
await filesToDeletePlugin.configResolved();
diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts
index d12ee0fdf8f5..d4f2d3c06db8 100644
--- a/packages/sveltekit/test/vite/sourceMaps.test.ts
+++ b/packages/sveltekit/test/vite/sourceMaps.test.ts
@@ -188,6 +188,61 @@ describe('makeCustomSentryVitePlugins()', () => {
await plugin.closeBundle();
expect(mockedMainPlugin.writeBundle).not.toHaveBeenCalled();
});
+
+ it('uploads source maps in the post `buildApp` hook when there is no separate SSR build (SvelteKit 3)', async () => {
+ const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
+
+ // @ts-expect-error this function exists!
+ plugin.configResolved({ build: { ssr: false } });
+ // @ts-expect-error this function exists!
+ await plugin.closeBundle();
+ expect(mockedMainPlugin.writeBundle).not.toHaveBeenCalled();
+
+ expect(plugin?.buildApp).toEqual({ order: 'post', handler: expect.any(Function) });
+ // @ts-expect-error this function exists!
+ await plugin.buildApp.handler({ environments: { ssr: { isBuilt: true }, client: { isBuilt: true } } });
+ expect(mockedMainPlugin.writeBundle).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't upload source maps in the post `buildApp` hook before any environment was built (SvelteKit 2 on Vite 7+)", async () => {
+ const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
+
+ // @ts-expect-error this function exists!
+ await plugin.buildApp.handler({ environments: { ssr: { isBuilt: false } } });
+ expect(mockedMainPlugin.writeBundle).not.toHaveBeenCalled();
+
+ // @ts-expect-error this function exists!
+ plugin.configResolved({ build: { ssr: true } });
+ // @ts-expect-error this function exists!
+ await plugin.closeBundle();
+ expect(mockedMainPlugin.writeBundle).toHaveBeenCalledTimes(1);
+ });
+
+ it('uploads source maps only once when both `closeBundle` and `buildApp` run (SvelteKit 2 on Vite 7+)', async () => {
+ const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
+
+ // @ts-expect-error this function exists!
+ plugin.configResolved({ build: { ssr: true } });
+ // @ts-expect-error this function exists!
+ await plugin.closeBundle();
+ // @ts-expect-error this function exists!
+ await plugin.buildApp.handler({ environments: { ssr: { isBuilt: true } } });
+
+ expect(mockedMainPlugin.writeBundle).toHaveBeenCalledTimes(1);
+ });
+
+ it('uploads source maps on every SSR rebuild in watch mode', async () => {
+ const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
+
+ // @ts-expect-error this function exists!
+ plugin.configResolved({ build: { ssr: true } });
+ // @ts-expect-error this function exists!
+ await plugin.closeBundle();
+ // @ts-expect-error this function exists!
+ await plugin.closeBundle();
+
+ expect(mockedMainPlugin.writeBundle).toHaveBeenCalledTimes(2);
+ });
});
it('catches errors while uploading source maps', async () => {
@@ -200,13 +255,10 @@ describe('makeCustomSentryVitePlugins()', () => {
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
- // @ts-expect-error this function exists!
- expect(plugin.closeBundle).not.toThrow();
-
// @ts-expect-error this function exists!
plugin.configResolved({ build: { ssr: true } });
// @ts-expect-error this function exists!
- await plugin.closeBundle();
+ await expect(plugin.closeBundle()).resolves.toBeUndefined();
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to upload source maps'));
expect(consoleLogSpy).toHaveBeenCalled();
@@ -348,11 +400,7 @@ describe('deleteFilesAfterUpload', () => {
plugin => plugin.name === 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
)!;
- // `config` records whether the user set `build.sourcemap`; `configResolved` resolves the
- // setting (it can't be awaited from `config` - see the note in `kitConfig.ts`)
- // @ts-expect-error these functions exist!
- filesToDeleteAfterUploadSettingPlugin.config(viteConfig);
- // @ts-expect-error these functions exist!
+ // @ts-expect-error this function exists!
await filesToDeleteAfterUploadSettingPlugin.configResolved();
await expect(mergedOptions.sourcemaps.filesToDeleteAfterUpload).resolves.toEqual([
@@ -430,11 +478,7 @@ describe('deleteFilesAfterUpload', () => {
plugin => plugin.name === 'sentry-sveltekit-files-to-delete-after-upload-setting-plugin',
)!;
- // `config` records whether the user set `build.sourcemap`; `configResolved` resolves the
- // setting (it can't be awaited from `config` - see the note in `kitConfig.ts`)
- // @ts-expect-error these functions exist!
- filesToDeleteAfterUploadSettingPlugin.config(viteConfig);
- // @ts-expect-error these functions exist!
+ // @ts-expect-error this function exists!
await filesToDeleteAfterUploadSettingPlugin.configResolved();
await expect(mergedOptions.sourcemaps.filesToDeleteAfterUpload).resolves.toEqual(