request.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { spanToJSON, hasTracingEnabled, setHttpStatus, getCurrentScope, getIsolationScope, startInactiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getClient, spanToTraceHeader, getDynamicSamplingContextFromSpan, getDynamicSamplingContextFromClient } from '@sentry/core';
  2. import { addFetchInstrumentationHandler, addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, generateSentryTraceHeader, dynamicSamplingContextToSentryBaggageHeader, BAGGAGE_HEADER_NAME, browserPerformanceTimeOrigin, stringMatchesSomePattern } from '@sentry/utils';
  3. import { instrumentFetchRequest } from '../common/fetch.js';
  4. import { addPerformanceInstrumentationHandler } from './instrument.js';
  5. /* eslint-disable max-lines */
  6. const DEFAULT_TRACE_PROPAGATION_TARGETS = ['localhost', /^\/(?!\/)/];
  7. /** Options for Request Instrumentation */
  8. const defaultRequestInstrumentationOptions = {
  9. traceFetch: true,
  10. traceXHR: true,
  11. enableHTTPTimings: true,
  12. // TODO (v8): Remove this property
  13. tracingOrigins: DEFAULT_TRACE_PROPAGATION_TARGETS,
  14. tracePropagationTargets: DEFAULT_TRACE_PROPAGATION_TARGETS,
  15. };
  16. /** Registers span creators for xhr and fetch requests */
  17. function instrumentOutgoingRequests(_options) {
  18. const {
  19. traceFetch,
  20. traceXHR,
  21. // eslint-disable-next-line deprecation/deprecation
  22. tracePropagationTargets,
  23. // eslint-disable-next-line deprecation/deprecation
  24. tracingOrigins,
  25. shouldCreateSpanForRequest,
  26. enableHTTPTimings,
  27. } = {
  28. traceFetch: defaultRequestInstrumentationOptions.traceFetch,
  29. traceXHR: defaultRequestInstrumentationOptions.traceXHR,
  30. ..._options,
  31. };
  32. const shouldCreateSpan =
  33. typeof shouldCreateSpanForRequest === 'function' ? shouldCreateSpanForRequest : (_) => true;
  34. // TODO(v8) Remove tracingOrigins here
  35. // The only reason we're passing it in here is because this instrumentOutgoingRequests function is publicly exported
  36. // and we don't want to break the API. We can remove it in v8.
  37. const shouldAttachHeadersWithTargets = (url) =>
  38. shouldAttachHeaders(url, tracePropagationTargets || tracingOrigins);
  39. const spans = {};
  40. if (traceFetch) {
  41. addFetchInstrumentationHandler(handlerData => {
  42. const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
  43. if (enableHTTPTimings && createdSpan) {
  44. addHTTPTimings(createdSpan);
  45. }
  46. });
  47. }
  48. if (traceXHR) {
  49. addXhrInstrumentationHandler(handlerData => {
  50. const createdSpan = xhrCallback(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans);
  51. if (enableHTTPTimings && createdSpan) {
  52. addHTTPTimings(createdSpan);
  53. }
  54. });
  55. }
  56. }
  57. function isPerformanceResourceTiming(entry) {
  58. return (
  59. entry.entryType === 'resource' &&
  60. 'initiatorType' in entry &&
  61. typeof (entry ).nextHopProtocol === 'string' &&
  62. (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest')
  63. );
  64. }
  65. /**
  66. * Creates a temporary observer to listen to the next fetch/xhr resourcing timings,
  67. * so that when timings hit their per-browser limit they don't need to be removed.
  68. *
  69. * @param span A span that has yet to be finished, must contain `url` on data.
  70. */
  71. function addHTTPTimings(span) {
  72. const { url } = spanToJSON(span).data || {};
  73. if (!url || typeof url !== 'string') {
  74. return;
  75. }
  76. const cleanup = addPerformanceInstrumentationHandler('resource', ({ entries }) => {
  77. entries.forEach(entry => {
  78. if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) {
  79. const spanData = resourceTimingEntryToSpanData(entry);
  80. spanData.forEach(data => span.setAttribute(...data));
  81. // In the next tick, clean this handler up
  82. // We have to wait here because otherwise this cleans itself up before it is fully done
  83. setTimeout(cleanup);
  84. }
  85. });
  86. });
  87. }
  88. /**
  89. * Converts ALPN protocol ids to name and version.
  90. *
  91. * (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
  92. * @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
  93. */
  94. function extractNetworkProtocol(nextHopProtocol) {
  95. let name = 'unknown';
  96. let version = 'unknown';
  97. let _name = '';
  98. for (const char of nextHopProtocol) {
  99. // http/1.1 etc.
  100. if (char === '/') {
  101. [name, version] = nextHopProtocol.split('/');
  102. break;
  103. }
  104. // h2, h3 etc.
  105. if (!isNaN(Number(char))) {
  106. name = _name === 'h' ? 'http' : _name;
  107. version = nextHopProtocol.split(_name)[1];
  108. break;
  109. }
  110. _name += char;
  111. }
  112. if (_name === nextHopProtocol) {
  113. // webrtc, ftp, etc.
  114. name = _name;
  115. }
  116. return { name, version };
  117. }
  118. function getAbsoluteTime(time = 0) {
  119. return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000;
  120. }
  121. function resourceTimingEntryToSpanData(resourceTiming) {
  122. const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol);
  123. const timingSpanData = [];
  124. timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]);
  125. if (!browserPerformanceTimeOrigin) {
  126. return timingSpanData;
  127. }
  128. return [
  129. ...timingSpanData,
  130. ['http.request.redirect_start', getAbsoluteTime(resourceTiming.redirectStart)],
  131. ['http.request.fetch_start', getAbsoluteTime(resourceTiming.fetchStart)],
  132. ['http.request.domain_lookup_start', getAbsoluteTime(resourceTiming.domainLookupStart)],
  133. ['http.request.domain_lookup_end', getAbsoluteTime(resourceTiming.domainLookupEnd)],
  134. ['http.request.connect_start', getAbsoluteTime(resourceTiming.connectStart)],
  135. ['http.request.secure_connection_start', getAbsoluteTime(resourceTiming.secureConnectionStart)],
  136. ['http.request.connection_end', getAbsoluteTime(resourceTiming.connectEnd)],
  137. ['http.request.request_start', getAbsoluteTime(resourceTiming.requestStart)],
  138. ['http.request.response_start', getAbsoluteTime(resourceTiming.responseStart)],
  139. ['http.request.response_end', getAbsoluteTime(resourceTiming.responseEnd)],
  140. ];
  141. }
  142. /**
  143. * A function that determines whether to attach tracing headers to a request.
  144. * This was extracted from `instrumentOutgoingRequests` to make it easier to test shouldAttachHeaders.
  145. * We only export this fuction for testing purposes.
  146. */
  147. function shouldAttachHeaders(url, tracePropagationTargets) {
  148. return stringMatchesSomePattern(url, tracePropagationTargets || DEFAULT_TRACE_PROPAGATION_TARGETS);
  149. }
  150. /**
  151. * Create and track xhr request spans
  152. *
  153. * @returns Span if a span was created, otherwise void.
  154. */
  155. // eslint-disable-next-line complexity
  156. function xhrCallback(
  157. handlerData,
  158. shouldCreateSpan,
  159. shouldAttachHeaders,
  160. spans,
  161. ) {
  162. const xhr = handlerData.xhr;
  163. const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY];
  164. if (!hasTracingEnabled() || !xhr || xhr.__sentry_own_request__ || !sentryXhrData) {
  165. return undefined;
  166. }
  167. const shouldCreateSpanResult = shouldCreateSpan(sentryXhrData.url);
  168. // check first if the request has finished and is tracked by an existing span which should now end
  169. if (handlerData.endTimestamp && shouldCreateSpanResult) {
  170. const spanId = xhr.__sentry_xhr_span_id__;
  171. if (!spanId) return;
  172. const span = spans[spanId];
  173. if (span && sentryXhrData.status_code !== undefined) {
  174. setHttpStatus(span, sentryXhrData.status_code);
  175. span.end();
  176. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  177. delete spans[spanId];
  178. }
  179. return undefined;
  180. }
  181. const scope = getCurrentScope();
  182. const isolationScope = getIsolationScope();
  183. const span = shouldCreateSpanResult
  184. ? startInactiveSpan({
  185. name: `${sentryXhrData.method} ${sentryXhrData.url}`,
  186. onlyIfParent: true,
  187. attributes: {
  188. type: 'xhr',
  189. 'http.method': sentryXhrData.method,
  190. url: sentryXhrData.url,
  191. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser',
  192. },
  193. op: 'http.client',
  194. })
  195. : undefined;
  196. if (span) {
  197. xhr.__sentry_xhr_span_id__ = span.spanContext().spanId;
  198. spans[xhr.__sentry_xhr_span_id__] = span;
  199. }
  200. const client = getClient();
  201. if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url) && client) {
  202. const { traceId, spanId, sampled, dsc } = {
  203. ...isolationScope.getPropagationContext(),
  204. ...scope.getPropagationContext(),
  205. };
  206. const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled);
  207. const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(
  208. dsc ||
  209. (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client, scope)),
  210. );
  211. setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader);
  212. }
  213. return span;
  214. }
  215. function setHeaderOnXhr(
  216. xhr,
  217. sentryTraceHeader,
  218. sentryBaggageHeader,
  219. ) {
  220. try {
  221. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  222. xhr.setRequestHeader('sentry-trace', sentryTraceHeader);
  223. if (sentryBaggageHeader) {
  224. // From MDN: "If this method is called several times with the same header, the values are merged into one single request header."
  225. // We can therefore simply set a baggage header without checking what was there before
  226. // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader
  227. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  228. xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader);
  229. }
  230. } catch (_) {
  231. // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
  232. }
  233. }
  234. export { DEFAULT_TRACE_PROPAGATION_TARGETS, defaultRequestInstrumentationOptions, extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders, xhrCallback };
  235. //# sourceMappingURL=request.js.map