|
| 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