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