@@ -12,11 +12,11 @@ vi.mock('@sentry/core', async importOriginal => {
1212 } ;
1313} ) ;
1414
15- const { createSentryTunnelRoute } = await import ( '../../src/server/tunnelRoute' ) ;
15+ const { createSentryTunnelRoute, registerSentryServerTunnelRoute } = await import ( '../../src/server/tunnelRoute' ) ;
1616
1717describe ( 'createSentryTunnelRoute' , ( ) => {
1818 afterEach ( ( ) => {
19- vi . clearAllMocks ( ) ;
19+ vi . resetAllMocks ( ) ;
2020 } ) ;
2121
2222 it ( 'returns a server route config with only a POST handler' , ( ) => {
@@ -49,10 +49,12 @@ describe('createSentryTunnelRoute', () => {
4949 } ) ;
5050
5151 it ( 'derives the allowed DSN from the active server Sentry client when allowedDsns is omitted' , async ( ) => {
52- const request = new Request ( 'http://localhost:3000/monitoring ' , { method : 'POST' , body : 'envelope' } ) ;
52+ const request = new Request ( 'http://localhost:3000/derive-dsn ' , { method : 'POST' , body : 'envelope' } ) ;
5353 const response = new Response ( 'ok' , { status : 200 } ) ;
5454
55- getClientSpy . mockReturnValueOnce ( {
55+ // `getClient` is called both by the path self-registration and the DSN derivation.
56+ getClientSpy . mockReturnValue ( {
57+ getOptions : ( ) => ( { } ) ,
5658 getDsn : ( ) => ( {
5759 protocol : 'http' ,
5860 publicKey : 'public' ,
@@ -77,6 +79,21 @@ describe('createSentryTunnelRoute', () => {
7779 expect ( result ) . toBe ( response ) ;
7880 } ) ;
7981
82+ it ( 'self-registers the request path so the streamed-span sampler can drop it' , async ( ) => {
83+ const request = new Request ( 'http://localhost:3000/handler-selfreg' , { method : 'POST' , body : 'envelope' } ) ;
84+ const options : { ignoreSpans ?: unknown [ ] } = { } ;
85+ getClientSpy . mockReturnValue ( { getOptions : ( ) => options , getDsn : ( ) => undefined } ) ;
86+ handleTunnelRequestSpy . mockResolvedValueOnce ( new Response ( 'ok' , { status : 200 } ) ) ;
87+
88+ await createSentryTunnelRoute ( { allowedDsns : [ 'https://public@o0.ingest.sentry.io/0' ] } ) . handlers . POST ( { request } ) ;
89+
90+ const matcher = options . ignoreSpans ?. find (
91+ ( entry ) : entry is { attributes : { 'http.target' : RegExp } } =>
92+ ! ! ( entry as { attributes ?: { 'http.target' ?: unknown } } ) ?. attributes ?. [ 'http.target' ] ,
93+ ) ;
94+ expect ( matcher ?. attributes [ 'http.target' ] . test ( '/handler-selfreg' ) ) . toBe ( true ) ;
95+ } ) ;
96+
8097 it ( 'returns 500 when allowedDsns is omitted and no active server Sentry client DSN exists' , async ( ) => {
8198 const request = new Request ( 'http://localhost:3000/monitoring' , { method : 'POST' , body : 'envelope' } ) ;
8299
@@ -90,3 +107,43 @@ describe('createSentryTunnelRoute', () => {
90107 await expect ( result . text ( ) ) . resolves . toContain ( 'Tunnel route requires Sentry server SDK initialized with a DSN' ) ;
91108 } ) ;
92109} ) ;
110+
111+ describe ( 'registerSentryServerTunnelRoute' , ( ) => {
112+ afterEach ( ( ) => {
113+ vi . resetAllMocks ( ) ;
114+ } ) ;
115+
116+ it ( 'adds an http.target ignoreSpans matcher for the tunnel route path so the transaction is dropped at span start' , ( ) => {
117+ const options : { ignoreSpans ?: unknown [ ] } = { ignoreSpans : [ / e x i s t i n g / ] } ;
118+ getClientSpy . mockReturnValue ( { getOptions : ( ) => options } ) ;
119+
120+ // Unique path per test to avoid the module-level dedupe Set across tests.
121+ registerSentryServerTunnelRoute ( '/abcd1234' ) ;
122+
123+ expect ( options . ignoreSpans ) . toHaveLength ( 2 ) ;
124+ const matcher = options . ignoreSpans ?. [ 1 ] as { attributes : { 'http.target' : RegExp } } ;
125+ const pattern = matcher . attributes [ 'http.target' ] ;
126+
127+ expect ( pattern . test ( '/abcd1234' ) ) . toBe ( true ) ;
128+ expect ( pattern . test ( '/abcd1234?o=1&p=2' ) ) . toBe ( true ) ;
129+ expect ( pattern . test ( '/abcd1234/' ) ) . toBe ( true ) ;
130+ // Must not match an unrelated route that merely shares the prefix.
131+ expect ( pattern . test ( '/abcd1234extra' ) ) . toBe ( false ) ;
132+ } ) ;
133+
134+ it ( 'does not register the same tunnel route path twice' , ( ) => {
135+ const options : { ignoreSpans ?: unknown [ ] } = { } ;
136+ getClientSpy . mockReturnValue ( { getOptions : ( ) => options } ) ;
137+
138+ registerSentryServerTunnelRoute ( '/dedupe-me' ) ;
139+ registerSentryServerTunnelRoute ( '/dedupe-me' ) ;
140+
141+ expect ( options . ignoreSpans ) . toHaveLength ( 1 ) ;
142+ } ) ;
143+
144+ it ( 'is a no-op when there is no active client' , ( ) => {
145+ getClientSpy . mockReturnValue ( undefined ) ;
146+
147+ expect ( ( ) => registerSentryServerTunnelRoute ( '/no-client' ) ) . not . toThrow ( ) ;
148+ } ) ;
149+ } ) ;
0 commit comments