request.js 9.5 KB

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