Skip to content

Commit af84269

Browse files
chargomeclaude
andauthored
fix(sveltekit): Upload source maps with SvelteKit 3 (#25139)
SvelteKit 3 builds all environments in a single Vite build and runs the adapter in a post `buildApp` hook. Our upload waited for a separate SSR build in `closeBundle`, which Kit 3 never does, so nothing was uploaded. We now also upload from a post `buildApp` hook, but only once an environment has been built. Kit 2 builds only after all `buildApp` hooks have run, so its `closeBundle` path still does the upload. Source maps were also never deleted after upload, on Kit 2 or Kit 3. The deletion check read `build.sourcemap` after our own plugin had already set it. The new `sveltekit-3-sourcemaps` e2e app builds against a mock Sentry server. It asserts that the upload happens, that every debug ID has a map, and that no maps are left in `build/`. Fixes #25096 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5b98d75 commit af84269

18 files changed

Lines changed: 409 additions & 96 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/.svelte-kit
5+
6+
# Mock Sentry server artifacts
7+
.tmp_*
8+
/tmp_mock_chunks
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as assert from 'node:assert/strict';
2+
import * as fs from 'node:fs';
3+
import * as path from 'node:path';
4+
import {
5+
findInjectedDebugIds,
6+
findSourceMapFiles,
7+
findSourceMappingUrlComments,
8+
getArtifactBundles,
9+
getAssembleRequests,
10+
getChunkUploadPosts,
11+
getDebugIdPairs,
12+
getSourcemaps,
13+
loadMockServerResults,
14+
} from '@sentry-internal/test-utils';
15+
16+
const BUILD_OUTPUT = 'build';
17+
const CLIENT_OUTPUT = path.join(BUILD_OUTPUT, 'client');
18+
const SERVER_OUTPUT = path.join(BUILD_OUTPUT, 'server');
19+
20+
/** Both markers sit in comments, so bundlers strip them from the code but keep them in `sourcesContent`. */
21+
const CLIENT_MARKER = 'SOURCEMAP_MARKER_CLIENT';
22+
const SERVER_MARKER = 'SOURCEMAP_MARKER_SERVER';
23+
24+
/** Written by the build, see `recordChunksWithoutSourceMap` in `vite.config.js`. */
25+
const debugIdsWithoutSourceMap = new Set(
26+
fs.existsSync('.tmp_debug_ids_without_source_map')
27+
? fs.readFileSync('.tmp_debug_ids_without_source_map', 'utf8').split('\n').filter(Boolean)
28+
: [],
29+
);
30+
31+
const requests = loadMockServerResults();
32+
33+
console.log(`Captured ${requests.length} requests to mock Sentry server:\n`);
34+
for (const request of requests) {
35+
console.log(` ${request.method} ${request.url} (${request.bodySize} bytes)`);
36+
}
37+
console.log('');
38+
39+
assert.ok(
40+
requests.some(r => r.authorization.includes('fake-auth-token')),
41+
'Expected requests with the configured auth token',
42+
);
43+
44+
assert.ok(
45+
requests.some(r => r.url?.includes('/releases') && r.method === 'POST'),
46+
'Expected a POST to create the release',
47+
);
48+
49+
assert.ok(
50+
getChunkUploadPosts(requests).some(r => r.bodySize > 0),
51+
'Expected at least one chunk upload POST with a non-empty body',
52+
);
53+
54+
const assembleRequests = getAssembleRequests(requests);
55+
assert.ok(assembleRequests.length > 0, 'Expected at least one assemble request');
56+
for (const request of assembleRequests) {
57+
assert.ok(request.assembleBody?.projects?.includes('test-project'), 'Expected assemble request for test-project');
58+
assert.equal(request.assembleBody?.version, 'test-release', 'Expected assemble request to reference the release');
59+
}
60+
61+
const bundles = getArtifactBundles(requests);
62+
assert.ok(bundles.length > 0, 'Expected at least one artifact bundle with a manifest');
63+
64+
const sourcemaps = getSourcemaps(bundles);
65+
66+
const containsMarker = (marker: string): boolean =>
67+
sourcemaps.some(map => map.sourcemap.sourcesContent?.some(source => source?.includes(marker)));
68+
69+
assert.ok(containsMarker(CLIENT_MARKER), 'Expected an uploaded sourcemap carrying the client source');
70+
assert.ok(containsMarker(SERVER_MARKER), 'Expected an uploaded sourcemap carrying the server source');
71+
72+
// The auto-instrument plugin loads wrapped modules under this suffix. It must be stripped before
73+
// upload, or the uploaded sources don't match the frames Sentry receives.
74+
const wrappedSources = sourcemaps
75+
.flatMap(map => map.sourcemap.sources ?? [])
76+
.filter(source => source.includes('?sentry-auto-wrap'));
77+
assert.deepEqual(wrappedSources, [], 'Expected no uploaded source to carry the auto-wrap query suffix');
78+
79+
const uploadedDebugIds = new Set(getDebugIdPairs(bundles).map(pair => pair.debugId.toLowerCase()));
80+
assert.ok(uploadedDebugIds.size > 0, 'Expected at least one JS/sourcemap pair with matching debug IDs');
81+
82+
for (const outputDir of [CLIENT_OUTPUT, SERVER_OUTPUT]) {
83+
const injectedDebugIds = findInjectedDebugIds({ outputDir });
84+
assert.ok(injectedDebugIds.length > 0, `Expected debug IDs to be injected into ${outputDir}`);
85+
86+
const unuploaded = injectedDebugIds.filter(
87+
debugId => !uploadedDebugIds.has(debugId) && !debugIdsWithoutSourceMap.has(debugId),
88+
);
89+
assert.deepEqual(unuploaded, [], `Expected every debug ID in ${outputDir} to have an uploaded sourcemap`);
90+
}
91+
92+
// The app does not set `build.sourcemap`, so the SDK enables hidden maps and must delete them after upload.
93+
assert.deepEqual(findSourceMapFiles({ outputDir: BUILD_OUTPUT }), [], `Expected no source maps in ${BUILD_OUTPUT}`);
94+
assert.deepEqual(
95+
findSourceMappingUrlComments({ outputDir: CLIENT_OUTPUT }),
96+
[],
97+
`Expected no sourceMappingURL comments in ${CLIENT_OUTPUT}`,
98+
);
99+
100+
console.log('All sourcemap assertions passed!');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "sveltekit-3-sourcemaps",
3+
"description": "E2E test app asserting what the SvelteKit SDK uploads to Sentry and what it leaves behind in `build`.",
4+
"version": "0.0.1",
5+
"private": true,
6+
"type": "module",
7+
"scripts": {
8+
"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",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "node assert-build.ts"
12+
},
13+
"dependencies": {
14+
"@sentry/sveltekit": "file:../../packed/sentry-sveltekit-packed.tgz"
15+
},
16+
"devDependencies": {
17+
"@sentry-internal/test-utils": "link:../../../test-utils",
18+
"@sveltejs/adapter-node": "^6.0.0",
19+
"@sveltejs/kit": "^3.0.0",
20+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
21+
"@types/node": "^22.20.0",
22+
"svelte": "^5.57.1",
23+
"typescript": "^6.0.0",
24+
"vite": "^8.0.12"
25+
},
26+
"volta": {
27+
"node": "22.20.0",
28+
"extends": "../../package.json"
29+
}
30+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// See https://kit.svelte.dev/docs/types#app
2+
// for information about these interfaces
3+
declare global {
4+
namespace App {
5+
// interface Error {}
6+
// interface Locals {}
7+
// interface PageData {}
8+
// interface PageState {}
9+
// interface Platform {}
10+
}
11+
}
12+
13+
export {};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
6+
<meta name="viewport" content="width=device-width" />
7+
%sveltekit.head%
8+
</head>
9+
<body data-sveltekit-preload-data="off">
10+
<div style="display: contents">%sveltekit.body%</div>
11+
</body>
12+
</html>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as Sentry from '@sentry/sveltekit';
2+
3+
Sentry.init({
4+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
5+
});
6+
7+
export const handleError = Sentry.handleErrorWithSentry();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/sveltekit';
2+
3+
export const handleError = Sentry.handleErrorWithSentry();
4+
5+
export const handle = Sentry.sentryHandle();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as Sentry from '@sentry/sveltekit';
2+
3+
// SvelteKit 3 loads this file before the app, which is where `Sentry.init` belongs.
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
// SOURCEMAP_MARKER_CLIENT
3+
function throwError() {
4+
throw new Error('Client error');
5+
}
6+
</script>
7+
8+
<button on:click={throwError}>Throw error</button>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SOURCEMAP_MARKER_SERVER
2+
export const load = async () => {
3+
throw new Error('Server load error');
4+
};

0 commit comments

Comments
 (0)