1- import type { CfProperties , IncomingRequestCfProperties } from '@cloudflare/workers-types' ;
2- import {
3- captureException ,
4- continueTrace ,
5- getHttpSpanDetailsFromUrlObject ,
6- httpHeadersToSpanAttributes ,
7- parseStringToURLObject ,
8- SEMANTIC_ATTRIBUTE_SENTRY_OP ,
9- setHttpStatus ,
10- startSpanManual ,
11- winterCGHeadersToDict ,
12- } from '@sentry/core' ;
13- import { captureIncomingRequestBody } from './integrations/httpServer' ;
141import { initBaseSdk } from './baseSdk' ;
15- import type { CloudflareClient , CloudflareOptions } from './client' ;
16- import type { ExecutionContextCompat } from './executionContext' ;
17- import { flushAndDispose , getOriginalWaitUntil } from './flush' ;
18- import { addCloudResourceContext , addCultureContext , addRequest } from './scope-utils' ;
19- import { withInvocationIsolationScope } from './utils/invocationScope' ;
20- import { classifyResponseStreaming } from './utils/streaming' ;
21-
22- function getRequestErrorMechanismType ( context : ExecutionContextCompat | undefined ) : string {
23- // Durable Object fetch handlers use DO state as context (see instrumentDurableObjectWithSentry)
24- return context && 'storage' in context ? 'auto.faas.cloudflare.durable_object' : 'auto.http.cloudflare' ;
25- }
26-
27- interface RequestHandlerWrapperOptions {
28- options : CloudflareOptions ;
29- request : Request < unknown , IncomingRequestCfProperties < unknown > | CfProperties < unknown > > ;
30- context : ExecutionContextCompat | undefined ;
31- /**
32- * If true, errors will be captured, rethrown and sent to Sentry.
33- * Otherwise, errors are rethrown but not captured.
34- *
35- * You most likely don't want to set this to `false`, if you use `wrapRequestHandler` directly.
36- * This is primarily meant as an escape hatch for higher-level SDKs relying on additional error
37- * capturing mechanisms where this wrapper captures errors too early or too generally.
38- *
39- * @default true
40- */
41- captureErrors ?: boolean ;
42- }
43-
44- type InitSdk = ( options : CloudflareOptions ) => CloudflareClient | undefined ;
2+ import type { CloudflareOptions } from './client' ;
3+ import { type RequestHandlerWrapperOptions , wrapRequestHandlerWithInit } from './wrapRequestHandlerWithInit' ;
454
465/**
476 * Wraps a cloudflare request handler in Sentry instrumentation.
@@ -61,173 +20,3 @@ export function wrapRequestHandler(
6120) : Promise < Response > {
6221 return wrapRequestHandlerWithInit ( wrapperOptions , handler , initBaseSdk ) ;
6322}
64-
65- /**
66- * Same as {@link wrapRequestHandler}, but with the SDK initialization injected.
67- *
68- * Wrappers that are only reachable from the main entry point — where `nodejs_compat` is a
69- * requirement anyway — pass `init` from `sdk.ts` to get the full default integrations.
70- *
71- * @internal
72- */
73- export function wrapRequestHandlerWithInit (
74- wrapperOptions : RequestHandlerWrapperOptions ,
75- handler : ( ...args : unknown [ ] ) => Response | Promise < Response > ,
76- initSdk : InitSdk ,
77- ) : Promise < Response > {
78- return withInvocationIsolationScope ( async isolationScope => {
79- const { options, request, captureErrors = true } = wrapperOptions ;
80- const context = wrapperOptions . context ;
81-
82- // Use getOriginalWaitUntil to get the un-instrumented waitUntil function.
83- // This is crucial to avoid deadlock: the flush lock mechanism wraps waitUntil
84- // to track pending tasks. If we use the instrumented version for flushAndDispose,
85- // it acquires the lock, then flushAndDispose tries to wait for the same lock,
86- // creating a deadlock.
87- const waitUntil = context ? getOriginalWaitUntil ( context ) . bind ( context ) : undefined ;
88- const errorMechanismType = getRequestErrorMechanismType ( context ) ;
89-
90- const client = initSdk ( { ...options , ctx : context } ) ;
91- isolationScope . setClient ( client ) ;
92-
93- const urlObject = parseStringToURLObject ( request . url ) ;
94- const [ name , attributes ] = getHttpSpanDetailsFromUrlObject (
95- urlObject ,
96- 'server' ,
97- 'auto.http.cloudflare' ,
98- request ,
99- undefined ,
100- client ,
101- ) ;
102-
103- const contentLength = request . headers . get ( 'content-length' ) ;
104- if ( contentLength ) {
105- attributes [ 'http.request.body.size' ] = parseInt ( contentLength , 10 ) ;
106- }
107-
108- const userAgentHeader = request . headers . get ( 'user-agent' ) ;
109- if ( userAgentHeader ) {
110- attributes [ 'user_agent.original' ] = userAgentHeader ;
111- }
112-
113- if ( client ) {
114- Object . assign (
115- attributes ,
116- httpHeadersToSpanAttributes ( winterCGHeadersToDict ( request . headers ) , client . getDataCollectionOptions ( ) ) ,
117- ) ;
118- }
119-
120- attributes [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] = 'http.server' ;
121-
122- addCloudResourceContext ( isolationScope ) ;
123- addRequest ( isolationScope , request ) ;
124- if ( request . cf ) {
125- addCultureContext ( isolationScope , request . cf ) ;
126-
127- if ( typeof request . cf . httpProtocol === 'string' ) {
128- attributes [ 'network.protocol.name' ] = request . cf . httpProtocol ;
129- }
130- }
131-
132- // Do not capture spans for OPTIONS and HEAD requests
133- if ( request . method === 'OPTIONS' || request . method === 'HEAD' ) {
134- try {
135- return await handler ( ) ;
136- } catch ( e ) {
137- if ( captureErrors ) {
138- captureException ( e , { mechanism : { handled : false , type : errorMechanismType } } ) ;
139- }
140- throw e ;
141- } finally {
142- waitUntil ?.( flushAndDispose ( client ) ) ;
143- }
144- }
145-
146- if ( client ) {
147- await captureIncomingRequestBody ( client , request ) ;
148- }
149-
150- return continueTrace (
151- { sentryTrace : request . headers . get ( 'sentry-trace' ) || '' , baggage : request . headers . get ( 'baggage' ) } ,
152- ( ) => {
153- // Note: This span will not have a duration unless I/O happens in the handler. This is
154- // because of how the cloudflare workers runtime works.
155- // See: https://developers.cloudflare.com/workers/runtime-apis/performance/
156-
157- // Use startSpanManual to control when span ends (needed for streaming responses)
158- return startSpanManual ( { name, attributes } , async span => {
159- let res : Response ;
160-
161- try {
162- res = await handler ( ) ;
163- setHttpStatus ( span , res . status ) ;
164-
165- // After the handler runs, the span name might have been updated by nested instrumentation
166- // (e.g., Remix parameterizing routes). The span should already have the correct name
167- // from that instrumentation, so we don't need to do anything here.
168- } catch ( e ) {
169- span . end ( ) ;
170- if ( captureErrors ) {
171- captureException ( e , { mechanism : { handled : false , type : errorMechanismType } } ) ;
172- }
173- waitUntil ?.( flushAndDispose ( client ) ) ;
174- throw e ;
175- }
176-
177- // Classify response to detect actual streaming
178- const classification = classifyResponseStreaming ( res ) ;
179-
180- if ( classification . isStreaming && res . body ) {
181- try {
182- let ended = false ;
183-
184- const endSpanOnce = ( ) : void => {
185- if ( ended ) return ;
186-
187- ended = true ;
188- span . end ( ) ;
189- waitUntil ?.( flushAndDispose ( client ) ) ;
190- } ;
191-
192- const transform = new TransformStream ( {
193- flush ( ) {
194- // Source stream completed normally.
195- endSpanOnce ( ) ;
196- } ,
197- cancel ( ) {
198- // Client disconnected (or downstream cancelled). The `cancel`
199- // is being called while the response is still considered
200- // active, so this is a safe place to end the span.
201- endSpanOnce ( ) ;
202- } ,
203- } ) ;
204-
205- return new Response ( res . body . pipeThrough ( transform ) , {
206- status : res . status ,
207- statusText : res . statusText ,
208- headers : res . headers ,
209- } ) ;
210- } catch {
211- span . end ( ) ;
212- waitUntil ?.( flushAndDispose ( client ) ) ;
213- return res ;
214- }
215- }
216-
217- // Non-streaming response - end span immediately and return original
218- span . end ( ) ;
219-
220- // Don't dispose for protocol upgrades (101 Switching Protocols) - the connection stays alive.
221- // This includes WebSocket upgrades where webSocketMessage/webSocketClose handlers
222- // will still be called and may need the client to capture errors.
223- if ( res . status === 101 ) {
224- waitUntil ?.( client ?. flush ( 2000 ) ) ;
225- } else {
226- waitUntil ?.( flushAndDispose ( client ) ) ;
227- }
228- return res ;
229- } ) ;
230- } ,
231- ) ;
232- } ) ;
233- }
0 commit comments