Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/nitro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@
"nitro": ">=3.0.0-0 <4.0.0 || 3.0.260311-beta || 3.0.260415-beta"
},
"dependencies": {
"@sentry/bundler-plugin-core": "^5.2.0",
"@sentry/core": "10.49.0",
"@sentry/node": "10.49.0",
"@sentry/opentelemetry": "10.49.0"
},
"devDependencies": {
"h3": "^2.0.1-rc.13",
"nitro": "^3.0.260415-beta"
"nitro": "^3.0.260415-beta",
"h3": "^2.0.1-rc.13"
},
"scripts": {
"build": "run-p build:transpile build:types",
Expand Down
2 changes: 1 addition & 1 deletion packages/nitro/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default [
makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/runtime/plugins/server.ts'],
packageSpecificConfig: {
external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/],
external: [/^nitro/, /^h3/, /^srvx/, /^@sentry\/opentelemetry/, '@sentry/bundler-plugin-core'],
},
}),
),
Expand Down
19 changes: 9 additions & 10 deletions packages/nitro/src/config.ts
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;
}
5 changes: 4 additions & 1 deletion packages/nitro/src/module.ts
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);
},
};
}
192 changes: 192 additions & 0 deletions packages/nitro/src/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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 (shouldSkipSourcemapUpload(nitro, options)) {
return;
}

nitro.hooks.hook('compiled', async (_nitro: Nitro) => {
await handleSourceMapUpload(_nitro, options, sentryEnabledSourcemaps);
});
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
sentry[bot] marked this conversation as resolved.
}

/**
* Determines if sourcemap uploads should be skipped.
*/
function shouldSkipSourcemapUpload(nitro: Nitro, options?: SentryNitroOptions): boolean {
return !!(
nitro.options.dev ||
nitro.options.preset === 'nitro-prerender' ||
nitro.options.sourcemap === false ||
(nitro.options.sourcemap as unknown) === 'inline' ||
options?.sourcemaps?.disable === true
);
}

/**
* 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, outputDir);

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried 3 scenarios locally:

  • default: Sourcemaps files get deleted
  • explicit true, sourcemap files is retained
  • disable-upload: retained but not uploaded

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 filesToDeleteAfterUpload option, is that what you meant?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, your scenarios plus:

  • respecting filesToDeleteAfterUpload
  • undefined source maps: setting them to hidden, uploading them and deleting them afterwards

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I did test filesToDeleteAfterUpload and it was respected. For hidden, it isn't part of nitro's v3 API but it works under the hood since it forwards it to vite so i'm not sure. I will follow your suggestion still.

}
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
logaretm marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcemap disable passed redundantly to plugin manager

Medium Severity

The sourcemaps.disable value (e.g., 'disable-upload') is passed to the bundler plugin manager via getPluginOptions at line 110, AND the same value is manually checked at line 55 to skip uploadSourcemaps/deleteArtifacts. If the plugin manager internally also honors disable: 'disable-upload' (as it does in other SDKs), these calls would be no-ops anyway. But if the plugin manager treats 'disable-upload' as also suppressing createRelease or injectDebugIds internally, those calls on lines 51 and 53 could silently fail. The Next.js implementation avoids this by letting the plugin manager handle disable internally rather than double-gating. Consider either not passing disable to the plugin options (letting the manual orchestration control the flow) or removing the manual check and letting the plugin manager handle it, but not both.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 26a6dd2. Configure here.

}

/**
* Normalizes the beginning of a path from e.g. ../../../ to ./
*/
function normalizePath(path: string): string {
return path.replace(/^(\.\.\/)+/, './');
}

/**
* Removes a trailing slash from a path so glob patterns can be appended cleanly.
*/
function removeTrailingSlash(path: string): string {
return path.replace(/\/$/, '');
}

/**
* Builds the plugin options for `createSentryBuildPluginManager` from the Sentry Nitro options.
*
* Only exported for testing purposes.
*/
// oxlint-disable-next-line complexity
export function getPluginOptions(
options?: SentryNitroOptions,
sentryEnabledSourcemaps?: boolean,
outputDir?: string,
): BundlerPluginOptions {
const defaultFilesToDelete =
sentryEnabledSourcemaps && outputDir ? [`${removeTrailingSlash(outputDir)}/**/*.map`] : undefined;

if (options?.debug && defaultFilesToDelete && options?.sourcemaps?.filesToDeleteAfterUpload === undefined) {
// eslint-disable-next-line no-console
console.log(
`[@sentry/nitro] Setting \`sourcemaps.filesToDeleteAfterUpload: ["${defaultFilesToDelete[0]}"]\` to delete generated source maps after they were uploaded to Sentry.`,
);
}

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 ?? defaultFilesToDelete,
rewriteSources: options?.sourcemaps?.rewriteSources ?? ((source: string) => normalizePath(source)),
},
release: options?.release,
bundleSizeOptimizations: options?.bundleSizeOptimizations,
_metaOptions: {
telemetry: {
metaFramework: 'nitro',
},
},
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
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 };
}

// Nitro types `sourcemap` as `boolean`, but it forwards the value to Vite which also accepts `'hidden'` and `'inline'`.
const userSourcemap = (config as { sourcemap?: boolean | 'hidden' | 'inline' }).sourcemap;

if (userSourcemap === 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 };
}

if (userSourcemap === 'inline') {
// eslint-disable-next-line no-console
console.warn(
'[@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.',
);
return { sentryEnabledSourcemaps: false };
}

let sentryEnabledSourcemaps = false;
if (userSourcemap === true || userSourcemap === 'hidden') {
if (moduleOptions?.debug) {
// eslint-disable-next-line no-console
console.log(
`[@sentry/nitro] Source maps are already enabled (\`sourcemap: ${JSON.stringify(userSourcemap)}\`). Sentry will upload them for error unminification.`,
);
}
} else {
// User did not explicitly set sourcemap, enable hidden source maps for Sentry.
// `'hidden'` emits .map files without adding a `//# sourceMappingURL=` comment to the output, avoiding public exposure.
(config as { sourcemap?: unknown }).sourcemap = 'hidden';
sentryEnabledSourcemaps = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source map config modified unconditionally including dev mode

Medium Severity

configureSourcemapSettings unconditionally sets config.sourcemap = 'hidden' at config construction time, affecting both dev and production builds. While shouldSkipSourcemapUpload correctly skips the upload hook in dev mode, the config-level sourcemap change to 'hidden' persists. This differs from the Nuxt implementation, which defers source map modifications to a lifecycle hook guarded by !nuxt.options.dev. Setting 'hidden' in dev mode removes the //# sourceMappingURL= comment, potentially degrading Node.js source map support during development.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3f30c9a. Configure here.

if (moduleOptions?.debug) {
Comment thread
sentry[bot] marked this conversation as resolved.
// eslint-disable-next-line no-console
console.log(
Comment thread
sentry[bot] marked this conversation as resolved.
'[@sentry/nitro] Enabled hidden source map generation for Sentry. Source map files will be deleted after upload.',
);
}
}
Comment thread
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 };
}
Loading
Loading