Skip to content

Commit 3c1d1ce

Browse files
logaretmclaude
andcommitted
feat(nitro): Handle sourcemap preparation and upload (#19304)
Adds automatic sourcemap handling to the Nitro SDK, using `@sentry/bundler-plugin-core` for builder-agnostic post-build upload. Nitro uses rollup or rolldown, so it made sense to make it as agnostic as possible. Closes #17992 --- **This PR is part of a stack:** - #20358 - #19224 - #19225 - #19304 👈 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ec90ba commit 3c1d1ce

6 files changed

Lines changed: 576 additions & 14 deletions

File tree

packages/nitro/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@
3838
"nitro": ">=3.0.0-0 <4.0.0 || 3.0.260311-beta || 3.0.260415-beta"
3939
},
4040
"dependencies": {
41+
"@sentry/bundler-plugin-core": "^5.2.0",
4142
"@sentry/core": "10.49.0",
4243
"@sentry/node": "10.49.0",
4344
"@sentry/opentelemetry": "10.49.0"
4445
},
4546
"devDependencies": {
46-
"h3": "^2.0.1-rc.13",
47-
"nitro": "^3.0.260415-beta"
47+
"nitro": "^3.0.260415-beta",
48+
"h3": "^2.0.1-rc.13"
4849
},
4950
"scripts": {
5051
"build": "run-p build:transpile build:types",

packages/nitro/rollup.npm.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default [
55
makeBaseNPMConfig({
66
entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'],
77
packageSpecificConfig: {
8-
external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/],
8+
external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/, '@sentry/bundler-plugin-core'],
99
},
1010
}),
1111
),

packages/nitro/src/config.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,33 @@
1+
import type { BuildTimeOptionsBase } from '@sentry/core';
12
import type { NitroConfig } from 'nitro/types';
23
import { createNitroModule } from './module';
4+
import { configureSourcemapSettings } from './sourceMaps';
35

4-
type SentryNitroOptions = {
5-
// TODO: Add options
6-
};
6+
export type SentryNitroOptions = BuildTimeOptionsBase;
77

88
/**
99
* Modifies the passed in Nitro configuration with automatic build-time instrumentation.
10-
*
11-
* @param config A Nitro configuration object, as usually exported in `nitro.config.ts` or `nitro.config.mjs`.
12-
* @returns The modified config to be exported
1310
*/
14-
export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitroOptions): NitroConfig {
15-
return setupSentryNitroModule(config, moduleOptions);
11+
export function withSentryConfig(config: NitroConfig, sentryOptions?: SentryNitroOptions): NitroConfig {
12+
return setupSentryNitroModule(config, sentryOptions);
1613
}
1714

1815
/**
1916
* Sets up the Sentry Nitro module, useful for meta framework integrations.
2017
*/
2118
export function setupSentryNitroModule(
2219
config: NitroConfig,
23-
_moduleOptions?: SentryNitroOptions,
20+
moduleOptions?: SentryNitroOptions,
2421
_serverConfigFile?: string,
2522
): NitroConfig {
2623
if (!config.tracingChannel) {
2724
config.tracingChannel = true;
2825
}
2926

27+
const { sentryEnabledSourcemaps } = configureSourcemapSettings(config, moduleOptions);
28+
3029
config.modules = config.modules || [];
31-
config.modules.push(createNitroModule());
30+
config.modules.push(createNitroModule(moduleOptions, sentryEnabledSourcemaps));
3231

3332
return config;
3433
}

packages/nitro/src/module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import type { NitroModule } from 'nitro/types';
2+
import type { SentryNitroOptions } from './config';
23
import { instrumentServer } from './instruments/instrumentServer';
4+
import { setupSourceMaps } from './sourceMaps';
35

46
/**
57
* Creates a Nitro module to setup the Sentry SDK.
68
*/
7-
export function createNitroModule(): NitroModule {
9+
export function createNitroModule(sentryOptions?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): NitroModule {
810
return {
911
name: 'sentry',
1012
setup: nitro => {
1113
instrumentServer(nitro);
14+
setupSourceMaps(nitro, sentryOptions, sentryEnabledSourcemaps);
1215
},
1316
};
1417
}

packages/nitro/src/sourceMaps.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import type { Options as BundlerPluginOptions } from '@sentry/bundler-plugin-core';
2+
import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core';
3+
import type { Nitro, NitroConfig } from 'nitro/types';
4+
import type { SentryNitroOptions } from './config';
5+
6+
/**
7+
* Registers a `compiled` hook to upload source maps after the build completes.
8+
*/
9+
export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): void {
10+
// The `compiled` hook fires on EVERY rebuild during `nitro dev` watch mode.
11+
// nitro.options.dev is reliably set by the time module setup runs.
12+
if (shouldSkipSourcemapUpload(nitro, options)) {
13+
return;
14+
}
15+
16+
nitro.hooks.hook('compiled', async (_nitro: Nitro) => {
17+
await handleSourceMapUpload(_nitro, options, sentryEnabledSourcemaps);
18+
});
19+
}
20+
21+
/**
22+
* Determines if sourcemap uploads should be skipped.
23+
*/
24+
function shouldSkipSourcemapUpload(nitro: Nitro, options?: SentryNitroOptions): boolean {
25+
return !!(
26+
nitro.options.dev ||
27+
nitro.options.preset === 'nitro-prerender' ||
28+
nitro.options.sourcemap === false ||
29+
(nitro.options.sourcemap as unknown) === 'inline' ||
30+
options?.sourcemaps?.disable === true
31+
);
32+
}
33+
34+
/**
35+
* Handles the actual source map upload after the build completes.
36+
*/
37+
async function handleSourceMapUpload(
38+
nitro: Nitro,
39+
options?: SentryNitroOptions,
40+
sentryEnabledSourcemaps?: boolean,
41+
): Promise<void> {
42+
const outputDir = nitro.options.output.serverDir;
43+
const pluginOptions = getPluginOptions(options, sentryEnabledSourcemaps, outputDir);
44+
45+
const sentryBuildPluginManager = createSentryBuildPluginManager(pluginOptions, {
46+
buildTool: 'nitro',
47+
loggerPrefix: '[@sentry/nitro]',
48+
});
49+
50+
await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
51+
await sentryBuildPluginManager.createRelease();
52+
53+
await sentryBuildPluginManager.injectDebugIds([outputDir]);
54+
55+
if (options?.sourcemaps?.disable !== 'disable-upload') {
56+
await sentryBuildPluginManager.uploadSourcemaps([outputDir], {
57+
// We don't prepare the artifacts because we injected debug IDs manually before
58+
prepareArtifacts: false,
59+
});
60+
await sentryBuildPluginManager.deleteArtifacts();
61+
}
62+
}
63+
64+
/**
65+
* Normalizes the beginning of a path from e.g. ../../../ to ./
66+
*/
67+
function normalizePath(path: string): string {
68+
return path.replace(/^(\.\.\/)+/, './');
69+
}
70+
71+
/**
72+
* Removes a trailing slash from a path so glob patterns can be appended cleanly.
73+
*/
74+
function removeTrailingSlash(path: string): string {
75+
return path.replace(/\/$/, '');
76+
}
77+
78+
/**
79+
* Builds the plugin options for `createSentryBuildPluginManager` from the Sentry Nitro options.
80+
*
81+
* Only exported for testing purposes.
82+
*/
83+
// oxlint-disable-next-line complexity
84+
export function getPluginOptions(
85+
options?: SentryNitroOptions,
86+
sentryEnabledSourcemaps?: boolean,
87+
outputDir?: string,
88+
): BundlerPluginOptions {
89+
const defaultFilesToDelete =
90+
sentryEnabledSourcemaps && outputDir ? [`${removeTrailingSlash(outputDir)}/**/*.map`] : undefined;
91+
92+
if (options?.debug && defaultFilesToDelete && options?.sourcemaps?.filesToDeleteAfterUpload === undefined) {
93+
// eslint-disable-next-line no-console
94+
console.log(
95+
`[@sentry/nitro] Setting \`sourcemaps.filesToDeleteAfterUpload: ["${defaultFilesToDelete[0]}"]\` to delete generated source maps after they were uploaded to Sentry.`,
96+
);
97+
}
98+
99+
return {
100+
org: options?.org ?? process.env.SENTRY_ORG,
101+
project: options?.project ?? process.env.SENTRY_PROJECT,
102+
authToken: options?.authToken ?? process.env.SENTRY_AUTH_TOKEN,
103+
url: options?.sentryUrl ?? process.env.SENTRY_URL,
104+
headers: options?.headers,
105+
telemetry: options?.telemetry ?? true,
106+
debug: options?.debug ?? false,
107+
silent: options?.silent ?? false,
108+
errorHandler: options?.errorHandler,
109+
sourcemaps: {
110+
disable: options?.sourcemaps?.disable,
111+
assets: options?.sourcemaps?.assets,
112+
ignore: options?.sourcemaps?.ignore,
113+
filesToDeleteAfterUpload: options?.sourcemaps?.filesToDeleteAfterUpload ?? defaultFilesToDelete,
114+
rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)),
115+
},
116+
release: options?.release,
117+
bundleSizeOptimizations: options?.bundleSizeOptimizations,
118+
_metaOptions: {
119+
telemetry: {
120+
metaFramework: 'nitro',
121+
},
122+
},
123+
};
124+
}
125+
126+
/* Source map configuration rules:
127+
1. User explicitly disabled source maps (sourcemap: false)
128+
- Keep their setting, emit a warning that errors won't be unminified in Sentry
129+
- We will not upload anything
130+
2. User enabled source map generation (true)
131+
- Keep their setting (don't modify besides uploading)
132+
3. User did not set source maps (undefined)
133+
- We enable source maps for Sentry
134+
- Configure `filesToDeleteAfterUpload` to clean up .map files after upload
135+
*/
136+
export function configureSourcemapSettings(
137+
config: NitroConfig,
138+
moduleOptions?: SentryNitroOptions,
139+
): { sentryEnabledSourcemaps: boolean } {
140+
const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true;
141+
if (sourcemapUploadDisabled) {
142+
return { sentryEnabledSourcemaps: false };
143+
}
144+
145+
// Nitro types `sourcemap` as `boolean`, but it forwards the value to Vite which also accepts `'hidden'` and `'inline'`.
146+
const userSourcemap = (config as { sourcemap?: boolean | 'hidden' | 'inline' }).sourcemap;
147+
148+
if (userSourcemap === false) {
149+
// eslint-disable-next-line no-console
150+
console.warn(
151+
'[@sentry/nitro] You have explicitly disabled source maps (`sourcemap: false`). Sentry will not upload source maps, and errors will not be unminified. To let Sentry handle source maps, remove the `sourcemap` option from your Nitro config, or use `sourcemaps: { disable: true }` in your Sentry options to silence this warning.',
152+
);
153+
return { sentryEnabledSourcemaps: false };
154+
}
155+
156+
if (userSourcemap === 'inline') {
157+
// eslint-disable-next-line no-console
158+
console.warn(
159+
'[@sentry/nitro] You have set `sourcemap: "inline"`. Inline source maps are embedded in the output bundle, so there are no `.map` files to upload. Sentry will not upload source maps. Set `sourcemap: "hidden"` (or leave it unset) to let Sentry upload source maps and un-minify errors.',
160+
);
161+
return { sentryEnabledSourcemaps: false };
162+
}
163+
164+
let sentryEnabledSourcemaps = false;
165+
if (userSourcemap === true || userSourcemap === 'hidden') {
166+
if (moduleOptions?.debug) {
167+
// eslint-disable-next-line no-console
168+
console.log(
169+
`[@sentry/nitro] Source maps are already enabled (\`sourcemap: ${JSON.stringify(userSourcemap)}\`). Sentry will upload them for error unminification.`,
170+
);
171+
}
172+
} else {
173+
// User did not explicitly set sourcemap, enable hidden source maps for Sentry.
174+
// `'hidden'` emits .map files without adding a `//# sourceMappingURL=` comment to the output, avoiding public exposure.
175+
(config as { sourcemap?: unknown }).sourcemap = 'hidden';
176+
sentryEnabledSourcemaps = true;
177+
if (moduleOptions?.debug) {
178+
// eslint-disable-next-line no-console
179+
console.log(
180+
'[@sentry/nitro] Enabled hidden source map generation for Sentry. Source map files will be deleted after upload.',
181+
);
182+
}
183+
}
184+
185+
// Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`,
186+
// `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`.
187+
// This makes sourcemaps unusable for Sentry.
188+
config.experimental = config.experimental || {};
189+
config.experimental.sourcemapMinify = false;
190+
191+
return { sentryEnabledSourcemaps };
192+
}

0 commit comments

Comments
 (0)