diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 077bda66745b..ffe35a311e99 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -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({ meta: { @@ -79,6 +80,8 @@ export default defineNuxtModule({ 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) { @@ -91,10 +94,11 @@ export default defineNuxtModule({ 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(); @@ -108,26 +112,35 @@ export default defineNuxtModule({ 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)};`, + }); + } + // Add the sentry config file to the include array nuxt.hook('prepare:types', options => { const tsConfig = options.tsConfig as { include?: string[] }; diff --git a/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts b/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts new file mode 100644 index 000000000000..d67102576158 --- /dev/null +++ b/packages/nuxt/src/runtime/plugins/route-detector-legacy.server.ts @@ -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, + }); + } + }); +}); diff --git a/packages/nuxt/src/runtime/plugins/route-detector.server.ts b/packages/nuxt/src/runtime/plugins/route-detector.server.ts index 37c6bc17a4b5..1ed3a2f1d36b 100644 --- a/packages/nuxt/src/runtime/plugins/route-detector.server.ts +++ b/packages/nuxt/src/runtime/plugins/route-detector.server.ts @@ -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) {