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,8 +47,8 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction_info: { source: 'url' },
transaction: `GET /test-param/:param`,
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
trace: {
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,8 +47,8 @@ test.describe('distributed tracing', () => {
});

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction_info: { source: 'url' },
transaction: `GET /test-param/:param`,
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
trace: {
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 @@ -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 @@ -23,7 +23,7 @@ test.describe('distributed tracing', () => {
const baggageMetaTagContent = await page.locator('meta[name="baggage"]').getAttribute('content');

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

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

expect(serverTxnEvent).toMatchObject({
transaction: `GET /test-param/${PARAM}`, // todo: parametrize (nitro)
transaction_info: { source: 'url' },
transaction: `GET /test-param/:param`,
transaction_info: { source: 'route' },
type: 'transaction',
contexts: {
trace: {
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}`,
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
49 changes: 49 additions & 0 deletions packages/nuxt/src/runtime/hooks/updateRouteBeforeResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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) {
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) {
const rootSpan = getRootSpan(activeSpan);
if (rootSpan) {
rootSpan.updateName(parametrizedTransactionName);
rootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
'http.route': matchedRoutePath,
});

const params = event.context?.params || null;

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.setAttribute(`url.path.parameter.${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