@@ -3,15 +3,18 @@ import {
33 getActiveSpan ,
44 getClient ,
55 getHttpSpanDetailsFromUrlObject ,
6+ getRootSpan ,
67 GLOBAL_OBJ ,
78 httpHeadersToSpanAttributes ,
89 parseStringToURLObject ,
910 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
1011 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
12+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ,
1113 setHttpStatus ,
1214 type Span ,
1315 SPAN_STATUS_ERROR ,
1416 startSpanManual ,
17+ updateSpanName ,
1518} from '@sentry/core' ;
1619import type { TracingRequestEvent as H3TracingRequestEvent } from 'h3/tracing' ;
1720import { tracingChannel } from 'otel-tracing-channel' ;
@@ -69,12 +72,37 @@ function onTraceError(data: { span?: Span; error: unknown }): void {
6972 data . span ?. end ( ) ;
7073}
7174
75+ /**
76+ * Extracts the parameterized route pattern from the h3 event context.
77+ */
78+ function getParameterizedRoute ( event : H3TracingRequestEvent [ 'event' ] ) : string | undefined {
79+ const matchedRoute = event . context ?. matchedRoute ;
80+ if ( ! matchedRoute ) {
81+ return undefined ;
82+ }
83+
84+ const routePath = matchedRoute . route ;
85+
86+ // Skip catch-all routes as they're not useful for transaction grouping
87+ if ( ! routePath || routePath === '/**' ) {
88+ return undefined ;
89+ }
90+
91+ return routePath ;
92+ }
93+
7294function setupH3TracingChannels ( ) : void {
7395 const h3Channel = tracingChannel < H3TracingRequestEvent > ( 'h3.request' , data => {
7496 const parsedUrl = parseStringToURLObject ( data . event . url . href ) ;
75- const [ spanName , urlAttributes ] = getHttpSpanDetailsFromUrlObject ( parsedUrl , 'server' , 'auto.http.nitro.h3' , {
76- method : data . event . req . method ,
77- } ) ;
97+ const routePattern = getParameterizedRoute ( data . event ) ;
98+
99+ const [ spanName , urlAttributes ] = getHttpSpanDetailsFromUrlObject (
100+ parsedUrl ,
101+ 'server' ,
102+ 'auto.http.nitro.h3' ,
103+ { method : data . event . req . method } ,
104+ routePattern ,
105+ ) ;
78106
79107 return startSpanManual (
80108 {
@@ -93,7 +121,30 @@ function setupH3TracingChannels(): void {
93121 start : NOOP ,
94122 asyncStart : NOOP ,
95123 end : NOOP ,
96- asyncEnd : onTraceEnd ,
124+ asyncEnd : ( data : H3TracingRequestEvent & { span ?: Span ; result ?: unknown } ) => {
125+ onTraceEnd ( data ) ;
126+
127+ if ( ! data . span ) {
128+ return ;
129+ }
130+
131+ // Update the root span (srvx transaction) with the parameterized route name.
132+ // The srvx span is created before h3 resolves the route, so it initially has the raw URL.
133+ // Note: data.type is always 'middleware' in asyncEnd regardless of handler type,
134+ // so we rely on getParameterizedRoute() to filter out catch-all routes instead.
135+ const rootSpan = getRootSpan ( data . span ) ;
136+ if ( rootSpan && rootSpan !== data . span ) {
137+ const routePattern = getParameterizedRoute ( data . event ) ;
138+ if ( routePattern ) {
139+ const method = data . event . req . method || 'GET' ;
140+ updateSpanName ( rootSpan , `${ method } ${ routePattern } ` ) ;
141+ rootSpan . setAttributes ( {
142+ [ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE ] : 'route' ,
143+ 'http.route' : routePattern ,
144+ } ) ;
145+ }
146+ }
147+ } ,
97148 error : onTraceError ,
98149 } ) ;
99150}
0 commit comments