httpclient.js 9.4 KB

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