this repo has no description
1import { _optionalChain } from '@sentry/utils/esm/buildPolyfills'; 2import { hasTracingEnabled, getCurrentHub } from '@sentry/core'; 3import { addInstrumentationHandler, browserPerformanceTimeOrigin, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, BAGGAGE_HEADER_NAME, SENTRY_XHR_DATA_KEY, stringMatchesSomePattern } from '@sentry/utils'; 4 5/* eslint-disable max-lines */ 6 7const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/]; 8 9/** Options for Request Instrumentation */ 10 11const defaultRequestInstrumentationOptions = { 12 traceFetch: true, 13 traceXHR: true, 14 // TODO (v8): Remove this property 15 tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS, 16 tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS, 17 _experiments: {}, 18}; 19 20/** Registers span creators for xhr and fetch requests */ 21function instrumentOutgoingRequests(_options) { 22 // eslint-disable-next-line deprecation/deprecation 23 const { traceFetch, traceXHR, tracePropagationTargets, tracingOrigins, shouldCreateSpanForRequest, _experiments } = { 24 traceFetch: defaultRequestInstrumentationOptions.traceFetch, 25 traceXHR: defaultRequestInstrumentationOptions.traceXHR, 26 ..._options, 27 }; 28 29 const shouldCreateSpan = 30 typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_) => true; 31 32 // TODO(v8) Remove tracingOrigins here 33 // The only reason we're passing it in here is because this instrumentOutgoingRequests function is publicly exported 34 // and we don't want to break the API. We can remove it in v8. 35 const shouldAttachHeadersWithTargets = (url) => 36 shouldAttachHeaders(url, tracePropagationTargets || tracingOrigins); 37 38 const spans = {}; 39 40 if (traceFetch) { 41 addInstrumentationHandler('fetch', (handlerData) => { 42 const createdSpan = fetchCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); 43 if (_optionalChain([_experiments, 'optionalAccess', _2 => _2.enableHTTPTimings]) && createdSpan) { 44 addHTTPTimings(createdSpan); 45 } 46 }); 47 } 48 49 if (traceXHR) { 50 addInstrumentationHandler('xhr', (handlerData) => { 51 const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans); 52 if (_optionalChain([_experiments, 'optionalAccess', _3 => _3.enableHTTPTimings]) && createdSpan) { 53 addHTTPTimings(createdSpan); 54 } 55 }); 56 } 57} 58 59/** 60 * Creates a temporary observer to listen to the next fetch/xhr resourcing timings, 61 * so that when timings hit their per-browser limit they don't need to be removed. 62 * 63 * @param span A span that has yet to be finished, must contain `url` on data. 64 */ 65function addHTTPTimings(span) { 66 const url = span.data.url; 67 const observer = new PerformanceObserver(list => { 68 const entries = list.getEntries() ; 69 entries.forEach(entry => { 70 if ((entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') && entry.name.endsWith(url)) { 71 const spanData = resourceTimingEntryToSpanData(entry); 72 spanData.forEach(data => span.setData(...data)); 73 observer.disconnect(); 74 } 75 }); 76 }); 77 observer.observe({ 78 entryTypes: ['resource'], 79 }); 80} 81 82function resourceTimingEntryToSpanData(resourceTiming) { 83 const version = resourceTiming.nextHopProtocol.split('/')[1] || 'none'; 84 85 const timingSpanData = []; 86 if (version) { 87 timingSpanData.push(['network.protocol.version', version]); 88 } 89 90 if (!browserPerformanceTimeOrigin) { 91 return timingSpanData; 92 } 93 return [ 94 ...timingSpanData, 95 ['http.request.connect_start', (browserPerformanceTimeOrigin + resourceTiming.connectStart) / 1000], 96 ['http.request.request_start', (browserPerformanceTimeOrigin + resourceTiming.requestStart) / 1000], 97 ['http.request.response_start', (browserPerformanceTimeOrigin + resourceTiming.responseStart) / 1000], 98 ]; 99} 100 101/** 102 * A function that determines whether to attach tracing headers to a request. 103 * This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders. 104 * We only export this fuction for testing purposes. 105 */ 106function shouldAttachHeaders(url, tracePropagationTargets) { 107 return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS); 108} 109 110/** 111 * Create and track fetch request spans 112 * 113 * @returns Span if a span was created, otherwise void. 114 */ 115function fetchCallback( 116 handlerData, 117 shouldCreateSpan, 118 shouldAttachHeaders, 119 spans, 120) { 121 if (!hasTracingEnabled() || !(handlerData.fetchData && shouldCreateSpan(handlerData.fetchData.url))) { 122 return; 123 } 124 125 if (handlerData.endTimestamp) { 126 const spanId = handlerData.fetchData.__span; 127 if (!spanId) return; 128 129 const span = spans[spanId]; 130 if (span) { 131 if (handlerData.response) { 132 // TODO (kmclb) remove this once types PR goes through 133 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 134 span.setHttpStatus(handlerData.response.status); 135 136 const contentLength = 137 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 138 handlerData.response && handlerData.response.headers && handlerData.response.headers.get('content-length'); 139 140 const contentLengthNum = parseInt(contentLength); 141 if (contentLengthNum > 0) { 142 span.setData('http.response_content_length', contentLengthNum); 143 } 144 } else if (handlerData.error) { 145 span.setStatus('internal_error'); 146 } 147 span.finish(); 148 149 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 150 delete spans[spanId]; 151 } 152 return; 153 } 154 155 const currentSpan = getCurrentHub().getScope().getSpan(); 156 const activeTransaction = currentSpan && currentSpan.transaction; 157 158 if (currentSpan && activeTransaction) { 159 const { method, url } = handlerData.fetchData; 160 const span = currentSpan.startChild({ 161 data: { 162 url, 163 type: 'fetch', 164 'http.method': method, 165 }, 166 description: `${method} ${url}`, 167 op: 'http.client', 168 }); 169 170 handlerData.fetchData.__span = span.spanId; 171 spans[span.spanId] = span; 172 173 const request = handlerData.args[0]; 174 175 // In case the user hasn't set the second argument of a fetch call we default it to `{}`. 176 handlerData.args[1] = handlerData.args[1] || {}; 177 178 // eslint-disable-next-line @typescript-eslint/no-explicit-any 179 const options = handlerData.args[1]; 180 181 if (shouldAttachHeaders(handlerData.fetchData.url)) { 182 options.headers = addTracingHeadersToFetchRequest( 183 request, 184 activeTransaction.getDynamicSamplingContext(), 185 span, 186 options, 187 ); 188 } 189 return span; 190 } 191} 192 193/** 194 * Adds sentry-trace and baggage headers to the various forms of fetch headers 195 */ 196function addTracingHeadersToFetchRequest( 197 request, // unknown is actually type Request but we can't export DOM types from this package, 198 dynamicSamplingContext, 199 span, 200 options 201 202, 203) { 204 const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); 205 const sentryTraceHeader = span.toTraceparent(); 206 207 const headers = 208 typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request ).headers : options.headers; 209 210 if (!headers) { 211 return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader }; 212 } else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) { 213 const newHeaders = new Headers(headers ); 214 215 newHeaders.append('sentry-trace', sentryTraceHeader); 216 217 if (sentryBaggageHeader) { 218 // If the same header is appended multiple times the browser will merge the values into a single request header. 219 // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. 220 newHeaders.append(BAGGAGE_HEADER_NAME, sentryBaggageHeader); 221 } 222 223 return newHeaders ; 224 } else if (Array.isArray(headers)) { 225 const newHeaders = [...headers, ['sentry-trace', sentryTraceHeader]]; 226 227 if (sentryBaggageHeader) { 228 // If there are multiple entries with the same key, the browser will merge the values into a single request header. 229 // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. 230 newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]); 231 } 232 233 return newHeaders ; 234 } else { 235 const existingBaggageHeader = 'baggage' in headers ? headers.baggage : undefined; 236 const newBaggageHeaders = []; 237 238 if (Array.isArray(existingBaggageHeader)) { 239 newBaggageHeaders.push(...existingBaggageHeader); 240 } else if (existingBaggageHeader) { 241 newBaggageHeaders.push(existingBaggageHeader); 242 } 243 244 if (sentryBaggageHeader) { 245 newBaggageHeaders.push(sentryBaggageHeader); 246 } 247 248 return { 249 ...(headers ), 250 'sentry-trace': sentryTraceHeader, 251 baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined, 252 }; 253 } 254} 255 256/** 257 * Create and track xhr request spans 258 * 259 * @returns Span if a span was created, otherwise void. 260 */ 261function xhrCallback( 262 handlerData, 263 shouldCreateSpan, 264 shouldAttachHeaders, 265 spans, 266) { 267 const xhr = handlerData.xhr; 268 const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY]; 269 270 if ( 271 !hasTracingEnabled() || 272 (xhr && xhr.__sentry_own_request__) || 273 !(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url)) 274 ) { 275 return; 276 } 277 278 // check first if the request has finished and is tracked by an existing span which should now end 279 if (handlerData.endTimestamp) { 280 const spanId = xhr.__sentry_xhr_span_id__; 281 if (!spanId) return; 282 283 const span = spans[spanId]; 284 if (span) { 285 span.setHttpStatus(sentryXhrData.status_code); 286 span.finish(); 287 288 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 289 delete spans[spanId]; 290 } 291 return; 292 } 293 294 const currentSpan = getCurrentHub().getScope().getSpan(); 295 const activeTransaction = currentSpan && currentSpan.transaction; 296 297 if (currentSpan && activeTransaction) { 298 const span = currentSpan.startChild({ 299 data: { 300 ...sentryXhrData.data, 301 type: 'xhr', 302 'http.method': sentryXhrData.method, 303 url: sentryXhrData.url, 304 }, 305 description: `${sentryXhrData.method} ${sentryXhrData.url}`, 306 op: 'http.client', 307 }); 308 309 xhr.__sentry_xhr_span_id__ = span.spanId; 310 spans[xhr.__sentry_xhr_span_id__] = span; 311 312 if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { 313 try { 314 xhr.setRequestHeader('sentry-trace', span.toTraceparent()); 315 316 const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext(); 317 const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); 318 319 if (sentryBaggageHeader) { 320 // From MDN: "If this method is called several times with the same header, the values are merged into one single request header." 321 // We can therefore simply set a baggage header without checking what was there before 322 // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader 323 xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader); 324 } 325 } catch (_) { 326 // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. 327 } 328 } 329 330 return span; 331 } 332} 333 334export { DEFAULT_TRACE_PROPAGATION_TARGETS, addTracingHeadersToFetchRequest, defaultRequestInstrumentationOptions, instrumentOutgoingRequests, shouldAttachHeaders }; 335//# sourceMappingURL=request.js.map