Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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);
},
};
}
158 changes: 158 additions & 0 deletions packages/nitro/src/sourceMaps.ts
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;
}
Comment thread
sentry[bot] marked this conversation as resolved.
Outdated

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

/**
* 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();
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(/^(\.\.\/)+/, './');
}

/**
* 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',
},
},
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 };
}

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) {
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.

They could also be enabled with 'inline' | 'hidden'

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.

Not part of Nitro's v3 API, it's always a boolean but hidden would work through vite still so I'm not sure what to do here.

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;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
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.

Suggested change
config.sourcemap = true;
config.sourcemap = 'hidden';

It's safer to set them to hidden, if they are not already set by something by the user.

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.

It's not part of nitro's API for some reason but it works, so I will follow your suggestion.

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