Skip to content

Commit ae74361

Browse files
authored
fix(nuxt): Use virtual module for Nuxt pages data (SSR route parametrization) (#20020)
Creates a virtual module with Vite when using Nuxt 4+ instead of creating a template. `useServerTemplate()` cannot be used here as it's not Nitro-only but the SSR-space (server) within Nuxt. Closes #20010
1 parent 73f03bb commit ae74361

File tree

3 files changed

+85
-23
lines changed

3 files changed

+85
-23
lines changed

packages/nuxt/src/module.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { addStorageInstrumentation } from './vite/storageConfig';
1818
import { addOTelCommonJSImportAlias, findDefaultSdkInitFile, getNitroMajorVersion } from './vite/utils';
1919

2020
export type ModuleOptions = SentryNuxtModuleOptions;
21+
type NuxtPageSubset = { file?: string; path: string };
2122

2223
export default defineNuxtModule<ModuleOptions>({
2324
meta: {
@@ -79,6 +80,8 @@ export default defineNuxtModule<ModuleOptions>({
7980

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

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

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

94-
addPlugin({
95-
src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'),
96-
mode: 'server',
97-
});
97+
if (isMinNuxtV4) {
98+
addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector.server'), mode: 'server' });
99+
} else {
100+
addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/route-detector-legacy.server'), mode: 'server' });
101+
}
98102

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

109113
addOTelCommonJSImportAlias(nuxt, isNitroV3);
110114

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

118117
nuxt.hooks.hook('pages:extend', pages => {
119-
pagesDataTemplate.getContents = () => {
120-
const pagesSubset = pages
121-
.map(page => ({ file: page.file, path: page.path }))
122-
.filter(page => {
123-
// Check for dynamic parameter (e.g., :userId or [userId])
124-
return page.path.includes(':') || page?.file?.includes('[');
125-
});
126-
127-
return `export default ${JSON.stringify(pagesSubset, null, 2)};`;
128-
};
118+
pagesData = pages
119+
.map(page => ({ file: page.file, path: page.path }))
120+
.filter(page => {
121+
// Check for dynamic parameter (e.g., :userId or [userId])
122+
return page.path.includes(':') || page?.file?.includes('[');
123+
});
129124
});
130125

126+
if (isMinNuxtV4) {
127+
const pagesDataVirtualModuleId = '#sentry/nuxt-pages-data.mjs';
128+
129+
// Vite virtual plugin (for the Vite SSR build, where addPlugin mode:'server' plugins are bundled)
130+
addVitePlugin({
131+
name: 'sentry-nuxt-pages-data-virtual',
132+
resolveId: id => (id === pagesDataVirtualModuleId ? `\0${pagesDataVirtualModuleId}` : null),
133+
load: id =>
134+
id === `\0${pagesDataVirtualModuleId}` ? `export default ${JSON.stringify(pagesData, null, 2)};` : undefined,
135+
});
136+
} else {
137+
// Nuxt v3: register as a build template (accessible via #build/)
138+
addTemplate({
139+
filename: 'sentry--nuxt-pages-data.mjs',
140+
getContents: () => `export default ${JSON.stringify(pagesData, null, 2)};`,
141+
});
142+
}
143+
131144
// Add the sentry config file to the include array
132145
nuxt.hook('prepare:types', options => {
133146
const tsConfig = options.tsConfig as { include?: string[] };
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { debug, getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
2+
import { defineNuxtPlugin } from 'nuxt/app';
3+
import type { NuxtPageSubset } from '../utils/route-extraction';
4+
import { extractParametrizedRouteFromContext } from '../utils/route-extraction';
5+
6+
export default defineNuxtPlugin(nuxtApp => {
7+
nuxtApp.hooks.hook('app:rendered', async renderContext => {
8+
let buildTimePagesData: NuxtPageSubset[];
9+
try {
10+
// 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
11+
// @ts-expect-error This import is dynamically resolved at build time (`addTemplate` in module.ts)
12+
const { default: importedPagesData } = await import('#build/sentry--nuxt-pages-data.mjs');
13+
buildTimePagesData = importedPagesData || [];
14+
debug.log('Imported build-time pages data:', buildTimePagesData);
15+
} catch (error) {
16+
buildTimePagesData = [];
17+
debug.warn('Failed to import build-time pages data:', error);
18+
}
19+
20+
const ssrContext = renderContext.ssrContext;
21+
22+
const routeInfo = extractParametrizedRouteFromContext(
23+
ssrContext?.modules,
24+
ssrContext?.url || ssrContext?.event._path,
25+
buildTimePagesData,
26+
);
27+
28+
if (routeInfo === null) {
29+
return;
30+
}
31+
32+
const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined
33+
34+
if (activeSpan && routeInfo.parametrizedRoute) {
35+
const rootSpan = getRootSpan(activeSpan);
36+
37+
if (!rootSpan) {
38+
return;
39+
}
40+
41+
debug.log('Matched parametrized server route:', routeInfo.parametrizedRoute);
42+
43+
rootSpan.setAttributes({
44+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
45+
'http.route': routeInfo.parametrizedRoute,
46+
});
47+
}
48+
});
49+
});

packages/nuxt/src/runtime/plugins/route-detector.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ export default defineNuxtPlugin(nuxtApp => {
77
nuxtApp.hooks.hook('app:rendered', async renderContext => {
88
let buildTimePagesData: NuxtPageSubset[];
99
try {
10-
// 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
11-
// @ts-expect-error This import is dynamically resolved at build time (`addTemplate` in module.ts)
12-
const { default: importedPagesData } = await import('#build/sentry--nuxt-pages-data.mjs');
10+
// Virtual module registered via addServerTemplate in module.ts (Nuxt v4+)
11+
// @ts-expect-error - This is a virtual module
12+
const { default: importedPagesData } = await import('#sentry/nuxt-pages-data.mjs');
1313
buildTimePagesData = importedPagesData || [];
1414
debug.log('Imported build-time pages data:', buildTimePagesData);
1515
} catch (error) {

0 commit comments

Comments
 (0)