@@ -4,6 +4,7 @@ import type { DynamicSamplingContext, Span } from '@sentry/types';
44import {
55 addInstrumentationHandler ,
66 BAGGAGE_HEADER_NAME ,
7+ browserPerformanceTimeOrigin ,
78 dynamicSamplingContextToSentryBaggageHeader ,
89 isInstanceOf ,
910 SENTRY_XHR_DATA_KEY ,
@@ -14,6 +15,13 @@ export const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
1415
1516/** Options for Request Instrumentation */
1617export interface RequestInstrumentationOptions {
18+ /**
19+ * Allow experiments for the request instrumentation.
20+ */
21+ _experiments : Partial < {
22+ enableHTTPTimings : boolean ;
23+ } > ;
24+
1725 /**
1826 * @deprecated Will be removed in v8.
1927 * Use `shouldCreateSpanForRequest` to control span creation and `tracePropagationTargets` to control
@@ -108,12 +116,13 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions
108116 // TODO (v8): Remove this property
109117 tracingOrigins : DEFAULT_TRACE_PROPAGATION_TARGETS ,
110118 tracePropagationTargets : DEFAULT_TRACE_PROPAGATION_TARGETS ,
119+ _experiments : { } ,
111120} ;
112121
113122/** Registers span creators for xhr and fetch requests */
114123export function instrumentOutgoingRequests ( _options ?: Partial < RequestInstrumentationOptions > ) : void {
115124 // eslint-disable-next-line deprecation/deprecation
116- const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest } = {
125+ const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = {
117126 traceFetch : defaultRequestInstrumentationOptions . traceFetch ,
118127 traceXHR : defaultRequestInstrumentationOptions . traceXHR ,
119128 ..._options ,
@@ -132,15 +141,63 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
132141
133142 if ( traceFetch ) {
134143 addInstrumentationHandler ( 'fetch' , ( handlerData : FetchData ) => {
135- fetchCallback ( handlerData , shouldCreateSpan , shouldAttachHeadersWithTargets , spans ) ;
144+ const createdSpan = fetchCallback ( handlerData , shouldCreateSpan , shouldAttachHeadersWithTargets , spans ) ;
145+ if ( _experiments ?. enableHTTPTimings && createdSpan ) {
146+ addHTTPTimings ( createdSpan ) ;
147+ }
136148 } ) ;
137149 }
138150
139151 if ( traceXHR ) {
140152 addInstrumentationHandler ( 'xhr' , ( handlerData : XHRData ) => {
141- xhrCallback ( handlerData , shouldCreateSpan , shouldAttachHeadersWithTargets , spans ) ;
153+ const createdSpan = xhrCallback ( handlerData , shouldCreateSpan , shouldAttachHeadersWithTargets , spans ) ;
154+ if ( _experiments ?. enableHTTPTimings && createdSpan ) {
155+ addHTTPTimings ( createdSpan ) ;
156+ }
157+ } ) ;
158+ }
159+ }
160+
161+ /**
162+ * Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
163+ * so that when timings hit their per-browser limit they don't need to be removed.
164+ *
165+ * @param span A span that has yet to be finished, must contain `url` on data.
166+ */
167+ function addHTTPTimings ( span : Span ) : void {
168+ const url = span . data . url ;
169+ const observer = new PerformanceObserver ( list => {
170+ const entries = list . getEntries ( ) as PerformanceResourceTiming [ ] ;
171+ entries . forEach ( entry => {
172+ if ( ( entry . initiatorType === 'fetch' || entry . initiatorType === 'xmlhttprequest' ) && entry . name . endsWith ( url ) ) {
173+ const spanData = resourceTimingEntryToSpanData ( entry ) ;
174+ spanData . forEach ( data => span . setData ( ...data ) ) ;
175+ observer . disconnect ( ) ;
176+ }
142177 } ) ;
178+ } ) ;
179+ observer . observe ( {
180+ entryTypes : [ 'resource' ] ,
181+ } ) ;
182+ }
183+
184+ function resourceTimingEntryToSpanData ( resourceTiming : PerformanceResourceTiming ) : [ string , string | number ] [ ] {
185+ const version = resourceTiming . nextHopProtocol . split ( '/' ) [ 1 ] || 'none' ;
186+
187+ const timingSpanData : [ string , string | number ] [ ] = [ ] ;
188+ if ( version ) {
189+ timingSpanData . push ( [ 'network.protocol.version' , version ] ) ;
190+ }
191+
192+ if ( ! browserPerformanceTimeOrigin ) {
193+ return timingSpanData ;
143194 }
195+ return [
196+ ...timingSpanData ,
197+ [ 'http.request.connect_start' , ( browserPerformanceTimeOrigin + resourceTiming . connectStart ) / 1000 ] ,
198+ [ 'http.request.request_start' , ( browserPerformanceTimeOrigin + resourceTiming . requestStart ) / 1000 ] ,
199+ [ 'http.request.response_start' , ( browserPerformanceTimeOrigin + resourceTiming . responseStart ) / 1000 ] ,
200+ ] ;
144201}
145202
146203/**
@@ -154,13 +211,15 @@ export function shouldAttachHeaders(url: string, tracePropagationTargets: (strin
154211
155212/**
156213 * Create and track fetch request spans
214+ *
215+ * @returns Span if a span was created, otherwise void.
157216 */
158- export function fetchCallback (
217+ function fetchCallback (
159218 handlerData : FetchData ,
160219 shouldCreateSpan : ( url : string ) => boolean ,
161220 shouldAttachHeaders : ( url : string ) => boolean ,
162221 spans : Record < string , Span > ,
163- ) : void {
222+ ) : Span | void {
164223 if ( ! hasTracingEnabled ( ) || ! ( handlerData . fetchData && shouldCreateSpan ( handlerData . fetchData . url ) ) ) {
165224 return ;
166225 }
@@ -229,6 +288,7 @@ export function fetchCallback(
229288 options ,
230289 ) ;
231290 }
291+ return span ;
232292 }
233293}
234294
@@ -301,13 +361,15 @@ export function addTracingHeadersToFetchRequest(
301361
302362/**
303363 * Create and track xhr request spans
364+ *
365+ * @returns Span if a span was created, otherwise void.
304366 */
305- export function xhrCallback (
367+ function xhrCallback (
306368 handlerData : XHRData ,
307369 shouldCreateSpan : ( url : string ) => boolean ,
308370 shouldAttachHeaders : ( url : string ) => boolean ,
309371 spans : Record < string , Span > ,
310- ) : void {
372+ ) : Span | void {
311373 const xhr = handlerData . xhr ;
312374 const sentryXhrData = xhr && xhr [ SENTRY_XHR_DATA_KEY ] ;
313375
@@ -370,5 +432,7 @@ export function xhrCallback(
370432 // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
371433 }
372434 }
435+
436+ return span ;
373437 }
374438}
0 commit comments