@@ -4,6 +4,7 @@ import type { DynamicSamplingContext, Span } from '@sentry/types';
4
4
import {
5
5
addInstrumentationHandler ,
6
6
BAGGAGE_HEADER_NAME ,
7
+ browserPerformanceTimeOrigin ,
7
8
dynamicSamplingContextToSentryBaggageHeader ,
8
9
isInstanceOf ,
9
10
SENTRY_XHR_DATA_KEY ,
@@ -14,6 +15,13 @@ export const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
14
15
15
16
/** Options for Request Instrumentation */
16
17
export interface RequestInstrumentationOptions {
18
+ /**
19
+ * Allow experiments for the request instrumentation.
20
+ */
21
+ _experiments : Partial < {
22
+ enableHTTPTimings : boolean ;
23
+ } > ;
24
+
17
25
/**
18
26
* @deprecated Will be removed in v8.
19
27
* Use `shouldCreateSpanForRequest` to control span creation and `tracePropagationTargets` to control
@@ -108,12 +116,13 @@ export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions
108
116
// TODO (v8): Remove this property
109
117
tracingOrigins : DEFAULT_TRACE_PROPAGATION_TARGETS ,
110
118
tracePropagationTargets : DEFAULT_TRACE_PROPAGATION_TARGETS ,
119
+ _experiments : { } ,
111
120
} ;
112
121
113
122
/** Registers span creators for xhr and fetch requests */
114
123
export function instrumentOutgoingRequests ( _options ?: Partial < RequestInstrumentationOptions > ) : void {
115
124
// eslint-disable-next-line deprecation/deprecation
116
- const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest } = {
125
+ const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = {
117
126
traceFetch : defaultRequestInstrumentationOptions . traceFetch ,
118
127
traceXHR : defaultRequestInstrumentationOptions . traceXHR ,
119
128
..._options ,
@@ -132,15 +141,63 @@ export function instrumentOutgoingRequests(_options?: Partial<RequestInstrumenta
132
141
133
142
if ( traceFetch ) {
134
143
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
+ }
136
148
} ) ;
137
149
}
138
150
139
151
if ( traceXHR ) {
140
152
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
+ }
142
177
} ) ;
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 ;
143
194
}
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
+ ] ;
144
201
}
145
202
146
203
/**
@@ -154,13 +211,15 @@ export function shouldAttachHeaders(url: string, tracePropagationTargets: (strin
154
211
155
212
/**
156
213
* Create and track fetch request spans
214
+ *
215
+ * @returns Span if a span was created, otherwise void.
157
216
*/
158
- export function fetchCallback (
217
+ function fetchCallback (
159
218
handlerData : FetchData ,
160
219
shouldCreateSpan : ( url : string ) => boolean ,
161
220
shouldAttachHeaders : ( url : string ) => boolean ,
162
221
spans : Record < string , Span > ,
163
- ) : void {
222
+ ) : Span | void {
164
223
if ( ! hasTracingEnabled ( ) || ! ( handlerData . fetchData && shouldCreateSpan ( handlerData . fetchData . url ) ) ) {
165
224
return ;
166
225
}
@@ -229,6 +288,7 @@ export function fetchCallback(
229
288
options ,
230
289
) ;
231
290
}
291
+ return span ;
232
292
}
233
293
}
234
294
@@ -301,13 +361,15 @@ export function addTracingHeadersToFetchRequest(
301
361
302
362
/**
303
363
* Create and track xhr request spans
364
+ *
365
+ * @returns Span if a span was created, otherwise void.
304
366
*/
305
- export function xhrCallback (
367
+ function xhrCallback (
306
368
handlerData : XHRData ,
307
369
shouldCreateSpan : ( url : string ) => boolean ,
308
370
shouldAttachHeaders : ( url : string ) => boolean ,
309
371
spans : Record < string , Span > ,
310
- ) : void {
372
+ ) : Span | void {
311
373
const xhr = handlerData . xhr ;
312
374
const sentryXhrData = xhr && xhr [ SENTRY_XHR_DATA_KEY ] ;
313
375
@@ -370,5 +432,7 @@ export function xhrCallback(
370
432
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
371
433
}
372
434
}
435
+
436
+ return span ;
373
437
}
374
438
}
0 commit comments