Skip to content

Commit 7401795

Browse files
authored
feat(tanstackstart-react): Add sentryTanstackStart vite plugin to manage automatic source map uploads (#18712)
Adds sentryTanstackStart Vite plugin to enable automatic source map uploads in TanStack Start applications. **What it does** I tried to align with what we do in other SDKs (e.g. SvelteKit). On a high-level it: - Uploads source maps to Sentry using @sentry/vite-plugin if configured, passing through all options set by the user (including release, sourcemaps, headers, etc.) - If the source maps option is not defined and also the files to delete option is not defined, we enable cleaning up all .map files after the source map upload - If the source map configuration is undefined, we additionally enable hidden source map generation **Usage** ``` // vite.config.ts import { defineConfig } from 'vite'; import { sentryTanstackStart } from '@sentry/tanstackstart-react'; import { tanstackStart } from '@tanstack/react-start/plugin/vite'; export default defineConfig({ plugins: [ sentryTanstackStart({ authToken: process.env.SENTRY_AUTH_TOKEN, org: 'my-org', project: 'my-project', }), tanstackStart(), ], }); ``` **Tests** - Added unit tests for sourceMaps.ts and sentryTanstackStart.ts covering plugin composition, options passing, and source map settings logic - Updated the E2E test to use the new plugin pattern Closes #18664
1 parent 656c5f5 commit 7401795

File tree

12 files changed

+572
-7
lines changed

12 files changed

+572
-7
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- **feat(tanstackstart-react): Add `sentryTanstackStart` Vite plugin for source maps upload**
8+
9+
You can now configure source maps upload for TanStack Start using the `sentryTanstackStart` Vite plugin:
10+
11+
```ts
12+
// vite.config.ts
13+
import { defineConfig } from 'vite';
14+
import { sentryTanstackStart } from '@sentry/tanstackstart-react';
15+
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
16+
17+
export default defineConfig({
18+
plugins: [
19+
sentryTanstackStart({
20+
authToken: process.env.SENTRY_AUTH_TOKEN,
21+
org: 'your-org',
22+
project: 'your-project',
23+
}),
24+
tanstackStart(),
25+
],
26+
});
27+
```
28+
729
Work in this release was contributed by @rreckonerr. Thank you for your contribution!
830

931
## 10.34.0

dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import tsConfigPaths from 'vite-tsconfig-paths';
33
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
44
import viteReact from '@vitejs/plugin-react-swc';
55
import { nitro } from 'nitro/vite';
6+
import { sentryTanstackStart } from '@sentry/tanstackstart-react';
67

78
export default defineConfig({
89
server: {
910
port: 3000,
1011
},
1112
plugins: [
13+
sentryTanstackStart({
14+
org: process.env.E2E_TEST_SENTRY_ORG_SLUG,
15+
project: process.env.E2E_TEST_SENTRY_PROJECT,
16+
authToken: process.env.E2E_TEST_AUTH_TOKEN,
17+
debug: true,
18+
}),
1219
tsConfigPaths(),
1320
tanstackStart(),
1421
nitro(),

packages/solidstart/src/vite/sourceMaps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function makeEnableSourceMapsVitePlugin(options: SentrySolidStartPluginOp
7676
];
7777
}
7878

79-
/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-j avascript/issues/13993)
79+
/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-javascript/issues/13993)
8080
*
8181
* 1. User explicitly disabled source maps
8282
* - keep this setting (emit a warning that errors won't be unminified in Sentry)

packages/tanstackstart-react/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@
5555
"@sentry-internal/browser-utils": "10.34.0",
5656
"@sentry/core": "10.34.0",
5757
"@sentry/node": "10.34.0",
58-
"@sentry/react": "10.34.0"
58+
"@sentry/react": "10.34.0",
59+
"@sentry/vite-plugin": "^4.6.2"
60+
},
61+
"devDependencies": {
62+
"vite": "^5.4.11"
5963
},
6064
"scripts": {
6165
"build": "run-p build:transpile build:types",

packages/tanstackstart-react/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
export * from './config';
44
export * from './server';
55
export * from './common';
6+
export * from './vite';

packages/tanstackstart-react/src/index.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './config';
1212
export * from './client';
1313
export * from './server';
1414
export * from './common';
15+
export * from './vite';
1516

1617
/** Initializes Sentry TanStack Start SDK */
1718
export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): Client | undefined;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { sentryTanstackStart } from './sentryTanstackStart';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { BuildTimeOptionsBase } from '@sentry/core';
2+
import type { Plugin } from 'vite';
3+
import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps';
4+
5+
/**
6+
* Vite plugins for the Sentry TanStack Start SDK.
7+
*
8+
* @example
9+
* ```typescript
10+
* // vite.config.ts
11+
* import { defineConfig } from 'vite';
12+
* import { sentryTanstackStart } from '@sentry/tanstackstart-react';
13+
* import { tanstackStart } from '@tanstack/react-start/plugin/vite';
14+
*
15+
* export default defineConfig({
16+
* plugins: [
17+
* sentryTanstackStart({
18+
* org: 'your-org',
19+
* project: 'your-project',
20+
* }),
21+
* tanstackStart(),
22+
* ],
23+
* });
24+
* ```
25+
*
26+
* @param options - Options to configure the Sentry Vite plugins
27+
* @returns An array of Vite plugins
28+
*/
29+
export function sentryTanstackStart(options: BuildTimeOptionsBase = {}): Plugin[] {
30+
// Only add plugins in production builds
31+
if (process.env.NODE_ENV === 'development') {
32+
return [];
33+
}
34+
35+
const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)];
36+
37+
const sourceMapsDisabled = options.sourcemaps?.disable === true || options.sourcemaps?.disable === 'disable-upload';
38+
if (!sourceMapsDisabled) {
39+
plugins.push(...makeEnableSourceMapsVitePlugin(options));
40+
}
41+
42+
return plugins;
43+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import type { BuildTimeOptionsBase } from '@sentry/core';
2+
import { sentryVitePlugin } from '@sentry/vite-plugin';
3+
import type { Plugin, UserConfig } from 'vite';
4+
5+
type FilesToDeleteAfterUpload = string | string[] | undefined;
6+
7+
/**
8+
* A Sentry plugin for adding the @sentry/vite-plugin to automatically upload source maps to Sentry.
9+
*/
10+
export function makeAddSentryVitePlugin(options: BuildTimeOptionsBase): Plugin[] {
11+
const {
12+
authToken,
13+
bundleSizeOptimizations,
14+
debug,
15+
errorHandler,
16+
headers,
17+
org,
18+
project,
19+
release,
20+
sentryUrl,
21+
silent,
22+
sourcemaps,
23+
telemetry,
24+
} = options;
25+
26+
// defer resolving the filesToDeleteAfterUpload until we got access to the Vite config
27+
let resolveFilesToDeleteAfterUpload: ((value: FilesToDeleteAfterUpload) => void) | undefined;
28+
const filesToDeleteAfterUploadPromise = new Promise<FilesToDeleteAfterUpload>(resolve => {
29+
resolveFilesToDeleteAfterUpload = resolve;
30+
});
31+
32+
const configPlugin: Plugin = {
33+
name: 'sentry-tanstackstart-files-to-delete-after-upload-plugin',
34+
apply: 'build',
35+
enforce: 'post',
36+
config(config) {
37+
const userFilesToDelete = sourcemaps?.filesToDeleteAfterUpload;
38+
39+
// Only auto-delete source maps if the user didn't configure sourcemaps at all
40+
if (typeof userFilesToDelete === 'undefined' && typeof config.build?.sourcemap === 'undefined') {
41+
if (debug) {
42+
// eslint-disable-next-line no-console
43+
console.log(
44+
'[Sentry] Automatically setting `sourcemaps.filesToDeleteAfterUpload: ["./**/*.map"]` to delete generated source maps after they were uploaded to Sentry.',
45+
);
46+
}
47+
resolveFilesToDeleteAfterUpload?.(['./**/*.map']);
48+
} else {
49+
resolveFilesToDeleteAfterUpload?.(userFilesToDelete);
50+
}
51+
},
52+
};
53+
54+
const sentryPlugins = sentryVitePlugin({
55+
authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN,
56+
bundleSizeOptimizations: bundleSizeOptimizations ?? undefined,
57+
debug: debug ?? false,
58+
errorHandler,
59+
headers,
60+
org: org ?? process.env.SENTRY_ORG,
61+
project: project ?? process.env.SENTRY_PROJECT,
62+
release,
63+
silent,
64+
sourcemaps: {
65+
assets: sourcemaps?.assets,
66+
disable: sourcemaps?.disable,
67+
ignore: sourcemaps?.ignore,
68+
filesToDeleteAfterUpload: filesToDeleteAfterUploadPromise,
69+
},
70+
telemetry: telemetry ?? true,
71+
url: sentryUrl,
72+
_metaOptions: {
73+
telemetry: {
74+
metaFramework: 'tanstackstart-react',
75+
},
76+
},
77+
});
78+
79+
return [configPlugin, ...sentryPlugins];
80+
}
81+
82+
/**
83+
* A Sentry plugin for TanStack Start React to enable "hidden" source maps if they are unset.
84+
*/
85+
export function makeEnableSourceMapsVitePlugin(options: BuildTimeOptionsBase): Plugin[] {
86+
return [
87+
{
88+
name: 'sentry-tanstackstart-react-source-maps',
89+
apply: 'build',
90+
enforce: 'post',
91+
config(viteConfig) {
92+
return {
93+
...viteConfig,
94+
build: {
95+
...viteConfig.build,
96+
sourcemap: getUpdatedSourceMapSettings(viteConfig, options),
97+
},
98+
};
99+
},
100+
},
101+
];
102+
}
103+
104+
/** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-javascript/issues/13993)
105+
*
106+
* 1. User explicitly disabled source maps
107+
* - keep this setting (emit a warning that errors won't be unminified in Sentry)
108+
* - We won't upload anything
109+
*
110+
* 2. Users enabled source map generation (true, 'hidden', 'inline').
111+
* - keep this setting (don't do anything - like deletion - besides uploading)
112+
*
113+
* 3. Users didn't set source maps generation
114+
* - we enable 'hidden' source maps generation
115+
* - configure `filesToDeleteAfterUpload` to delete all .map files (we emit a log about this)
116+
*
117+
* --> only exported for testing
118+
*/
119+
export function getUpdatedSourceMapSettings(
120+
viteConfig: UserConfig,
121+
sentryPluginOptions?: BuildTimeOptionsBase,
122+
): boolean | 'inline' | 'hidden' {
123+
viteConfig.build = viteConfig.build || {};
124+
125+
const viteUserSourceMapSetting = viteConfig.build?.sourcemap;
126+
const settingKey = 'vite.build.sourcemap';
127+
const debug = sentryPluginOptions?.debug;
128+
129+
// Respect user source map setting if it is explicitly set
130+
if (viteUserSourceMapSetting === false) {
131+
if (debug) {
132+
// eslint-disable-next-line no-console
133+
console.warn(
134+
`[Sentry] Source map generation is currently disabled in your TanStack Start configuration (\`${settingKey}: false\`). Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified.`,
135+
);
136+
} else {
137+
// eslint-disable-next-line no-console
138+
console.warn('[Sentry] Source map generation is disabled in your TanStack Start configuration.');
139+
}
140+
141+
return viteUserSourceMapSetting;
142+
} else if (viteUserSourceMapSetting && ['hidden', 'inline', true].includes(viteUserSourceMapSetting)) {
143+
if (debug) {
144+
// eslint-disable-next-line no-console
145+
console.log(
146+
`[Sentry] We discovered \`${settingKey}\` is set to \`${viteUserSourceMapSetting.toString()}\`. Sentry will keep this source map setting.`,
147+
);
148+
}
149+
150+
return viteUserSourceMapSetting;
151+
}
152+
153+
// If the user did not specify a source map setting, we enable 'hidden' by default
154+
if (debug) {
155+
// eslint-disable-next-line no-console
156+
console.log(
157+
`[Sentry] Enabled source map generation in the build options with \`${settingKey}: 'hidden'\`. The source maps will be deleted after they were uploaded to Sentry.`,
158+
);
159+
}
160+
161+
return 'hidden';
162+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { Plugin } from 'vite';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { sentryTanstackStart } from '../../src/vite/sentryTanstackStart';
4+
5+
const mockSourceMapsConfigPlugin: Plugin = {
6+
name: 'sentry-tanstackstart-files-to-delete-after-upload-plugin',
7+
apply: 'build',
8+
enforce: 'pre',
9+
config: vi.fn(),
10+
};
11+
12+
const mockSentryVitePlugin: Plugin = {
13+
name: 'sentry-vite-debug-id-upload-plugin',
14+
writeBundle: vi.fn(),
15+
};
16+
17+
const mockEnableSourceMapsPlugin: Plugin = {
18+
name: 'sentry-tanstackstart-react-source-maps',
19+
apply: 'build',
20+
enforce: 'post',
21+
config: vi.fn(),
22+
};
23+
24+
vi.mock('../../src/vite/sourceMaps', () => ({
25+
makeAddSentryVitePlugin: vi.fn(() => [mockSourceMapsConfigPlugin, mockSentryVitePlugin]),
26+
makeEnableSourceMapsVitePlugin: vi.fn(() => [mockEnableSourceMapsPlugin]),
27+
}));
28+
29+
describe('sentryTanstackStart()', () => {
30+
beforeEach(() => {
31+
vi.clearAllMocks();
32+
process.env.NODE_ENV = 'production';
33+
});
34+
35+
afterEach(() => {
36+
process.env.NODE_ENV = 'production';
37+
});
38+
39+
it('returns plugins in production mode', () => {
40+
const plugins = sentryTanstackStart({ org: 'test-org' });
41+
42+
expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]);
43+
});
44+
45+
it('returns no plugins in development mode', () => {
46+
process.env.NODE_ENV = 'development';
47+
48+
const plugins = sentryTanstackStart({ org: 'test-org' });
49+
50+
expect(plugins).toEqual([]);
51+
});
52+
53+
it('returns Sentry Vite plugins but not enable source maps plugin when sourcemaps.disable is true', () => {
54+
const plugins = sentryTanstackStart({
55+
sourcemaps: { disable: true },
56+
});
57+
58+
expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin]);
59+
});
60+
61+
it('returns Sentry Vite plugins but not enable source maps plugin when sourcemaps.disable is "disable-upload"', () => {
62+
const plugins = sentryTanstackStart({
63+
sourcemaps: { disable: 'disable-upload' },
64+
});
65+
66+
expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin]);
67+
});
68+
69+
it('returns Sentry Vite plugins and enable source maps plugin when sourcemaps.disable is false', () => {
70+
const plugins = sentryTanstackStart({
71+
sourcemaps: { disable: false },
72+
});
73+
74+
expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]);
75+
});
76+
77+
it('returns Sentry Vite Plugins and enable source maps plugin by default when sourcemaps is not specified', () => {
78+
const plugins = sentryTanstackStart({});
79+
80+
expect(plugins).toEqual([mockSourceMapsConfigPlugin, mockSentryVitePlugin, mockEnableSourceMapsPlugin]);
81+
});
82+
});

0 commit comments

Comments
 (0)