httpclient.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import { defineIntegration, convertIntegrationFnToClass, getClient, captureEvent, isSentryRequestUrl } from '@sentry/core';
  2. import { supportsNativeFetch, addFetchInstrumentationHandler, GLOBAL_OBJ, addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, logger, addExceptionMechanism } from '@sentry/utils';
  3. import { DEBUG_BUILD } from './debug-build.js';
  4. const INTEGRATION_NAME = 'HttpClient';
  5. const _httpClientIntegration = ((options = {}) => {
  6. const _options = {
  7. failedRequestStatusCodes: [[500, 599]],
  8. failedRequestTargets: [/.*/],
  9. ...options,
  10. };
  11. return {
  12. name: INTEGRATION_NAME,
  13. // TODO v8: Remove this
  14. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  15. setup(client) {
  16. _wrapFetch(client, _options);
  17. _wrapXHR(client, _options);
  18. },
  19. };
  20. }) ;
  21. const httpClientIntegration = defineIntegration(_httpClientIntegration);
  22. /**
  23. * Create events for failed client side HTTP requests.
  24. * @deprecated Use `httpClientIntegration()` instead.
  25. */
  26. // eslint-disable-next-line deprecation/deprecation
  27. const HttpClient = convertIntegrationFnToClass(INTEGRATION_NAME, httpClientIntegration)
  28. ;
  29. /**
  30. * Interceptor function for fetch requests
  31. *
  32. * @param requestInfo The Fetch API request info
  33. * @param response The Fetch API response
  34. * @param requestInit The request init object
  35. */
  36. function _fetchResponseHandler(
  37. options,
  38. requestInfo,
  39. response,
  40. requestInit,
  41. ) {
  42. if (_shouldCaptureResponse(options, response.status, response.url)) {
  43. const request = _getRequest(requestInfo, requestInit);
  44. let requestHeaders, responseHeaders, requestCookies, responseCookies;
  45. if (_shouldSendDefaultPii()) {
  46. [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [
  47. { cookieHeader: 'Cookie', obj: request },
  48. { cookieHeader: 'Set-Cookie', obj: response },
  49. ].map(({ cookieHeader, obj }) => {
  50. const headers = _extractFetchHeaders(obj.headers);
  51. let cookies;
  52. try {
  53. const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined;
  54. if (cookieString) {
  55. cookies = _parseCookieString(cookieString);
  56. }
  57. } catch (e) {
  58. DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`);
  59. }
  60. return {
  61. headers,
  62. cookies,
  63. };
  64. });
  65. }
  66. const event = _createEvent({
  67. url: request.url,
  68. method: request.method,
  69. status: response.status,
  70. requestHeaders,
  71. responseHeaders,
  72. requestCookies,
  73. responseCookies,
  74. });
  75. captureEvent(event);
  76. }
  77. }
  78. /**
  79. * Interceptor function for XHR requests
  80. *
  81. * @param xhr The XHR request
  82. * @param method The HTTP method
  83. * @param headers The HTTP headers
  84. */
  85. function _xhrResponseHandler(
  86. options,
  87. xhr,
  88. method,
  89. headers,
  90. ) {
  91. if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) {
  92. let requestHeaders, responseCookies, responseHeaders;
  93. if (_shouldSendDefaultPii()) {
  94. try {
  95. const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined;
  96. if (cookieString) {
  97. responseCookies = _parseCookieString(cookieString);
  98. }
  99. } catch (e) {
  100. DEBUG_BUILD && logger.log('Could not extract cookies from response headers');
  101. }
  102. try {
  103. responseHeaders = _getXHRResponseHeaders(xhr);
  104. } catch (e) {
  105. DEBUG_BUILD && logger.log('Could not extract headers from response');
  106. }
  107. requestHeaders = headers;
  108. }
  109. const event = _createEvent({
  110. url: xhr.responseURL,
  111. method,
  112. status: xhr.status,
  113. requestHeaders,
  114. // Can't access request cookies from XHR
  115. responseHeaders,
  116. responseCookies,
  117. });
  118. captureEvent(event);
  119. }
  120. }
  121. /**
  122. * Extracts response size from `Content-Length` header when possible
  123. *
  124. * @param headers
  125. * @returns The response size in bytes or undefined
  126. */
  127. function _getResponseSizeFromHeaders(headers) {
  128. if (headers) {
  129. const contentLength = headers['Content-Length'] || headers['content-length'];
  130. if (contentLength) {
  131. return parseInt(contentLength, 10);
  132. }
  133. }
  134. return undefined;
  135. }
  136. /**
  137. * Creates an object containing cookies from the given cookie string
  138. *
  139. * @param cookieString The cookie string to parse
  140. * @returns The parsed cookies
  141. */
  142. function _parseCookieString(cookieString) {
  143. return cookieString.split('; ').reduce((acc, cookie) => {
  144. const [key, value] = cookie.split('=');
  145. acc[key] = value;
  146. return acc;
  147. }, {});
  148. }
  149. /**
  150. * Extracts the headers as an object from the given Fetch API request or response object
  151. *
  152. * @param headers The headers to extract
  153. * @returns The extracted headers as an object
  154. */
  155. function _extractFetchHeaders(headers) {
  156. const result = {};
  157. headers.forEach((value, key) => {
  158. result[key] = value;
  159. });
  160. return result;
  161. }
  162. /**
  163. * Extracts the response headers as an object from the given XHR object
  164. *
  165. * @param xhr The XHR object to extract the response headers from
  166. * @returns The response headers as an object
  167. */
  168. function _getXHRResponseHeaders(xhr) {
  169. const headers = xhr.getAllResponseHeaders();
  170. if (!headers) {
  171. return {};
  172. }
  173. return headers.split('\r\n').reduce((acc, line) => {
  174. const [key, value] = line.split(': ');
  175. acc[key] = value;
  176. return acc;
  177. }, {});
  178. }
  179. /**
  180. * Checks if the given target url is in the given list of targets
  181. *
  182. * @param target The target url to check
  183. * @returns true if the target url is in the given list of targets, false otherwise
  184. */
  185. function _isInGivenRequestTargets(
  186. failedRequestTargets,
  187. target,
  188. ) {
  189. return failedRequestTargets.some((givenRequestTarget) => {
  190. if (typeof givenRequestTarget === 'string') {
  191. return target.includes(givenRequestTarget);
  192. }
  193. return givenRequestTarget.test(target);
  194. });
  195. }
  196. /**
  197. * Checks if the given status code is in the given range
  198. *
  199. * @param status The status code to check
  200. * @returns true if the status code is in the given range, false otherwise
  201. */
  202. function _isInGivenStatusRanges(
  203. failedRequestStatusCodes,
  204. status,
  205. ) {
  206. return failedRequestStatusCodes.some((range) => {
  207. if (typeof range === 'number') {
  208. return range === status;
  209. }
  210. return status >= range[0] && status <= range[1];
  211. });
  212. }
  213. /**
  214. * Wraps `fetch` function to capture request and response data
  215. */
  216. function _wrapFetch(client, options) {
  217. if (!supportsNativeFetch()) {
  218. return;
  219. }
  220. addFetchInstrumentationHandler(handlerData => {
  221. if (getClient() !== client) {
  222. return;
  223. }
  224. const { response, args } = handlerData;
  225. const [requestInfo, requestInit] = args ;
  226. if (!response) {
  227. return;
  228. }
  229. _fetchResponseHandler(options, requestInfo, response , requestInit);
  230. });
  231. }
  232. /**
  233. * Wraps XMLHttpRequest to capture request and response data
  234. */
  235. function _wrapXHR(client, options) {
  236. if (!('XMLHttpRequest' in GLOBAL_OBJ)) {
  237. return;
  238. }
  239. addXhrInstrumentationHandler(handlerData => {
  240. if (getClient() !== client) {
  241. return;
  242. }
  243. const xhr = handlerData.xhr ;
  244. const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY];
  245. if (!sentryXhrData) {
  246. return;
  247. }
  248. const { method, request_headers: headers } = sentryXhrData;
  249. try {
  250. _xhrResponseHandler(options, xhr, method, headers);
  251. } catch (e) {
  252. DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e);
  253. }
  254. });
  255. }
  256. /**
  257. * Checks whether to capture given response as an event
  258. *
  259. * @param status response status code
  260. * @param url response url
  261. */
  262. function _shouldCaptureResponse(options, status, url) {
  263. return (
  264. _isInGivenStatusRanges(options.failedRequestStatusCodes, status) &&
  265. _isInGivenRequestTargets(options.failedRequestTargets, url) &&
  266. !isSentryRequestUrl(url, getClient())
  267. );
  268. }
  269. /**
  270. * Creates a synthetic Sentry event from given response data
  271. *
  272. * @param data response data
  273. * @returns event
  274. */
  275. function _createEvent(data
  276. ) {
  277. const message = `HTTP Client Error with status code: ${data.status}`;
  278. const event = {
  279. message,
  280. exception: {
  281. values: [
  282. {
  283. type: 'Error',
  284. value: message,
  285. },
  286. ],
  287. },
  288. request: {
  289. url: data.url,
  290. method: data.method,
  291. headers: data.requestHeaders,
  292. cookies: data.requestCookies,
  293. },
  294. contexts: {
  295. response: {
  296. status_code: data.status,
  297. headers: data.responseHeaders,
  298. cookies: data.responseCookies,
  299. body_size: _getResponseSizeFromHeaders(data.responseHeaders),
  300. },
  301. },
  302. };
  303. addExceptionMechanism(event, {
  304. type: 'http.client',
  305. handled: false,
  306. });
  307. return event;
  308. }
  309. function _getRequest(requestInfo, requestInit) {
  310. if (!requestInit && requestInfo instanceof Request) {
  311. return requestInfo;
  312. }
  313. // If both are set, we try to construct a new Request with the given arguments
  314. // However, if e.g. the original request has a `body`, this will throw an error because it was already accessed
  315. // In this case, as a fallback, we just use the original request - using both is rather an edge case
  316. if (requestInfo instanceof Request && requestInfo.bodyUsed) {
  317. return requestInfo;
  318. }
  319. return new Request(requestInfo, requestInit);
  320. }
  321. function _shouldSendDefaultPii() {
  322. const client = getClient();
  323. return client ? Boolean(client.getOptions().sendDefaultPii) : false;
  324. }
  325. export { HttpClient, httpClientIntegration };
  326. //# sourceMappingURL=httpclient.js.map