Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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);
},
};
}
177 changes: 177 additions & 0 deletions packages/nitro/src/sourceMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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 ||
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?

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

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

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) {
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.
sentryEnabledSourcemaps = true;
if (moduleOptions?.debug) {
Comment on lines +158 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The function only checks for boolean config.sourcemap values, incorrectly handling valid string options like "inline" by falling into the default case and overwriting the user's setting.
Severity: MEDIUM

Suggested Fix

Update the conditional logic to correctly handle string values for config.sourcemap. Instead of strict boolean checks, use a condition that includes valid string options, such as ['hidden', true].includes(config.sourcemap), to determine if sourcemaps are enabled. This will prevent the else block from incorrectly overwriting the user's configuration.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/nitro/src/sourceMaps.ts#L158-L162

Potential issue: The `configureSourcemapSettings` function checks for `config.sourcemap`
using strict boolean comparisons (`=== true` and `=== false`). When a user provides a
valid string value like `"inline"` or `"hidden"`, these checks fail. The code then
executes the `else` block, which is intended for when the option is unset. This block
incorrectly overwrites the user's setting with `config.sourcemap = true` and enables
`sentryEnabledSourcemaps`. As a result, the user's intended sourcemap configuration is
ignored, and the generated sourcemaps are incorrectly marked for deletion after upload.

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