1- import type { Client , HandlerDataXhr , SentryWrappedXMLHttpRequest , Span , WebFetchHeaders } from '@sentry/core' ;
1+ import type {
2+ Client ,
3+ HandlerDataXhr ,
4+ RequestHookInfo ,
5+ ResponseHookInfo ,
6+ SentryWrappedXMLHttpRequest ,
7+ Span ,
8+ } from '@sentry/core' ;
29import {
310 addFetchEndInstrumentationHandler ,
411 addFetchInstrumentationHandler ,
@@ -26,7 +33,7 @@ import {
2633 SENTRY_XHR_DATA_KEY ,
2734} from '@sentry-internal/browser-utils' ;
2835import type { BrowserClient } from '../client' ;
29- import { WINDOW } from '../helpers ' ;
36+ import { baggageHeaderHasSentryValues , createHeadersSafely , getFullURL , isPerformanceResourceTiming } from './utils ' ;
3037
3138/** Options for Request Instrumentation */
3239export interface RequestInstrumentationOptions {
@@ -102,7 +109,12 @@ export interface RequestInstrumentationOptions {
102109 /**
103110 * Is called when spans are started for outgoing requests.
104111 */
105- onRequestSpanStart ?( span : Span , requestInformation : { headers ?: WebFetchHeaders } ) : void ;
112+ onRequestSpanStart ?( span : Span , requestInformation : RequestHookInfo ) : void ;
113+
114+ /**
115+ * Is called when spans end for outgoing requests, providing access to response headers.
116+ */
117+ onRequestSpanEnd ?( span : Span , responseInformation : ResponseHookInfo ) : void ;
106118}
107119
108120const responseToSpanId = new WeakMap < object , string > ( ) ;
@@ -125,6 +137,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
125137 enableHTTPTimings,
126138 tracePropagationTargets,
127139 onRequestSpanStart,
140+ onRequestSpanEnd,
128141 } = {
129142 ...defaultRequestInstrumentationOptions ,
130143 ..._options ,
@@ -171,6 +184,7 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
171184 addFetchInstrumentationHandler ( handlerData => {
172185 const createdSpan = instrumentFetchRequest ( handlerData , shouldCreateSpan , shouldAttachHeadersWithTargets , spans , {
173186 propagateTraceparent,
187+ onRequestSpanEnd,
174188 } ) ;
175189
176190 if ( handlerData . response && handlerData . fetchData . __span ) {
@@ -205,34 +219,22 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial<Re
205219 shouldAttachHeadersWithTargets ,
206220 spans ,
207221 propagateTraceparent ,
222+ onRequestSpanEnd ,
208223 ) ;
209224
210225 if ( createdSpan ) {
211226 if ( enableHTTPTimings ) {
212227 addHTTPTimings ( createdSpan ) ;
213228 }
214229
215- let headers ;
216- try {
217- headers = new Headers ( handlerData . xhr . __sentry_xhr_v3__ ?. request_headers ) ;
218- } catch {
219- // noop
220- }
221- onRequestSpanStart ?.( createdSpan , { headers } ) ;
230+ onRequestSpanStart ?.( createdSpan , {
231+ headers : createHeadersSafely ( handlerData . xhr . __sentry_xhr_v3__ ?. request_headers ) ,
232+ } ) ;
222233 }
223234 } ) ;
224235 }
225236}
226237
227- function isPerformanceResourceTiming ( entry : PerformanceEntry ) : entry is PerformanceResourceTiming {
228- return (
229- entry . entryType === 'resource' &&
230- 'initiatorType' in entry &&
231- typeof ( entry as PerformanceResourceTiming ) . nextHopProtocol === 'string' &&
232- ( entry . initiatorType === 'fetch' || entry . initiatorType === 'xmlhttprequest' )
233- ) ;
234- }
235-
236238/**
237239 * Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
238240 * so that when timings hit their per-browser limit they don't need to be removed.
@@ -315,6 +317,7 @@ function xhrCallback(
315317 shouldAttachHeaders : ( url : string ) => boolean ,
316318 spans : Record < string , Span > ,
317319 propagateTraceparent ?: boolean ,
320+ onRequestSpanEnd ?: RequestInstrumentationOptions [ 'onRequestSpanEnd' ] ,
318321) : Span | undefined {
319322 const xhr = handlerData . xhr ;
320323 const sentryXhrData = xhr ?. [ SENTRY_XHR_DATA_KEY ] ;
@@ -337,6 +340,10 @@ function xhrCallback(
337340 setHttpStatus ( span , sentryXhrData . status_code ) ;
338341 span . end ( ) ;
339342
343+ onRequestSpanEnd ?.( span , {
344+ headers : createHeadersSafely ( sentryXhrData . request_headers ) ,
345+ } ) ;
346+
340347 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
341348 delete spans [ spanId ] ;
342349 }
@@ -438,18 +445,3 @@ function setHeaderOnXhr(
438445 // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
439446 }
440447}
441-
442- function baggageHeaderHasSentryValues ( baggageHeader : string ) : boolean {
443- return baggageHeader . split ( ',' ) . some ( value => value . trim ( ) . startsWith ( 'sentry-' ) ) ;
444- }
445-
446- function getFullURL ( url : string ) : string | undefined {
447- try {
448- // By adding a base URL to new URL(), this will also work for relative urls
449- // If `url` is a full URL, the base URL is ignored anyhow
450- const parsed = new URL ( url , WINDOW . location . origin ) ;
451- return parsed . href ;
452- } catch {
453- return undefined ;
454- }
455- }
0 commit comments