-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(nitro): Handle sourcemap preparation and upload #19304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
572a830
d3b0f0f
06d6b67
5117570
ea8606e
d114ac3
af91304
4a4a873
2f93472
4195464
80bc2ee
fb181b5
14b31ea
52626ee
1c13825
36ddac0
3f30c9a
26a6dd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,33 @@ | ||
| import type { BuildTimeOptionsBase } from '@sentry/core'; | ||
| import type { NitroConfig } from 'nitro/types'; | ||
| import { createNitroModule } from './module'; | ||
| import { configureSourcemapSettings } from './sourceMaps'; | ||
|
|
||
| type SentryNitroOptions = { | ||
| // TODO: Add options | ||
| }; | ||
| export type SentryNitroOptions = BuildTimeOptionsBase; | ||
|
|
||
| /** | ||
| * Modifies the passed in Nitro configuration with automatic build-time instrumentation. | ||
| * | ||
| * @param config A Nitro configuration object, as usually exported in `nitro.config.ts` or `nitro.config.mjs`. | ||
| * @returns The modified config to be exported | ||
| */ | ||
| export function withSentryConfig(config: NitroConfig, moduleOptions?: SentryNitroOptions): NitroConfig { | ||
| return setupSentryNitroModule(config, moduleOptions); | ||
| export function withSentryConfig(config: NitroConfig, sentryOptions?: SentryNitroOptions): NitroConfig { | ||
| return setupSentryNitroModule(config, sentryOptions); | ||
| } | ||
|
|
||
| /** | ||
| * Sets up the Sentry Nitro module, useful for meta framework integrations. | ||
| */ | ||
| export function setupSentryNitroModule( | ||
| config: NitroConfig, | ||
| _moduleOptions?: SentryNitroOptions, | ||
| moduleOptions?: SentryNitroOptions, | ||
| _serverConfigFile?: string, | ||
| ): NitroConfig { | ||
| if (!config.tracingChannel) { | ||
| config.tracingChannel = true; | ||
| } | ||
|
|
||
| const { sentryEnabledSourcemaps } = configureSourcemapSettings(config, moduleOptions); | ||
|
|
||
| config.modules = config.modules || []; | ||
| config.modules.push(createNitroModule()); | ||
| config.modules.push(createNitroModule(moduleOptions, sentryEnabledSourcemaps)); | ||
|
|
||
| return config; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,17 @@ | ||
| import type { NitroModule } from 'nitro/types'; | ||
| import type { SentryNitroOptions } from './config'; | ||
| import { instrumentServer } from './instruments/instrumentServer'; | ||
| import { setupSourceMaps } from './sourceMaps'; | ||
|
|
||
| /** | ||
| * Creates a Nitro module to setup the Sentry SDK. | ||
| */ | ||
| export function createNitroModule(): NitroModule { | ||
| export function createNitroModule(sentryOptions?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): NitroModule { | ||
| return { | ||
| name: 'sentry', | ||
| setup: nitro => { | ||
| instrumentServer(nitro); | ||
| setupSourceMaps(nitro, sentryOptions, sentryEnabledSourcemaps); | ||
| }, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,158 @@ | ||||||
| import type { Options as BundlerPluginOptions } from '@sentry/bundler-plugin-core'; | ||||||
| import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core'; | ||||||
| import type { Nitro, NitroConfig } from 'nitro/types'; | ||||||
| import type { SentryNitroOptions } from './config'; | ||||||
|
|
||||||
| /** | ||||||
| * Registers a `compiled` hook to upload source maps after the build completes. | ||||||
| */ | ||||||
| export function setupSourceMaps(nitro: Nitro, options?: SentryNitroOptions, sentryEnabledSourcemaps?: boolean): void { | ||||||
| // The `compiled` hook fires on EVERY rebuild during `nitro dev` watch mode. | ||||||
| // nitro.options.dev is reliably set by the time module setup runs. | ||||||
| if (nitro.options.dev) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Nitro spawns a nested Nitro instance for prerendering with the user's `modules` re-installed. | ||||||
| // Uploading here would double-upload source maps and create a duplicate release. | ||||||
| if (nitro.options.preset === 'nitro-prerender') { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| // Respect user's explicit disable | ||||||
| if (options?.sourcemaps?.disable === true) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| nitro.hooks.hook('compiled', async (_nitro: Nitro) => { | ||||||
| await handleSourceMapUpload(_nitro, options, sentryEnabledSourcemaps); | ||||||
| }); | ||||||
|
cursor[bot] marked this conversation as resolved.
sentry[bot] marked this conversation as resolved.
|
||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Handles the actual source map upload after the build completes. | ||||||
| */ | ||||||
| async function handleSourceMapUpload( | ||||||
| nitro: Nitro, | ||||||
| options?: SentryNitroOptions, | ||||||
| sentryEnabledSourcemaps?: boolean, | ||||||
| ): Promise<void> { | ||||||
| const outputDir = nitro.options.output.serverDir; | ||||||
| const pluginOptions = getPluginOptions(options, sentryEnabledSourcemaps); | ||||||
|
|
||||||
| const sentryBuildPluginManager = createSentryBuildPluginManager(pluginOptions, { | ||||||
| buildTool: 'nitro', | ||||||
| loggerPrefix: '[@sentry/nitro]', | ||||||
| }); | ||||||
|
|
||||||
| await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); | ||||||
| await sentryBuildPluginManager.createRelease(); | ||||||
|
|
||||||
| await sentryBuildPluginManager.injectDebugIds([outputDir]); | ||||||
|
|
||||||
| if (options?.sourcemaps?.disable !== 'disable-upload') { | ||||||
| await sentryBuildPluginManager.uploadSourcemaps([outputDir], { | ||||||
| // We don't prepare the artifacts because we injected debug IDs manually before | ||||||
| prepareArtifacts: false, | ||||||
| }); | ||||||
| await sentryBuildPluginManager.deleteArtifacts(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you manually also check if this deletes the correct source maps - based on the user-set source map setting?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried 3 scenarios locally:
I did miss that it should only delete nitro's sourcemaps (fixed now in 1c13825) but not sure what other scenarios to take into account. Also checked that it respects the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, your scenarios plus:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep I did test |
||||||
| } | ||||||
|
sentry[bot] marked this conversation as resolved.
logaretm marked this conversation as resolved.
cursor[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sourcemap
|
||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Normalizes the beginning of a path from e.g. ../../../ to ./ | ||||||
| */ | ||||||
| function normalizePath(path: string): string { | ||||||
| return path.replace(/^(\.\.\/)+/, './'); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Builds the plugin options for `createSentryBuildPluginManager` from the Sentry Nitro options. | ||||||
| * | ||||||
| * Only exported for testing purposes. | ||||||
| */ | ||||||
| export function getPluginOptions( | ||||||
| options?: SentryNitroOptions, | ||||||
| sentryEnabledSourcemaps?: boolean, | ||||||
| ): BundlerPluginOptions { | ||||||
| return { | ||||||
| org: options?.org ?? process.env.SENTRY_ORG, | ||||||
| project: options?.project ?? process.env.SENTRY_PROJECT, | ||||||
| authToken: options?.authToken ?? process.env.SENTRY_AUTH_TOKEN, | ||||||
| url: options?.sentryUrl ?? process.env.SENTRY_URL, | ||||||
| headers: options?.headers, | ||||||
| telemetry: options?.telemetry ?? true, | ||||||
| debug: options?.debug ?? false, | ||||||
| silent: options?.silent ?? false, | ||||||
| errorHandler: options?.errorHandler, | ||||||
| sourcemaps: { | ||||||
| disable: options?.sourcemaps?.disable, | ||||||
| assets: options?.sourcemaps?.assets, | ||||||
| ignore: options?.sourcemaps?.ignore, | ||||||
| filesToDeleteAfterUpload: | ||||||
| options?.sourcemaps?.filesToDeleteAfterUpload ?? (sentryEnabledSourcemaps ? ['**/*.map'] : undefined), | ||||||
| rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)), | ||||||
| }, | ||||||
| release: options?.release, | ||||||
| bundleSizeOptimizations: options?.bundleSizeOptimizations, | ||||||
| _metaOptions: { | ||||||
| telemetry: { | ||||||
| metaFramework: 'nitro', | ||||||
| }, | ||||||
| }, | ||||||
|
cursor[bot] marked this conversation as resolved.
sentry[bot] marked this conversation as resolved.
|
||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /* Source map configuration rules: | ||||||
| 1. User explicitly disabled source maps (sourcemap: false) | ||||||
| - Keep their setting, emit a warning that errors won't be unminified in Sentry | ||||||
| - We will not upload anything | ||||||
| 2. User enabled source map generation (true) | ||||||
| - Keep their setting (don't modify besides uploading) | ||||||
| 3. User did not set source maps (undefined) | ||||||
| - We enable source maps for Sentry | ||||||
| - Configure `filesToDeleteAfterUpload` to clean up .map files after upload | ||||||
| */ | ||||||
| export function configureSourcemapSettings( | ||||||
| config: NitroConfig, | ||||||
| moduleOptions?: SentryNitroOptions, | ||||||
| ): { sentryEnabledSourcemaps: boolean } { | ||||||
| const sourcemapUploadDisabled = moduleOptions?.sourcemaps?.disable === true; | ||||||
| if (sourcemapUploadDisabled) { | ||||||
| return { sentryEnabledSourcemaps: false }; | ||||||
| } | ||||||
|
|
||||||
| if (config.sourcemap === false) { | ||||||
| // eslint-disable-next-line no-console | ||||||
| console.warn( | ||||||
| '[@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.', | ||||||
| ); | ||||||
| return { sentryEnabledSourcemaps: false }; | ||||||
| } | ||||||
|
|
||||||
| let sentryEnabledSourcemaps = false; | ||||||
| if (config.sourcemap === true) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They could also be enabled with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not part of Nitro's v3 API, it's always a boolean but |
||||||
| if (moduleOptions?.debug) { | ||||||
| // eslint-disable-next-line no-console | ||||||
| console.log('[@sentry/nitro] Source maps are already enabled. Sentry will upload them for error unminification.'); | ||||||
| } | ||||||
| } else { | ||||||
| // User did not explicitly set sourcemap — enable it for Sentry | ||||||
| config.sourcemap = true; | ||||||
|
cursor[bot] marked this conversation as resolved.
Outdated
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It's safer to set them to hidden, if they are not already set by something by the user.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not part of nitro's API for some reason but it works, so I will follow your suggestion. |
||||||
| sentryEnabledSourcemaps = true; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Source map config modified unconditionally including dev modeMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 3f30c9a. Configure here. |
||||||
| if (moduleOptions?.debug) { | ||||||
|
sentry[bot] marked this conversation as resolved.
|
||||||
| // eslint-disable-next-line no-console | ||||||
| console.log( | ||||||
|
sentry[bot] marked this conversation as resolved.
|
||||||
| '[@sentry/nitro] Enabled source map generation for Sentry. Source map files will be deleted after upload.', | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
|
cursor[bot] marked this conversation as resolved.
|
||||||
|
|
||||||
| // Nitro v3 has a `sourcemapMinify` plugin that destructively deletes `sourcesContent`, | ||||||
| // `x_google_ignoreList`, and clears `mappings` for any chunk containing `node_modules`. | ||||||
| // This makes sourcemaps unusable for Sentry. | ||||||
| config.experimental = config.experimental || {}; | ||||||
| config.experimental.sourcemapMinify = false; | ||||||
|
|
||||||
| return { sentryEnabledSourcemaps }; | ||||||
| } | ||||||


Uh oh!
There was an error while loading. Please reload this page.