11import { afterAll , describe , expect } from 'vitest' ;
22import { cleanupChildProcesses , createEsmAndCjsTests } from '../../utils/runner' ;
33
4+ // Verifies that Hono is auto-instrumented out of the box by `@sentry/node` (the `honoIntegration`
5+ // default), without importing `@sentry/hono` or registering the `sentry()` middleware manually.
6+ //
7+ // The SDK runs with the default `traceLifecycle` (span streaming), so the transaction is asserted via
8+ // the streamed span container (`container.items`, root = `is_segment`) rather than a `transaction`
9+ // envelope, and `.unordered()` lets the segment/child spans and error events arrive in any order
10+ // while ignoring unrelated envelopes (client reports, etc.).
11+
12+ // oxlint-disable-next-line typescript/no-explicit-any
13+ type StreamedSpan = { name ?: string ; status ?: string ; is_segment ?: boolean ; attributes ?: Record < string , any > } ;
14+
15+ const attr = ( span : StreamedSpan , key : string ) : unknown => span . attributes ?. [ key ] ?. value ;
16+ const op = ( span : StreamedSpan ) : unknown => attr ( span , 'sentry.op' ) ;
17+
418describe ( 'hono auto-instrumentation' , ( ) => {
519 afterAll ( ( ) => {
620 cleanupChildProcesses ( ) ;
@@ -9,15 +23,15 @@ describe('hono auto-instrumentation', () => {
923 createEsmAndCjsTests ( __dirname , 'scenario.mjs' , 'instrument.mjs' , ( createRunner , test ) => {
1024 test ( 'creates a transaction for a basic GET request' , async ( ) => {
1125 const runner = createRunner ( )
26+ . unordered ( )
1227 . expect ( {
13- transaction : {
14- transaction : 'GET /' ,
15- contexts : {
16- trace : {
17- op : 'http.server' ,
18- status : 'ok' ,
19- } ,
20- } ,
28+ span : container => {
29+ const segment = container . items . find ( item => item . is_segment && item . name === 'GET /' ) ;
30+ if ( ! segment ) {
31+ throw new Error ( 'segment for `GET /` not in this container' ) ;
32+ }
33+ expect ( op ( segment ) ) . toBe ( 'http.server' ) ;
34+ expect ( segment . status ) . toBe ( 'ok' ) ;
2135 } ,
2236 } )
2337 . start ( ) ;
@@ -27,18 +41,15 @@ describe('hono auto-instrumentation', () => {
2741
2842 test ( 'creates a transaction with a parametrized route name' , async ( ) => {
2943 const runner = createRunner ( )
44+ . unordered ( )
3045 . expect ( {
31- transaction : {
32- transaction : 'GET /hello/:name' ,
33- transaction_info : {
34- source : 'route' ,
35- } ,
36- contexts : {
37- trace : {
38- op : 'http.server' ,
39- status : 'ok' ,
40- } ,
41- } ,
46+ span : container => {
47+ const segment = container . items . find ( item => item . is_segment && item . name === 'GET /hello/:name' ) ;
48+ if ( ! segment ) {
49+ throw new Error ( 'segment for `GET /hello/:name` not in this container' ) ;
50+ }
51+ expect ( op ( segment ) ) . toBe ( 'http.server' ) ;
52+ expect ( attr ( segment , 'sentry.segment.name.source' ) ) . toBe ( 'route' ) ;
4253 } ,
4354 } )
4455 . start ( ) ;
@@ -48,7 +59,7 @@ describe('hono auto-instrumentation', () => {
4859
4960 test ( 'captures an error with the correct mechanism' , async ( ) => {
5061 const runner = createRunner ( )
51- . ignore ( 'transaction' )
62+ . unordered ( )
5263 . expect ( {
5364 event : {
5465 exception : {
@@ -73,19 +84,16 @@ describe('hono auto-instrumentation', () => {
7384
7485 test ( 'creates a transaction with internal_error status when an error occurs' , async ( ) => {
7586 const runner = createRunner ( )
76- . ignore ( 'event' )
87+ . unordered ( )
7788 . expect ( {
78- transaction : {
79- transaction : 'GET /error/:param' ,
80- contexts : {
81- trace : {
82- op : 'http.server' ,
83- status : 'internal_error' ,
84- data : expect . objectContaining ( {
85- 'http.response.status_code' : 500 ,
86- } ) ,
87- } ,
88- } ,
89+ span : container => {
90+ const segment = container . items . find ( item => item . is_segment && item . name === 'GET /error/:param' ) ;
91+ if ( ! segment ) {
92+ throw new Error ( 'segment for `GET /error/:param` not in this container' ) ;
93+ }
94+ expect ( op ( segment ) ) . toBe ( 'http.server' ) ;
95+ expect ( segment . status ) . toBe ( 'error' ) ;
96+ expect ( attr ( segment , 'http.response.status_code' ) ) . toBe ( 500 ) ;
8997 } ,
9098 } )
9199 . start ( ) ;
@@ -95,16 +103,19 @@ describe('hono auto-instrumentation', () => {
95103
96104 test ( 'does not create a middleware span for the Sentry middleware in a mounted sub-app' , async ( ) => {
97105 const runner = createRunner ( )
106+ . unordered ( )
98107 . expect ( {
99- transaction : transaction => {
100- expect ( transaction . transaction ) . toBe ( 'GET /sub/hello' ) ;
101- const middlewareSpans = ( transaction . spans || [ ] ) . filter ( span => span . op === 'middleware' ) ;
102- const names = middlewareSpans . map ( span => span . description ) ;
108+ span : container => {
109+ const segment = container . items . find ( item => item . is_segment && item . name === 'GET /sub/hello' ) ;
110+ if ( ! segment ) {
111+ throw new Error ( 'segment for `GET /sub/hello` not in this container' ) ;
112+ }
113+ const middlewareNames = container . items . filter ( item => op ( item ) === 'middleware' ) . map ( item => item . name ) ;
103114 // The sub-app's user middleware is traced …
104- expect ( names ) . toContain ( 'subMiddleware' ) ;
115+ expect ( middlewareNames ) . toContain ( 'subMiddleware' ) ;
105116 // … but the sub-app's own auto-registered Sentry middleware must not appear as a span
106117 // (it is copied into the parent at mount time and must stay unwrapped).
107- expect ( names ) . not . toContain ( '<anonymous>' ) ;
118+ expect ( middlewareNames ) . not . toContain ( '<anonymous>' ) ;
108119 } ,
109120 } )
110121 . start ( ) ;
@@ -114,21 +125,25 @@ describe('hono auto-instrumentation', () => {
114125
115126 test ( 'traces an internal .request() call without the inner app re-instrumenting the request' , async ( ) => {
116127 const runner = createRunner ( )
128+ . unordered ( )
117129 . expect ( {
118- transaction : transaction => {
119- // The transaction is named after the outer route, not the internal one.
120- expect ( transaction . transaction ) . toBe ( 'GET /outer/:itemId' ) ;
121-
122- const spans = transaction . spans || [ ] ;
130+ span : container => {
131+ // Transaction is named after the outer route, not the internal one.
132+ const segment = container . items . find ( item => item . is_segment && item . name === 'GET /outer/:itemId' ) ;
133+ if ( ! segment ) {
134+ throw new Error ( 'segment for `GET /outer/:itemId` not in this container' ) ;
135+ }
123136
124137 // The internal dispatch is traced with the raw inner path — the inner app's Sentry
125138 // middleware must not re-name it to the parametrized route.
126- const internalRequestSpans = spans . filter ( span => span . origin === 'auto.http.hono.internal_request' ) ;
139+ const internalRequestSpans = container . items . filter (
140+ item => attr ( item , 'sentry.origin' ) === 'auto.http.hono.internal_request' ,
141+ ) ;
127142 expect ( internalRequestSpans ) . toHaveLength ( 1 ) ;
128- expect ( internalRequestSpans [ 0 ] ?. description ) . toBe ( 'GET /item/self-watering-plant' ) ;
143+ expect ( internalRequestSpans [ 0 ] ?. name ) . toBe ( 'GET /item/self-watering-plant' ) ;
129144
130145 // The inner app's Sentry middleware must not add a middleware span.
131- const middlewareNames = spans . filter ( span => span . op === 'middleware' ) . map ( span => span . description ) ;
146+ const middlewareNames = container . items . filter ( item => op ( item ) === 'middleware' ) . map ( item => item . name ) ;
132147 expect ( middlewareNames ) . not . toContain ( '<anonymous>' ) ;
133148 } ,
134149 } )
@@ -139,7 +154,7 @@ describe('hono auto-instrumentation', () => {
139154
140155 test ( 'captures an error thrown after an internal .request() with the outer request data' , async ( ) => {
141156 const runner = createRunner ( )
142- . ignore ( 'transaction' )
157+ . unordered ( )
143158 . expect ( {
144159 event : event => {
145160 expect ( event . exception ?. values ?. [ 0 ] ?. value ) . toBe ( 'Test error from outer Hono app after internal request' ) ;
0 commit comments