Skip to content

Commit b746c23

Browse files
authored
fix(nuxt): Parametrize routes on the server-side (#16785)
Adds route parametrization for server API routes for production builds. part of #16684
1 parent 64904f5 commit b746c23

File tree

8 files changed

+79
-16
lines changed

8 files changed

+79
-16
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/tracing.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test.describe('distributed tracing', () => {
1010
});
1111

1212
const serverTxnEventPromise = waitForTransaction('nuxt-3-dynamic-import', txnEvent => {
13-
return txnEvent.transaction.includes('GET /test-param/');
13+
return txnEvent.transaction?.includes('GET /test-param/') || false;
1414
});
1515

1616
const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
50+
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
5151
transaction_info: { source: 'url' },
5252
type: 'transaction',
5353
contexts: {
@@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
135135
expect(serverReqTxnEvent).toEqual(
136136
expect.objectContaining({
137137
type: 'transaction',
138-
transaction: `GET /api/user/${PARAM}`,
139-
transaction_info: { source: 'url' },
138+
transaction: `GET /api/user/:userId`, // parametrized route
139+
transaction_info: { source: 'route' },
140140
contexts: expect.objectContaining({
141141
trace: expect.objectContaining({
142142
op: 'http.server',

dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
50+
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
5151
transaction_info: { source: 'url' },
5252
type: 'transaction',
5353
contexts: {
@@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
135135
expect(serverReqTxnEvent).toEqual(
136136
expect.objectContaining({
137137
type: 'transaction',
138-
transaction: `GET /api/user/${PARAM}`,
139-
transaction_info: { source: 'url' },
138+
transaction: `GET /api/user/:userId`, // parametrized route
139+
transaction_info: { source: 'route' },
140140
contexts: expect.objectContaining({
141141
trace: expect.objectContaining({
142142
op: 'http.server',

dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/tracing.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
50+
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
5151
transaction_info: { source: 'url' },
5252
type: 'transaction',
5353
contexts: {
@@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
135135
expect(serverReqTxnEvent).toEqual(
136136
expect.objectContaining({
137137
type: 'transaction',
138-
transaction: `GET /api/user/${PARAM}`,
139-
transaction_info: { source: 'url' },
138+
transaction: `GET /api/user/:userId`, // parametrized route
139+
transaction_info: { source: 'route' },
140140
contexts: expect.objectContaining({
141141
trace: expect.objectContaining({
142142
op: 'http.server',

dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
50+
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
5151
transaction_info: { source: 'url' },
5252
type: 'transaction',
5353
contexts: {
@@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
135135
expect(serverReqTxnEvent).toEqual(
136136
expect.objectContaining({
137137
type: 'transaction',
138-
transaction: `GET /api/user/${PARAM}`,
139-
transaction_info: { source: 'url' },
138+
transaction: `GET /api/user/:userId`, // parametrized route
139+
transaction_info: { source: 'route' },
140140
contexts: expect.objectContaining({
141141
trace: expect.objectContaining({
142142
op: 'http.server',

dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
50+
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
5151
transaction_info: { source: 'url' },
5252
type: 'transaction',
5353
contexts: {
@@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
135135
expect(serverReqTxnEvent).toEqual(
136136
expect.objectContaining({
137137
type: 'transaction',
138-
transaction: `GET /api/user/${PARAM}`,
139-
transaction_info: { source: 'url' },
138+
transaction: `GET /api/user/:userId`, // parametrized route
139+
transaction_info: { source: 'route' },
140140
contexts: expect.objectContaining({
141141
trace: expect.objectContaining({
142142
op: 'http.server',
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { getActiveSpan, getCurrentScope, getRootSpan, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
2+
import type { H3Event } from 'h3';
3+
4+
/**
5+
* Update the root span (transaction) name for routes with parameters based on the matched route.
6+
*/
7+
export function updateRouteBeforeResponse(event: H3Event): void {
8+
if (!event.context.matchedRoute) {
9+
return;
10+
}
11+
12+
const matchedRoutePath = event.context.matchedRoute.path;
13+
14+
// If the matched route path is defined and differs from the event's path, it indicates a parametrized route
15+
// Example: Matched route is "/users/:id" and the event's path is "/users/123",
16+
if (matchedRoutePath && matchedRoutePath !== event._path) {
17+
if (matchedRoutePath === '/**') {
18+
// todo: support parametrized SSR pageload spans
19+
// If page is server-side rendered, the whole path gets transformed to `/**` (Example : `/users/123` becomes `/**` instead of `/users/:id`).
20+
return; // Skip if the matched route is a catch-all route.
21+
}
22+
23+
const method = event._method || 'GET';
24+
25+
const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`;
26+
getCurrentScope().setTransactionName(parametrizedTransactionName);
27+
28+
const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined
29+
if (!activeSpan) {
30+
return;
31+
}
32+
33+
const rootSpan = getRootSpan(activeSpan);
34+
if (!rootSpan) {
35+
return;
36+
}
37+
38+
rootSpan.setAttributes({
39+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
40+
'http.route': matchedRoutePath,
41+
});
42+
43+
const params = event.context?.params;
44+
45+
if (params && typeof params === 'object') {
46+
Object.entries(params).forEach(([key, value]) => {
47+
// Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey
48+
rootSpan.setAttributes({
49+
[`url.path.parameter.${key}`]: String(value),
50+
[`params.${key}`]: String(value),
51+
});
52+
});
53+
}
54+
55+
logger.log(`Updated transaction name for parametrized route: ${parametrizedTransactionName}`);
56+
}
57+
}

packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { H3Event } from 'h3';
66
import type { NitroApp, NitroAppPlugin } from 'nitropack';
77
import type { NuxtRenderHTMLContext } from 'nuxt/app';
88
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
9+
import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
910
import { addSentryTracingMetaTags } from '../utils';
1011

1112
interface CfEventType {
@@ -139,6 +140,8 @@ export const sentryCloudflareNitroPlugin =
139140
},
140141
});
141142

143+
nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);
144+
142145
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
143146
nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => {
144147
const storedTraceData = event?.context?.cf ? traceDataMap.get(event.context.cf) : undefined;

packages/nuxt/src/runtime/plugins/sentry.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import { type EventHandler } from 'h3';
55
import { defineNitroPlugin } from 'nitropack/runtime';
66
import type { NuxtRenderHTMLContext } from 'nuxt/app';
77
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
8+
import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
89
import { addSentryTracingMetaTags, flushIfServerless } from '../utils';
910

1011
export default defineNitroPlugin(nitroApp => {
1112
nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler);
1213

14+
nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);
15+
1316
nitroApp.hooks.hook('error', sentryCaptureErrorHook);
1417

1518
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context

0 commit comments

Comments
 (0)