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
16 changes: 16 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,22 @@ The deprecated `sourceMapsUploadOptions` and other deprecated Vite/build plugin

### `@sentry/nuxt`

Removed support for the `public/instrument.server.[ext]` file. Move the file to the root of your project, next to `nuxt.config.ts`, and rename it to `sentry.server.config.[ext]`. Its contents do not change.

```
// before
public/instrument.server.ts

// after
sentry.server.config.ts
```

After the rename, the SDK also emits `.output/server/sentry.server.config.mjs` for you to preload:

```bash
node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs
```

The deprecated `sourceMapsUploadOptions` module option was removed. Move its fields to the root level of the `sentry` module options. Note that `url` was renamed to `sentryUrl`, and `enabled` was replaced by `sourcemaps.disable` (inverted: `enabled: false` becomes `sourcemaps: { disable: true }`).

```ts
Expand Down
9 changes: 9 additions & 0 deletions packages/nuxt/src/common/devMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GLOBAL_OBJ } from '@sentry/core';

/** Global flag set by the generated `<buildDir>/dev/sentry.server.config.mjs`. */
export const NUXT_DEV_MODE_FLAG = '__SENTRY_NUXT_DEV_MODE__';

/** Whether the SDK was preloaded by the generated `nuxt dev` server config file. */
export function isNuxtDevRuntime(): boolean {
return NUXT_DEV_MODE_FLAG in GLOBAL_OBJ && GLOBAL_OBJ[NUXT_DEV_MODE_FLAG] === true;
}
34 changes: 27 additions & 7 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ import type {} from '@nuxt/nitro-server';
import { consoleSandbox } from '@sentry/core';
import * as path from 'path';
import type { SentryNuxtModuleOptions } from './common/types';
import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig';
import {
addDevServerConfigFile,
addDynamicImportEntryFileWrapper,
addSentryTopImport,
addServerConfigToBuild,
DEV_SERVER_CONFIG_PATH,
} from './vite/addServerConfig';
import { addDatabaseInstrumentation } from './vite/databaseConfig';
import { addMiddlewareImports, addMiddlewareInstrumentation } from './vite/middlewareConfig';
import { setupOrchestrion } from './vite/orchestrion';
import { setupSourceMaps } from './vite/sourceMaps';
import { addStorageInstrumentation } from './vite/storageConfig';
import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils';
import {
addOTelCommonJSImportAlias,
findDefaultSdkInitFile,
getNitroMajorVersion,
toImportSpecifier,
} from './vite/utils';

export type ModuleOptions = SentryNuxtModuleOptions;
type NuxtPageSubset = { file?: string; path: string };
Expand Down Expand Up @@ -113,6 +124,11 @@ export default defineNuxtModule<ModuleOptions>({
addMiddlewareImports();
addStorageInstrumentation(nuxt, !isNitroV3);
addDatabaseInstrumentation(nuxt.options.nitro, !isNitroV3, moduleOptions);

// Outside `nitro:init` so that `nuxt prepare` writes the file before the first `nuxt dev`.
if (isNitroV3) {
addDevServerConfigFile(nuxt, serverConfigFile);
}
}

if (clientConfigFile || serverConfigFile) {
Expand Down Expand Up @@ -177,9 +193,7 @@ export default defineNuxtModule<ModuleOptions>({

if (serverConfigFile) {
addMiddlewareInstrumentation(nitro);
}

if (serverConfigFile?.includes('.server.config')) {
consoleSandbox(() => {
const serverDir = nitro.options.output.serverDir;

Expand All @@ -201,14 +215,20 @@ export default defineNuxtModule<ModuleOptions>({
});

if (moduleOptions.autoInjectServerSentry !== 'experimental_dynamic-import') {
addServerConfigToBuild(moduleOptions, nitro, serverConfigFile);
// Nitro 3 (in Nuxt 5) is not bundled in dev mode. See `addDevServerConfigFile` for how we add the file now.
if (!(isNitroV3 && nitro.options.dev)) {
addServerConfigToBuild(moduleOptions, nitro, serverConfigFile);
}

if (moduleOptions.debug) {
const serverDirResolver = createResolver(nitro.options.output.serverDir);
const serverConfigPath = serverDirResolver.resolve('sentry.server.config.mjs');

// For the default nitro node-preset build output this relative path would be: ./.output/server/sentry.server.config.mjs
const serverConfigRelativePath = `.${path.sep}${path.relative(nitro.options.rootDir, serverConfigPath)}`;
const serverConfigRelativePath = toImportSpecifier(nitro.options.rootDir, serverConfigPath);
const devConfigRelativePath = isNitroV3
? toImportSpecifier(nuxt.options.rootDir, path.join(nuxt.options.buildDir, DEV_SERVER_CONFIG_PATH))
: serverConfigRelativePath;

consoleSandbox(() => {
// eslint-disable-next-line no-console
Expand All @@ -219,7 +239,7 @@ export default defineNuxtModule<ModuleOptions>({
if (nitro.options.dev) {
// eslint-disable-next-line no-console
console.log(
`[Sentry] During development, preload Sentry with the NODE_OPTIONS environment variable: \`NODE_OPTIONS='--import ${serverConfigRelativePath}' nuxt dev\`. The file is generated in the build directory (usually '.nuxt'). If you delete the build directory, run \`nuxt dev\` to regenerate it.`,
`[Sentry] During development, preload Sentry with the NODE_OPTIONS environment variable: \`NODE_OPTIONS='--import ${devConfigRelativePath}' nuxt dev\`. The file is generated in the build directory (usually '.nuxt'). If you delete the build directory, run \`nuxt prepare\` to regenerate it.`,
);
} else {
// eslint-disable-next-line no-console
Expand Down
12 changes: 6 additions & 6 deletions packages/nuxt/src/server/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Client, Event, EventProcessor } from '@sentry/core';
import { applySdkMetadata, debug, DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT, getGlobalScope } from '@sentry/core';
import { init as initNode } from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { isNuxtDevRuntime } from '../common/devMode';
import type { SentryNuxtServerOptions } from '../common/types';

/**
Expand All @@ -11,15 +12,14 @@ import type { SentryNuxtServerOptions } from '../common/types';
* @param options Configuration options for the SDK.
*/
export function init(options: SentryNuxtServerOptions): Client | undefined {
let envFallback: string;
/*! rollup-include-cjs-only */
envFallback = DEFAULT_ENVIRONMENT;
/*! rollup-include-cjs-only-end */

let isDevBuild = false;
/*! rollup-include-esm-only */
envFallback = import.meta.dev ? DEV_ENVIRONMENT : DEFAULT_ENVIRONMENT;
isDevBuild = !!import.meta.dev;
/*! rollup-include-esm-only-end */

// Nitro v3 does not bundle the Sentry server config file, so `import.meta.dev` stays undefined there

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.

nit: should this comment be above the isDevBuild = !!import.meta.dev line?

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.

doesn't really matter actually 🤔 but this one is more related to isNuxtDevRuntime()

const envFallback = isDevBuild || isNuxtDevRuntime() ? DEV_ENVIRONMENT : DEFAULT_ENVIRONMENT;

const sentryOptions = {
environment: options.environment ?? process.env.SENTRY_ENVIRONMENT ?? envFallback,
...options,
Expand Down
46 changes: 44 additions & 2 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { existsSync } from 'node:fs';
import { createResolver } from '@nuxt/kit';
import { pathToFileURL } from 'node:url';
import { addTemplate, createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { debug } from '@sentry/core';
import * as fs from 'fs';
import type { Nitro } from 'nitropack';
import * as path from 'path';
import type { InputPluginOption } from 'rollup';
import { NUXT_DEV_MODE_FLAG } from '../common/devMode';
import type { SentryNuxtModuleOptions } from '../common/types';
import {
constructFunctionReExport,
Expand All @@ -14,9 +18,47 @@ import {
SENTRY_REEXPORTED_FUNCTIONS,
SENTRY_WRAPPED_ENTRY,
SENTRY_WRAPPED_FUNCTIONS,
SERVER_CONFIG_FILENAME,
toImportSpecifier,
} from './utils';

const SERVER_CONFIG_FILENAME = 'sentry.server.config';
/** Path of the generated dev-mode config file, relative to the Nuxt build directory. */
export const DEV_SERVER_CONFIG_PATH = `dev/${SERVER_CONFIG_FILENAME}.mjs`;

/**
* Writes the file users preload with `node --import` to enable Sentry in `nuxt dev` (for Nuxt 5 with Nitro 3).
*
* In dev-mode, Nitro v3 has no server bundle to emit into, so Node loads the server config file as it is written.
*/
export function addDevServerConfigFile(nuxt: Nuxt, serverConfigFile: string): void {
const configPath = createResolver(nuxt.options.rootDir).resolve(`/${serverConfigFile}`);
const importSpecifier = toImportSpecifier(
nuxt.options.rootDir,
path.join(nuxt.options.buildDir, DEV_SERVER_CONFIG_PATH),
);

const failureMessage =
`[Sentry] Could not load \`${path.basename(configPath)}\`, so Sentry is disabled during development. ` +
'Node loads this file without a build step, so it supports neither path aliases (like #import) nor non-erasable TypeScript syntax (like enums).';

addTemplate({
filename: DEV_SERVER_CONFIG_PATH,
write: true,
getContents: () =>
[
'// Generated by @sentry/nuxt. Preload it to enable Sentry during development:',
`// NODE_OPTIONS='--import ${importSpecifier}' nuxt dev`,
// A static import would hoist above this assignment, and would make a broken config crash the dev server.
`globalThis.${NUXT_DEV_MODE_FLAG} = true;`,
'try {',
` await import(${JSON.stringify(pathToFileURL(configPath).href)});`,
'} catch (error) {',
` console.warn(${JSON.stringify(failureMessage)}, error);`,
'}',
'',
].join('\n'),
});
}

/**
* Adds the `sentry.server.config.ts` file as `sentry.server.config.mjs` to the `.output` directory to be able to reference this file in the node --import option.
Expand Down
21 changes: 8 additions & 13 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,14 @@ export async function getNitroMajorVersion(): Promise<number> {

/**
* Find the default SDK init file for the given type (client or server).
* The sentry.server.config file is prioritized over the instrument.server file.
*/
export async function findDefaultSdkInitFile(
type: 'server' | 'client',
nuxt?: Nuxt,
options?: SentryNuxtModuleOptions,
): Promise<string | undefined> {
const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'];
const relativePaths: string[] = [];

if (type === 'server') {
for (const ext of possibleFileExtensions) {
relativePaths.push(`sentry.${type}.config.${ext}`);
relativePaths.push(path.join('public', `instrument.${type}.${ext}`));
}
} else {
for (const ext of possibleFileExtensions) {
relativePaths.push(`sentry.${type}.config.${ext}`);
}
}
const relativePaths = possibleFileExtensions.map(ext => `sentry.${type}.config.${ext}`);

// Get layers from highest priority to lowest
const layers = [...(nuxt?.options._layers ?? [])].reverse();
Expand All @@ -70,6 +58,13 @@ export async function findDefaultSdkInitFile(
return undefined;
}

export const SERVER_CONFIG_FILENAME = 'sentry.server.config';

/** Builds the value for `node --import`. Node reads it as a URL, so it needs forward slashes on Windows too. */
export function toImportSpecifier(fromDir: string, filePath: string): string {
return `./${path.relative(fromDir, filePath).split(/[\\/]/).join('/')}`;
}

/**
* Extracts the filename from a node command with a path.
*/
Expand Down
28 changes: 28 additions & 0 deletions packages/nuxt/test/server/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Event, EventProcessor } from '@sentry/core';
import * as SentryNode from '@sentry/node';
import { getGlobalScope, Scope, SDK_VERSION } from '@sentry/node';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NUXT_DEV_MODE_FLAG } from '../../src/common/devMode';
import { init } from '../../src/server';
import { clientSourceMapErrorFilter, lowQualityTransactionsFilter } from '../../src/server/sdk';

Expand Down Expand Up @@ -126,6 +127,33 @@ describe('Nuxt Server SDK', () => {
expect(callArgs?.environment).toBeDefined();
});

it('falls back to the dev environment when preloaded by the generated dev config file', () => {
const globalWithFlag = globalThis as { __SENTRY_NUXT_DEV_MODE__?: boolean };

// The generated file sets this by name, so a rename must break the test rather than the runtime.
expect(NUXT_DEV_MODE_FLAG).toBe('__SENTRY_NUXT_DEV_MODE__');

globalWithFlag.__SENTRY_NUXT_DEV_MODE__ = true;

try {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});

expect(nodeInit).toHaveBeenCalledWith(expect.objectContaining({ environment: 'development' }));
} finally {
globalWithFlag.__SENTRY_NUXT_DEV_MODE__ = undefined;
}
});

it('falls back to the production environment without the dev flag', () => {
init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
});

expect(nodeInit).toHaveBeenCalledWith(expect.objectContaining({ environment: 'production' }));
});

it('prioritizes options.environment over SENTRY_ENVIRONMENT env var', () => {
process.env.SENTRY_ENVIRONMENT = 'env-from-variable';

Expand Down
75 changes: 75 additions & 0 deletions packages/nuxt/test/vite/addServerConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { Nuxt } from '@nuxt/schema';
import * as path from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { addDevServerConfigFile, DEV_SERVER_CONFIG_PATH } from '../../src/vite/addServerConfig';

const addTemplateMock = vi.hoisted(() => vi.fn());

vi.mock('@nuxt/kit', () => ({
addTemplate: addTemplateMock,
// `@nuxt/kit` resolves rather than joins, which is what lets an absolute layer path win over the base.
createResolver: (base: string) => ({ resolve: (input: string) => path.resolve(base, input) }),
}));

const APP_ROOT = '/my/monorepo/apps/web';
// `findDefaultSdkInitFile` always returns an absolute path, built from the layer's own `cwd`.
const APP_CONFIG = `${APP_ROOT}/sentry.server.config.ts`;
const LAYER_CONFIG = '/my/monorepo/layers/base/sentry.server.config.ts';

function generate(serverConfigFile: string): string {
const nuxt = { options: { rootDir: APP_ROOT, buildDir: path.join(APP_ROOT, '.nuxt') } } as Nuxt;

addDevServerConfigFile(nuxt, serverConfigFile);

return addTemplateMock.mock.calls[0]?.[0].getContents();
}

describe('addDevServerConfigFile', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('writes the file into the build directory so `--import` can resolve it', () => {
generate(APP_CONFIG);

expect(addTemplateMock).toHaveBeenCalledWith({
filename: DEV_SERVER_CONFIG_PATH,
write: true,
getContents: expect.any(Function),
});
});

it('imports the user config as a file URL so Node can load it directly', () => {
expect(generate(APP_CONFIG)).toContain(`await import("file://${APP_CONFIG}")`);
});

it('sets the dev flag before importing the config', () => {
const contents = generate(APP_CONFIG);

// A static import would be hoisted above the assignment and `Sentry.init()` would then see no flag.
expect(contents).not.toMatch(/^import /m);
expect(contents.indexOf('__SENTRY_NUXT_DEV_MODE__')).toBeLessThan(contents.indexOf('await import('));
});

it('catches a config Node cannot load, so a broken config does not stop the dev server', () => {
const contents = generate(APP_CONFIG);

expect(contents).toMatch(/try \{[\s\S]*await import\([\s\S]*\} catch \(error\) \{[\s\S]*console\.warn\(/);
expect(contents).toContain('Could not load `sentry.server.config.ts`');
});

it('documents the command that preloads the file', () => {
expect(generate(APP_CONFIG)).toContain("NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs'");
});

describe('when the config comes from a layer outside the project root', () => {
it('imports the config from the layer it belongs to', () => {
expect(generate(LAYER_CONFIG)).toContain(`await import("file://${LAYER_CONFIG}")`);
});

it('keeps the preload path relative to the project root', () => {
// The file we generate always lives in the app's own build directory, wherever the config came from.
expect(generate(LAYER_CONFIG)).toContain("NODE_OPTIONS='--import ./.nuxt/dev/sentry.server.config.mjs'");
});
});
});
Loading
Loading