Skip to content

Commit af00c8f

Browse files
authored
fix(v8/sveltekit): Ensure source maps deletion is called after source ma… (#14963)
Backport of #14942
1 parent 8926cb7 commit af00c8f

File tree

3 files changed

+220
-25
lines changed

3 files changed

+220
-25
lines changed

packages/sveltekit/src/vite/sourceMaps.ts

+85-4
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
7676
plugin => plugin.name === 'sentry-vite-debug-id-upload-plugin',
7777
);
7878

79+
const sentryViteFileDeletionPlugin = sentryPlugins.find(plugin => plugin.name === 'sentry-file-deletion-plugin');
80+
81+
const sentryViteReleaseManagementPlugin = sentryPlugins.find(
82+
// sentry-debug-id-upload-plugin was the old (misleading) name of the plugin
83+
// sentry-release-management-plugin is the new name
84+
plugin => plugin.name === 'sentry-debug-id-upload-plugin' || plugin.name === 'sentry-release-management-plugin',
85+
);
86+
7987
if (!sentryViteDebugIdUploadPlugin) {
8088
debug &&
8189
// eslint-disable-next-line no-console
@@ -85,7 +93,33 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
8593
return sentryPlugins;
8694
}
8795

88-
const restOfSentryVitePlugins = sentryPlugins.filter(plugin => plugin.name !== 'sentry-vite-debug-id-upload-plugin');
96+
if (!sentryViteFileDeletionPlugin) {
97+
debug &&
98+
// eslint-disable-next-line no-console
99+
console.warn(
100+
'sentry-file-deletion-plugin not found in sentryPlugins! Cannot modify plugin - returning default Sentry Vite plugins',
101+
);
102+
return sentryPlugins;
103+
}
104+
105+
if (!sentryViteReleaseManagementPlugin) {
106+
debug &&
107+
// eslint-disable-next-line no-console
108+
console.warn(
109+
'sentry-release-management-plugin not found in sentryPlugins! Cannot modify plugin - returning default Sentry Vite plugins',
110+
);
111+
return sentryPlugins;
112+
}
113+
114+
const unchangedSentryVitePlugins = sentryPlugins.filter(
115+
plugin =>
116+
![
117+
'sentry-vite-debug-id-upload-plugin',
118+
'sentry-file-deletion-plugin',
119+
'sentry-release-management-plugin', // new name of release management plugin
120+
'sentry-debug-id-upload-plugin', // old name of release management plugin
121+
].includes(plugin.name),
122+
);
89123

90124
let isSSRBuild = true;
91125

@@ -95,8 +129,8 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
95129
__sentry_sveltekit_output_dir: outputDir,
96130
};
97131

98-
const customPlugin: Plugin = {
99-
name: 'sentry-upload-sveltekit-source-maps',
132+
const customDebugIdUploadPlugin: Plugin = {
133+
name: 'sentry-sveltekit-debug-id-upload-plugin',
100134
apply: 'build', // only apply this plugin at build time
101135
enforce: 'post', // this needs to be set to post, otherwise we don't pick up the output from the SvelteKit adapter
102136

@@ -248,7 +282,54 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
248282
},
249283
};
250284

251-
return [...restOfSentryVitePlugins, customPlugin];
285+
// The file deletion plugin is originally called in `writeBundle`.
286+
// We need to call it in `closeBundle` though, because we also postpone
287+
// the upload step to `closeBundle`
288+
const customFileDeletionPlugin: Plugin = {
289+
name: 'sentry-sveltekit-file-deletion-plugin',
290+
apply: 'build', // only apply this plugin at build time
291+
enforce: 'post',
292+
closeBundle: async () => {
293+
if (!isSSRBuild) {
294+
return;
295+
}
296+
297+
const writeBundleFn = sentryViteFileDeletionPlugin?.writeBundle;
298+
if (typeof writeBundleFn === 'function') {
299+
// This is fine though, because the original method doesn't consume any arguments in its `writeBundle` callback.
300+
const outDir = path.resolve(process.cwd(), outputDir);
301+
try {
302+
// @ts-expect-error - the writeBundle hook expects two args we can't pass in here (they're only available in `writeBundle`)
303+
await writeBundleFn({ dir: outDir });
304+
} catch (e) {
305+
// eslint-disable-next-line no-console
306+
console.warn('Failed to delete source maps:', e);
307+
}
308+
}
309+
},
310+
};
311+
312+
const customReleaseManagementPlugin: Plugin = {
313+
name: 'sentry-sveltekit-release-management-plugin',
314+
apply: 'build', // only apply this plugin at build time
315+
enforce: 'post',
316+
closeBundle: async () => {
317+
try {
318+
// @ts-expect-error - this hook exists on the plugin!
319+
await sentryViteReleaseManagementPlugin.writeBundle();
320+
} catch (e) {
321+
// eslint-disable-next-line no-console
322+
console.warn('[Source Maps Plugin] Failed to upload release data:', e);
323+
}
324+
},
325+
};
326+
327+
return [
328+
...unchangedSentryVitePlugins,
329+
customReleaseManagementPlugin,
330+
customDebugIdUploadPlugin,
331+
customFileDeletionPlugin,
332+
];
252333
}
253334

254335
function getFiles(dir: string): string[] {

packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ describe('sentrySvelteKit()', () => {
5555
// default source maps plugins:
5656
'sentry-telemetry-plugin',
5757
'sentry-vite-release-injection-plugin',
58-
'sentry-debug-id-upload-plugin',
5958
'sentry-vite-debug-id-injection-plugin',
60-
'sentry-file-deletion-plugin',
59+
// custom release plugin:
60+
'sentry-sveltekit-release-management-plugin',
6161
// custom source maps plugin:
62-
'sentry-upload-sveltekit-source-maps',
62+
'sentry-sveltekit-debug-id-upload-plugin',
63+
// custom deletion plugin
64+
'sentry-sveltekit-file-deletion-plugin',
6365
]);
6466
});
6567

@@ -76,7 +78,7 @@ describe('sentrySvelteKit()', () => {
7678
const instrumentPlugin = plugins[0];
7779

7880
expect(plugins).toHaveLength(1);
79-
expect(instrumentPlugin.name).toEqual('sentry-auto-instrumentation');
81+
expect(instrumentPlugin?.name).toEqual('sentry-auto-instrumentation');
8082

8183
process.env.NODE_ENV = previousEnv;
8284
});

packages/sveltekit/test/vite/sourceMaps.test.ts

+129-17
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,31 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
33
import type { Plugin } from 'vite';
44
import { makeCustomSentryVitePlugins } from '../../src/vite/sourceMaps';
55

6-
const mockedSentryVitePlugin = {
6+
const mockedViteDebugIdUploadPlugin = {
77
name: 'sentry-vite-debug-id-upload-plugin',
88
writeBundle: vi.fn(),
99
};
1010

11+
const mockedViteReleaseManagementPlugin = {
12+
name: 'sentry-release-management-plugin',
13+
writeBundle: vi.fn(),
14+
};
15+
16+
const mockedFileDeletionPlugin = {
17+
name: 'sentry-file-deletion-plugin',
18+
writeBundle: vi.fn(),
19+
};
20+
1121
vi.mock('@sentry/vite-plugin', async () => {
1222
const original = (await vi.importActual('@sentry/vite-plugin')) as any;
1323

1424
return {
1525
...original,
16-
sentryVitePlugin: () => [mockedSentryVitePlugin],
26+
sentryVitePlugin: () => [
27+
mockedViteReleaseManagementPlugin,
28+
mockedViteDebugIdUploadPlugin,
29+
mockedFileDeletionPlugin,
30+
],
1731
};
1832
});
1933

@@ -30,20 +44,22 @@ beforeEach(() => {
3044
vi.clearAllMocks();
3145
});
3246

33-
async function getCustomSentryViteUploadSourcemapsPlugin(): Promise<Plugin | undefined> {
47+
async function getSentryViteSubPlugin(name: string): Promise<Plugin | undefined> {
3448
const plugins = await makeCustomSentryVitePlugins({
3549
authToken: 'token',
3650
org: 'org',
3751
project: 'project',
3852
adapter: 'other',
3953
});
40-
return plugins.find(plugin => plugin.name === 'sentry-upload-sveltekit-source-maps');
54+
55+
return plugins.find(plugin => plugin.name === name);
4156
}
4257

4358
describe('makeCustomSentryVitePlugin()', () => {
4459
it('returns the custom sentry source maps plugin', async () => {
45-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
46-
expect(plugin?.name).toEqual('sentry-upload-sveltekit-source-maps');
60+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
61+
62+
expect(plugin?.name).toEqual('sentry-sveltekit-debug-id-upload-plugin');
4763
expect(plugin?.apply).toEqual('build');
4864
expect(plugin?.enforce).toEqual('post');
4965

@@ -58,9 +74,9 @@ describe('makeCustomSentryVitePlugin()', () => {
5874
expect(plugin?.writeBundle).toBeUndefined();
5975
});
6076

61-
describe('Custom sentry vite plugin', () => {
77+
describe('Custom debug id source maps plugin plugin', () => {
6278
it('enables source map generation', async () => {
63-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
79+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
6480
// @ts-expect-error this function exists!
6581
const sentrifiedConfig = plugin.config({ build: { foo: {} }, test: {} });
6682
expect(sentrifiedConfig).toEqual({
@@ -73,7 +89,7 @@ describe('makeCustomSentryVitePlugin()', () => {
7389
});
7490

7591
it('injects the output dir into the server hooks file', async () => {
76-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
92+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
7793
// @ts-expect-error this function exists!
7894
const transformOutput = await plugin.transform('foo', '/src/hooks.server.ts');
7995
const transformedCode = transformOutput.code;
@@ -84,34 +100,34 @@ describe('makeCustomSentryVitePlugin()', () => {
84100
});
85101

86102
it('uploads source maps during the SSR build', async () => {
87-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
103+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
88104
// @ts-expect-error this function exists!
89105
plugin.configResolved({ build: { ssr: true } });
90106
// @ts-expect-error this function exists!
91107
await plugin.closeBundle();
92-
expect(mockedSentryVitePlugin.writeBundle).toHaveBeenCalledTimes(1);
108+
expect(mockedViteDebugIdUploadPlugin.writeBundle).toHaveBeenCalledTimes(1);
93109
});
94110

95111
it("doesn't upload source maps during the non-SSR builds", async () => {
96-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
112+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
97113

98114
// @ts-expect-error this function exists!
99115
plugin.configResolved({ build: { ssr: false } });
100116
// @ts-expect-error this function exists!
101117
await plugin.closeBundle();
102-
expect(mockedSentryVitePlugin.writeBundle).not.toHaveBeenCalled();
118+
expect(mockedViteDebugIdUploadPlugin.writeBundle).not.toHaveBeenCalled();
103119
});
104120
});
105121

106122
it('catches errors while uploading source maps', async () => {
107-
mockedSentryVitePlugin.writeBundle.mockImplementationOnce(() => {
123+
mockedViteDebugIdUploadPlugin.writeBundle.mockImplementationOnce(() => {
108124
throw new Error('test error');
109125
});
110126

111-
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
112-
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
127+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementationOnce(() => {});
128+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementationOnce(() => {});
113129

114-
const plugin = await getCustomSentryViteUploadSourcemapsPlugin();
130+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
115131

116132
// @ts-expect-error this function exists!
117133
expect(plugin.closeBundle).not.toThrow();
@@ -124,4 +140,100 @@ describe('makeCustomSentryVitePlugin()', () => {
124140
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to upload source maps'));
125141
expect(consoleLogSpy).toHaveBeenCalled();
126142
});
143+
144+
describe('Custom release management plugin', () => {
145+
it('has the expected hooks and properties', async () => {
146+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-release-management-plugin');
147+
148+
expect(plugin).toEqual({
149+
name: 'sentry-sveltekit-release-management-plugin',
150+
apply: 'build',
151+
enforce: 'post',
152+
closeBundle: expect.any(Function),
153+
});
154+
});
155+
156+
it('calls the original release management plugin to start the release creation pipeline', async () => {
157+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-release-management-plugin');
158+
// @ts-expect-error this function exists!
159+
await plugin.closeBundle();
160+
expect(mockedViteReleaseManagementPlugin.writeBundle).toHaveBeenCalledTimes(1);
161+
});
162+
163+
it('catches errors during release creation', async () => {
164+
mockedViteReleaseManagementPlugin.writeBundle.mockImplementationOnce(() => {
165+
throw new Error('test error');
166+
});
167+
168+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementationOnce(() => {});
169+
170+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-release-management-plugin');
171+
172+
// @ts-expect-error this function exists!
173+
expect(plugin.closeBundle).not.toThrow();
174+
175+
// @ts-expect-error this function exists!
176+
await plugin.closeBundle();
177+
178+
expect(consoleWarnSpy).toHaveBeenCalledWith(
179+
expect.stringContaining('Failed to upload release data'),
180+
expect.any(Error),
181+
);
182+
});
183+
184+
it('also works correctly if the original release management plugin has its old name', async () => {
185+
const currentName = mockedViteReleaseManagementPlugin.name;
186+
mockedViteReleaseManagementPlugin.name = 'sentry-debug-id-upload-plugin';
187+
188+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-release-management-plugin');
189+
190+
// @ts-expect-error this function exists!
191+
await plugin.closeBundle();
192+
193+
expect(mockedViteReleaseManagementPlugin.writeBundle).toHaveBeenCalledTimes(1);
194+
195+
mockedViteReleaseManagementPlugin.name = currentName;
196+
});
197+
});
198+
199+
describe('Custom file deletion plugin', () => {
200+
it('has the expected hooks and properties', async () => {
201+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-file-deletion-plugin');
202+
203+
expect(plugin).toEqual({
204+
name: 'sentry-sveltekit-file-deletion-plugin',
205+
apply: 'build',
206+
enforce: 'post',
207+
closeBundle: expect.any(Function),
208+
});
209+
});
210+
211+
it('calls the original file deletion plugin to delete files', async () => {
212+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-file-deletion-plugin');
213+
// @ts-expect-error this function exists!
214+
await plugin.closeBundle();
215+
expect(mockedFileDeletionPlugin.writeBundle).toHaveBeenCalledTimes(1);
216+
});
217+
218+
it('catches errors during file deletion', async () => {
219+
mockedFileDeletionPlugin.writeBundle.mockImplementationOnce(() => {
220+
throw new Error('test error');
221+
});
222+
223+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementationOnce(() => {});
224+
225+
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-file-deletion-plugin');
226+
227+
// @ts-expect-error this function exists!
228+
expect(plugin.closeBundle).not.toThrow();
229+
230+
// @ts-expect-error this function exists!
231+
await plugin.closeBundle();
232+
233+
expect(consoleWarnSpy).toHaveBeenCalledWith(
234+
expect.stringContaining('Failed to delete source maps'),
235+
expect.any(Error),
236+
);
237+
});
238+
});
127239
});

0 commit comments

Comments
 (0)