Skip to content
Merged
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
53 changes: 33 additions & 20 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { addStorageInstrumentation } from './vite/storageConfig';
import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils';

export type ModuleOptions = SentryNuxtModuleOptions;
type NuxtPageSubset = { file?: string; path: string };

export default defineNuxtModule<ModuleOptions>({
meta: {
Expand Down Expand Up @@ -79,6 +80,8 @@ export default defineNuxtModule<ModuleOptions>({

const serverConfigFile = findDefaultSdkInitFile('server', nuxt);
const isNitroV3 = (await getNitroMajorVersion()) >= 3;
const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10);
const isMinNuxtV4 = nuxtMajor >= 4;

if (serverConfigFile) {
if (isNitroV3) {
Expand All @@ -91,10 +94,11 @@ export default defineNuxtModule<ModuleOptions>({

addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server'));

addPlugin({
src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'),
mode: 'server',
});
if (isMinNuxtV4) {
addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), mode: 'server' });
} else {
addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector-legacy.server'), mode: 'server' });
}

// Preps the middleware instrumentation module.
addMiddlewareImports();
Expand All @@ -108,26 +112,35 @@ export default defineNuxtModule<ModuleOptions>({

addOTelCommonJSImportAlias(nuxt, isNitroV3);

const pagesDataTemplate = addTemplate({
filename: 'sentry--nuxt-pages-data.mjs',
// Initial empty array (later filled in pages:extend hook)
// Template needs to be created in the root-level of the module to work
getContents: () => 'export default [];',
});
let pagesData: NuxtPageSubset[] = [];

nuxt.hooks.hook('pages:extend', pages => {
pagesDataTemplate.getContents = () => {
const pagesSubset = pages
.map(page => ({ file: page.file, path: page.path }))
.filter(page => {
// Check for dynamic parameter (e.g., :userId or [userId])
return page.path.includes(':') || page?.file?.includes('[');
});

return `export default ${JSON.stringify(pagesSubset, null, 2)};`;
};
pagesData = pages
.map(page => ({ file: page.file, path: page.path }))
.filter(page => {
// Check for dynamic parameter (e.g., :userId or [userId])
return page.path.includes(':') || page?.file?.includes('[');
});
});

if (isMinNuxtV4) {
const pagesDataVirtualModuleId = '#sentry/nuxt-pages-data.mjs';

// Vite virtual plugin (for the Vite SSR build, where addPlugin mode:'server' plugins are bundled)
addVitePlugin({
name: 'sentry-nuxt-pages-data-virtual',
resolveId: id => (id === pagesDataVirtualModuleId ? `\0${pagesDataVirtualModuleId}` : null),
load: id =>
id === `\0${pagesDataVirtualModuleId}` ? `export default ${JSON.stringify(pagesData, null, 2)};` : undefined,
});
} else {
// Nuxt v3: register as a build template (accessible via #build/)
addTemplate({
filename: 'sentry--nuxt-pages-data.mjs',
getContents: () => `export default ${JSON.stringify(pagesData, null, 2)};`,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fix PR missing regression test for virtual module

Low Severity

This is a fix PR that introduces a new virtual module strategy for Nuxt v4+ and a new legacy plugin file, but doesn't include any unit, integration, or E2E test to validate the regression (SSR route parametrization issue #20010). Adding a test that verifies the virtual module approach works for Nuxt v4+ and the template fallback for Nuxt v3 would help prevent regressions.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

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.

We have Nuxt 4 tests already covering that


// Add the sentry config file to the include array
nuxt.hook('prepare:types', options => {
const tsConfig = options.tsConfig as { include?: string[] };
Expand Down
49 changes: 49 additions & 0 deletions packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { debug, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import { defineNuxtPlugin } from 'nuxt/app';
import type { NuxtPageSubset } from '../utils/route-extraction';
import { extractParametrizedRouteFromContext } from '../utils/route-extraction';

export default defineNuxtPlugin(nuxtApp => {
nuxtApp.hooks.hook('app:rendered', async renderContext => {
let buildTimePagesData: NuxtPageSubset[];
try {
// This is a common Nuxt pattern to import build-time generated data (until Nuxt v3): https://nuxt.com/docs/4.x/api/kit/templates#creating-a-virtual-file-for-runtime-plugin
// @ts-expect-error This import is dynamically resolved at build time (`addTemplate` in module.ts)
const { default: importedPagesData } = await import('#build/sentry--nuxt-pages-data.mjs');
buildTimePagesData = importedPagesData || [];
debug.log('Imported build-time pages data:', buildTimePagesData);
} catch (error) {
buildTimePagesData = [];
debug.warn('Failed to import build-time pages data:', error);
}

const ssrContext = renderContext.ssrContext;

const routeInfo = extractParametrizedRouteFromContext(
ssrContext?.modules,
ssrContext?.url || ssrContext?.event._path,
buildTimePagesData,
);

if (routeInfo === null) {
return;
}

const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined

if (activeSpan && routeInfo.parametrizedRoute) {
const rootSpan = getRootSpan(activeSpan);

if (!rootSpan) {
return;
}

debug.log('Matched parametrized server route:', routeInfo.parametrizedRoute);

rootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'http.route': routeInfo.parametrizedRoute,
});
}
});
});
6 changes: 3 additions & 3 deletions packages/nuxt/src/runtime/plugins/route-detector.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export default defineNuxtPlugin(nuxtApp => {
nuxtApp.hooks.hook('app:rendered', async renderContext => {
let buildTimePagesData: NuxtPageSubset[];
try {
// This is a common Nuxt pattern to import build-time generated data: https://nuxt.com/docs/4.x/api/kit/templates#creating-a-virtual-file-for-runtime-plugin
// @ts-expect-error This import is dynamically resolved at build time (`addTemplate` in module.ts)
const { default: importedPagesData } = await import('#build/sentry--nuxt-pages-data.mjs');
// Virtual module registered via addServerTemplate in module.ts (Nuxt v4+)
// @ts-expect-error - This is a virtual module
const { default: importedPagesData } = await import('#sentry/nuxt-pages-data.mjs');
buildTimePagesData = importedPagesData || [];
debug.log('Imported build-time pages data:', buildTimePagesData);
} catch (error) {
Expand Down
Loading