Skip to content

Commit 3537d54

Browse files
committed
fix(nuxt): Parametrize routes on the server-side
1 parent 32d72fb commit 3537d54

File tree

7 files changed

+56
-11
lines changed

7 files changed

+56
-11
lines changed

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

Lines changed: 3 additions & 3 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,8 +47,8 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
51-
transaction_info: { source: 'url' },
50+
transaction: `GET /test-param/:param`,
51+
transaction_info: { source: 'route' },
5252
type: 'transaction',
5353
contexts: {
5454
trace: {

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

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

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
51-
transaction_info: { source: 'url' },
50+
transaction: `GET /test-param/:param`,
51+
transaction_info: { source: 'route' },
5252
type: 'transaction',
5353
contexts: {
5454
trace: {

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
@@ -23,7 +23,7 @@ test.describe('distributed tracing', () => {
2323
const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content');
2424

2525
expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`);
26-
expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param'
26+
expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); // URL-encoded for 'GET /test-param/:param'
2727
expect(baggageMetaTagContent).toContain('sentry-sampled=true');
2828
expect(baggageMetaTagContent).toContain('sentry-sample_rate=1');
2929

@@ -47,8 +47,8 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
51-
transaction_info: { source: 'url' },
50+
transaction: `GET /test-param/:param`,
51+
transaction_info: { source: 'route' },
5252
type: 'transaction',
5353
contexts: {
5454
trace: {

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
@@ -23,7 +23,7 @@ test.describe('distributed tracing', () => {
2323
const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content');
2424

2525
expect(baggageMetaTagContent).toContain(`sentry-trace_id=${serverTxnEvent.contexts?.trace?.trace_id}`);
26-
expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F${PARAM}`); // URL-encoded for 'GET /test-param/s0me-param'
26+
expect(baggageMetaTagContent).toContain(`sentry-transaction=GET%20%2Ftest-param%2F%3Aparam`); // URL-encoded for 'GET /test-param/:param'
2727
expect(baggageMetaTagContent).toContain('sentry-sampled=true');
2828
expect(baggageMetaTagContent).toContain('sentry-sample_rate=1');
2929

@@ -47,8 +47,8 @@ test.describe('distributed tracing', () => {
4747
});
4848

4949
expect(serverTxnEvent).toMatchObject({
50-
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
51-
transaction_info: { source: 'url' },
50+
transaction: `GET /test-param/:param`,
51+
transaction_info: { source: 'route' },
5252
type: 'transaction',
5353
contexts: {
5454
trace: {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
const matchedRoute = event.context.matchedRoute;
10+
const matchedRoutePath = matchedRoute.path;
11+
const params = event.context?.params || null;
12+
const method = event._method || 'GET';
13+
14+
// If the matched route path is defined and differs from the event's path, it indicates a parametrized route
15+
// Example: If the matched route is "/users/:id" and the event's path is "/users/123",
16+
if (matchedRoutePath && matchedRoutePath !== event._path) {
17+
const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`;
18+
getCurrentScope().setTransactionName(parametrizedTransactionName);
19+
20+
const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined
21+
if (activeSpan) {
22+
const rootSpan = getRootSpan(activeSpan);
23+
if (rootSpan) {
24+
rootSpan.updateName(parametrizedTransactionName);
25+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
26+
rootSpan.setAttribute('http.route', matchedRoutePath);
27+
28+
if (params && typeof params === 'object') {
29+
Object.entries(params).forEach(([key, value]) => {
30+
rootSpan.setAttribute(`params.${key}`, String(value));
31+
});
32+
}
33+
34+
logger.log(`Updated transaction name for parametrized route: ${parametrizedTransactionName}`);
35+
}
36+
}
37+
}
38+
}
39+
}

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)