@@ -8,13 +8,14 @@ import {
88 parseStringToURLObject ,
99 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
1010 setHttpStatus ,
11- startSpan ,
11+ startSpanManual ,
1212 winterCGHeadersToDict ,
1313 withIsolationScope ,
1414} from '@sentry/core' ;
1515import type { CloudflareOptions } from './client' ;
1616import { addCloudResourceContext , addCultureContext , addRequest } from './scope-utils' ;
1717import { init } from './sdk' ;
18+ import { classifyResponseStreaming } from './utils/streaming' ;
1819
1920interface RequestHandlerWrapperOptions {
2021 options : CloudflareOptions ;
@@ -98,26 +99,79 @@ export function wrapRequestHandler(
9899 // Note: This span will not have a duration unless I/O happens in the handler. This is
99100 // because of how the cloudflare workers runtime works.
100101 // See: https://developers.cloudflare.com/workers/runtime-apis/performance/
101- return startSpan (
102- {
103- name,
104- attributes,
105- } ,
106- async span => {
107- try {
108- const res = await handler ( ) ;
109- setHttpStatus ( span , res . status ) ;
110- return res ;
111- } catch ( e ) {
112- if ( captureErrors ) {
113- captureException ( e , { mechanism : { handled : false , type : 'auto.http.cloudflare' } } ) ;
114- }
115- throw e ;
116- } finally {
102+
103+ // Use startSpanManual to control when span ends (needed for streaming responses)
104+ return startSpanManual ( { name, attributes } , async span => {
105+ let res : Response ;
106+
107+ try {
108+ res = await handler ( ) ;
109+ setHttpStatus ( span , res . status ) ;
110+ } catch ( e ) {
111+ span . end ( ) ; // End span on error
112+ if ( captureErrors ) {
113+ captureException ( e , { mechanism : { handled : false , type : 'auto.http.cloudflare' } } ) ;
114+ }
115+ waitUntil ?.( flush ( 2000 ) ) ;
116+ throw e ;
117+ }
118+
119+ // Classify response to detect actual streaming
120+ const classification = await classifyResponseStreaming ( res ) ;
121+
122+ if ( classification . isStreaming ) {
123+ // Streaming response detected - monitor consumption to keep span alive
124+ if ( ! classification . response . body ) {
125+ // Shouldn't happen since isStreaming requires body, but handle gracefully
126+ span . end ( ) ;
117127 waitUntil ?.( flush ( 2000 ) ) ;
128+ return classification . response ;
118129 }
119- } ,
120- ) ;
130+
131+ const [ clientStream , monitorStream ] = classification . response . body . tee ( ) ;
132+
133+ // Monitor stream consumption and end span when complete
134+ const streamMonitor = ( async ( ) => {
135+ const reader = monitorStream . getReader ( ) ;
136+
137+ // Safety timeout to prevent infinite loops if stream hangs
138+ const timeout = setTimeout ( ( ) => {
139+ span . end ( ) ;
140+ reader . cancel ( ) . catch ( ( ) => { } ) ;
141+ } , 30000 ) ; // 30 second max
142+
143+ try {
144+ let done = false ;
145+ while ( ! done ) {
146+ const result = await reader . read ( ) ;
147+ done = result . done ;
148+ }
149+ clearTimeout ( timeout ) ;
150+ span . end ( ) ;
151+ } catch ( err ) {
152+ clearTimeout ( timeout ) ;
153+ span . end ( ) ;
154+ } finally {
155+ reader . releaseLock ( ) ;
156+ }
157+ } ) ( ) ;
158+
159+ // Use waitUntil to keep context alive and flush after span ends
160+ waitUntil ?.( streamMonitor . then ( ( ) => flush ( 2000 ) ) ) ;
161+
162+ // Return response with client stream
163+ return new Response ( clientStream , {
164+ status : classification . response . status ,
165+ statusText : classification . response . statusText ,
166+ headers : classification . response . headers ,
167+ } ) ;
168+ }
169+
170+ // Non-streaming response - end span immediately and return original
171+ span . end ( ) ;
172+ waitUntil ?.( flush ( 2000 ) ) ;
173+ return classification . response ;
174+ } ) ;
121175 } ,
122176 ) ;
123177 } ) ;
0 commit comments