Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test.describe('distributed tracing', () => {
});

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

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

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
Expand Down Expand Up @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
expect(serverReqTxnEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: `GET /api/user/${PARAM}`,
transaction_info: { source: 'url' },
transaction: `GET /api/user/:userId`, // parametrized route
transaction_info: { source: 'route' },
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
Expand Down Expand Up @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
expect(serverReqTxnEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: `GET /api/user/${PARAM}`,
transaction_info: { source: 'url' },
transaction: `GET /api/user/:userId`, // parametrized route
transaction_info: { source: 'route' },
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
Expand Down Expand Up @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
expect(serverReqTxnEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: `GET /api/user/${PARAM}`,
transaction_info: { source: 'url' },
transaction: `GET /api/user/:userId`, // parametrized route
transaction_info: { source: 'route' },
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
Expand Down Expand Up @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
expect(serverReqTxnEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: `GET /api/user/${PARAM}`,
transaction_info: { source: 'url' },
transaction: `GET /api/user/:userId`, // parametrized route
transaction_info: { source: 'route' },
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction: `GET /test-param/${PARAM}`, // todo: parametrize
transaction_info: { source: 'url' },
type: 'transaction',
contexts: {
Expand Down Expand Up @@ -135,8 +135,8 @@ test.describe('distributed tracing', () => {
expect(serverReqTxnEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: `GET /api/user/${PARAM}`,
transaction_info: { source: 'url' },
transaction: `GET /api/user/:userId`, // parametrized route
transaction_info: { source: 'route' },
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
Expand Down
57 changes: 57 additions & 0 deletions packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getActiveSpan, getCurrentScope, getRootSpan, logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import type { H3Event } from 'h3';

/**
* Update the root span (transaction) name for routes with parameters based on the matched route.
*/
export function updateRouteBeforeResponse(event: H3Event): void {
if (!event.context.matchedRoute) {
return;
}

const matchedRoutePath = event.context.matchedRoute.path;

// If the matched route path is defined and differs from the event's path, it indicates a parametrized route
// Example: Matched route is "/users/:id" and the event's path is "/users/123",
if (matchedRoutePath && matchedRoutePath !== event._path) {
if (matchedRoutePath === '/**') {
// todo: support parametrized SSR pageload spans
// If page is server-side rendered, the whole path gets transformed to `/**` (Example : `/users/123` becomes `/**` instead of `/users/:id`).
return; // Skip if the matched route is a catch-all route.
}

const method = event._method || 'GET';

const parametrizedTransactionName = `${method.toUpperCase()} ${matchedRoutePath}`;
getCurrentScope().setTransactionName(parametrizedTransactionName);

const activeSpan = getActiveSpan(); // In development mode, getActiveSpan() is always undefined
if (!activeSpan) {
return;
}

const rootSpan = getRootSpan(activeSpan);
if (!rootSpan) {
return;
}

rootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'http.route': matchedRoutePath,
});

const params = event.context?.params;

if (params && typeof params === 'object') {
Object.entries(params).forEach(([key, value]) => {
// Based on this convention: https://getsentry.github.io/sentry-conventions/generated/attributes/url.html#urlpathparameterkey
rootSpan.setAttributes({
[`url.path.parameter.${key}`]: String(value),
[`params.${key}`]: String(value),
});
});
}

logger.log(`Updated transaction name for parametrized route: ${parametrizedTransactionName}`);
}
}
3 changes: 3 additions & 0 deletions packages/nuxt/src/runtime/plugins/sentry-cloudflare.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { H3Event } from 'h3';
import type { NitroApp, NitroAppPlugin } from 'nitropack';
import type { NuxtRenderHTMLContext } from 'nuxt/app';
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
import { addSentryTracingMetaTags } from '../utils';

interface CfEventType {
Expand Down Expand Up @@ -139,6 +140,8 @@ export const sentryCloudflareNitroPlugin =
},
});

nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);

// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => {
const storedTraceData = event?.context?.cf ? traceDataMap.get(event.context.cf) : undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/nuxt/src/runtime/plugins/sentry.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { type EventHandler } from 'h3';
import { defineNitroPlugin } from 'nitropack/runtime';
import type { NuxtRenderHTMLContext } from 'nuxt/app';
import { sentryCaptureErrorHook } from '../hooks/captureErrorHook';
import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse';
import { addSentryTracingMetaTags, flushIfServerless } from '../utils';

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

nitroApp.hooks.hook('beforeResponse', updateRouteBeforeResponse);

nitroApp.hooks.hook('error', sentryCaptureErrorHook);

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