-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(nuxt): Parametrize SSR routes #16843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
e740361
3b4a57c
8492c82
a215c8c
6999a67
6435448
b13dfdb
ef90f2a
b3bbf0c
5614e30
02d60e1
d392ec5
8bf9820
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { getActiveSpan, getRootSpan, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; | ||
import { defineNuxtPlugin } from 'nuxt/app'; | ||
import type { NuxtPage } from 'nuxt/schema'; | ||
import { extractParametrizedRouteFromContext } from '../utils/route-extraction'; | ||
|
||
export default defineNuxtPlugin(nuxtApp => { | ||
nuxtApp.hooks.hook('app:rendered', async renderContext => { | ||
let buildTimePagesData: NuxtPage[] = []; | ||
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'); | ||
buildTimePagesData = importedPagesData || []; | ||
} catch (error) { | ||
buildTimePagesData = []; | ||
} | ||
s1gr1d marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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; | ||
} | ||
|
||
const method = ssrContext?.event?._method || 'GET'; | ||
const parametrizedTransactionName = `${method.toUpperCase()} ${routeInfo.parametrizedRoute}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m: We create the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh that's left from before the last commit 😅 |
||
|
||
logger.log('Matched parametrized server route:', parametrizedTransactionName); | ||
|
||
rootSpan.setAttributes({ | ||
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', | ||
'http.route': routeInfo.parametrizedRoute, | ||
}); | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { logger } from '@sentry/core'; | ||
import type { NuxtSSRContext } from 'nuxt/app'; | ||
import type { NuxtPage } from 'nuxt/schema'; | ||
|
||
/** | ||
* Extracts route information from the SSR context modules and URL. | ||
* | ||
* The function matches the requested URL against the build-time pages data. The build-time pages data | ||
* contains the routes that were generated during the build process, which allows us to set the parametrized route. | ||
* | ||
* @param ssrContextModules - The modules from the SSR context. | ||
* This is a Set of module paths that were used when loading one specific page. | ||
* Example: `Set(['app.vue', 'components/Button.vue', 'pages/user/[userId].vue'])` | ||
* | ||
* @param currentUrl - The requested URL string | ||
* Example: `/user/123` | ||
* | ||
* @param buildTimePagesData | ||
* An array of NuxtPage objects representing the build-time pages data. | ||
* Example: [{ name: 'some-path', path: '/some/path' }, { name: 'user-userId', path: '/user/:userId()' }] | ||
*/ | ||
export function extractParametrizedRouteFromContext( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: should we memoize this function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah the function is quite expensive as the lookup is hard :/ I added some general, small optimizations for the function. Using the However, as I think the key generation regardless its overhead still makes sense, I added it now. |
||
ssrContextModules?: NuxtSSRContext['modules'], | ||
currentUrl?: NuxtSSRContext['url'], | ||
buildTimePagesData: NuxtPage[] = [], | ||
): null | { parametrizedRoute: string } { | ||
if (!ssrContextModules || !currentUrl) { | ||
logger.warn('SSR context modules or URL is not available.'); | ||
return null; | ||
} | ||
|
||
if (buildTimePagesData.length === 0) { | ||
return null; | ||
} | ||
|
||
const modulesArray = Array.from(ssrContextModules); | ||
|
||
// Find the route data that corresponds to a module in ssrContext.modules | ||
const foundRouteData = buildTimePagesData.find(routeData => { | ||
if (!routeData.file) return false; | ||
|
||
return modulesArray.some(module => { | ||
// Extract the folder name and relative path from the page file | ||
// e.g., 'pages/test-param/[param].vue' -> folder: 'pages', path: 'test-param/[param].vue' | ||
const filePathParts = module.split('/'); | ||
|
||
// Exclude root-level files (e.g., 'app.vue') | ||
if (filePathParts.length < 2) return false; | ||
|
||
// Normalize path separators to handle both Unix and Windows paths | ||
const normalizedRouteFile = routeData.file?.replace(/\\/g, '/'); | ||
|
||
const pagesFolder = filePathParts[0]; | ||
const pageRelativePath = filePathParts.slice(1).join('/'); | ||
|
||
// Check if any module in ssrContext.modules ends with the same folder/relative path structure | ||
return normalizedRouteFile?.endsWith(`/${pagesFolder}/${pageRelativePath}`); | ||
}); | ||
}); | ||
|
||
const parametrizedRoute = foundRouteData?.path ?? null; | ||
|
||
return parametrizedRoute === null ? null : { parametrizedRoute }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m: So stringifying all of the pages can get pretty big from what I can see in the
NuxtPage
type.Perhaps we should just save a subset? We really only need the
file
and thepath
from looking atextractParametrizedRouteFromContext
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I also filtered for only dynamic pages.