UNPKG

11.1 kBJavaScriptView Raw
1import { logger, supportsNativeFetch, fill, GLOBAL_OBJ, addExceptionMechanism } from '@sentry/utils';
2
3/** HTTPClient integration creates events for failed client side HTTP requests. */
4class HttpClient {
5 /**
6 * @inheritDoc
7 */
8 static __initStatic() {this.id = 'HttpClient';}
9
10 /**
11 * @inheritDoc
12 */
13 __init() {this.name = HttpClient.id;}
14
15 /**
16 * Returns current hub.
17 */
18
19 /**
20 * @inheritDoc
21 *
22 * @param options
23 */
24 constructor(options) {HttpClient.prototype.__init.call(this);
25 this._options = {
26 failedRequestStatusCodes: [[500, 599]],
27 failedRequestTargets: [/.*/],
28 ...options,
29 };
30 }
31
32 /**
33 * @inheritDoc
34 *
35 * @param options
36 */
37 setupOnce(_, getCurrentHub) {
38 this._getCurrentHub = getCurrentHub;
39 this._wrapFetch();
40 this._wrapXHR();
41 }
42
43 /**
44 * Interceptor function for fetch requests
45 *
46 * @param requestInfo The Fetch API request info
47 * @param response The Fetch API response
48 * @param requestInit The request init object
49 */
50 _fetchResponseHandler(requestInfo, response, requestInit) {
51 if (this._getCurrentHub && this._shouldCaptureResponse(response.status, response.url)) {
52 const request = new Request(requestInfo, requestInit);
53 const hub = this._getCurrentHub();
54
55 let requestHeaders, responseHeaders, requestCookies, responseCookies;
56
57 if (hub.shouldSendDefaultPii()) {
58 [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] =
59 [
60 { cookieHeader: 'Cookie', obj: request },
61 { cookieHeader: 'Set-Cookie', obj: response },
62 ].map(({ cookieHeader, obj }) => {
63 const headers = this._extractFetchHeaders(obj.headers);
64 let cookies;
65
66 try {
67 const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined;
68
69 if (cookieString) {
70 cookies = this._parseCookieString(cookieString);
71 }
72 } catch (e) {
73 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log(`Could not extract cookies from header ${cookieHeader}`);
74 }
75
76 return {
77 headers,
78 cookies,
79 };
80 });
81 }
82
83 const event = this._createEvent({
84 url: request.url,
85 method: request.method,
86 status: response.status,
87 requestHeaders,
88 responseHeaders,
89 requestCookies,
90 responseCookies,
91 });
92
93 hub.captureEvent(event);
94 }
95 }
96
97 /**
98 * Interceptor function for XHR requests
99 *
100 * @param xhr The XHR request
101 * @param method The HTTP method
102 * @param headers The HTTP headers
103 */
104 _xhrResponseHandler(xhr, method, headers) {
105 if (this._getCurrentHub && this._shouldCaptureResponse(xhr.status, xhr.responseURL)) {
106 let requestHeaders, responseCookies, responseHeaders;
107 const hub = this._getCurrentHub();
108
109 if (hub.shouldSendDefaultPii()) {
110 try {
111 const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined;
112
113 if (cookieString) {
114 responseCookies = this._parseCookieString(cookieString);
115 }
116 } catch (e) {
117 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('Could not extract cookies from response headers');
118 }
119
120 try {
121 responseHeaders = this._getXHRResponseHeaders(xhr);
122 } catch (e) {
123 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('Could not extract headers from response');
124 }
125
126 requestHeaders = headers;
127 }
128
129 const event = this._createEvent({
130 url: xhr.responseURL,
131 method: method,
132 status: xhr.status,
133 requestHeaders,
134 // Can't access request cookies from XHR
135 responseHeaders,
136 responseCookies,
137 });
138
139 hub.captureEvent(event);
140 }
141 }
142
143 /**
144 * Extracts response size from `Content-Length` header when possible
145 *
146 * @param headers
147 * @returns The response size in bytes or undefined
148 */
149 _getResponseSizeFromHeaders(headers) {
150 if (headers) {
151 const contentLength = headers['Content-Length'] || headers['content-length'];
152
153 if (contentLength) {
154 return parseInt(contentLength, 10);
155 }
156 }
157
158 return undefined;
159 }
160
161 /**
162 * Creates an object containing cookies from the given cookie string
163 *
164 * @param cookieString The cookie string to parse
165 * @returns The parsed cookies
166 */
167 _parseCookieString(cookieString) {
168 return cookieString.split('; ').reduce((acc, cookie) => {
169 const [key, value] = cookie.split('=');
170 acc[key] = value;
171 return acc;
172 }, {});
173 }
174
175 /**
176 * Extracts the headers as an object from the given Fetch API request or response object
177 *
178 * @param headers The headers to extract
179 * @returns The extracted headers as an object
180 */
181 _extractFetchHeaders(headers) {
182 const result = {};
183
184 headers.forEach((value, key) => {
185 result[key] = value;
186 });
187
188 return result;
189 }
190
191 /**
192 * Extracts the response headers as an object from the given XHR object
193 *
194 * @param xhr The XHR object to extract the response headers from
195 * @returns The response headers as an object
196 */
197 _getXHRResponseHeaders(xhr) {
198 const headers = xhr.getAllResponseHeaders();
199
200 if (!headers) {
201 return {};
202 }
203
204 return headers.split('\r\n').reduce((acc, line) => {
205 const [key, value] = line.split(': ');
206 acc[key] = value;
207 return acc;
208 }, {});
209 }
210
211 /**
212 * Checks if the given target url is in the given list of targets
213 *
214 * @param target The target url to check
215 * @returns true if the target url is in the given list of targets, false otherwise
216 */
217 _isInGivenRequestTargets(target) {
218 if (!this._options.failedRequestTargets) {
219 return false;
220 }
221
222 return this._options.failedRequestTargets.some((givenRequestTarget) => {
223 if (typeof givenRequestTarget === 'string') {
224 return target.includes(givenRequestTarget);
225 }
226
227 return givenRequestTarget.test(target);
228 });
229 }
230
231 /**
232 * Checks if the given status code is in the given range
233 *
234 * @param status The status code to check
235 * @returns true if the status code is in the given range, false otherwise
236 */
237 _isInGivenStatusRanges(status) {
238 if (!this._options.failedRequestStatusCodes) {
239 return false;
240 }
241
242 return this._options.failedRequestStatusCodes.some((range) => {
243 if (typeof range === 'number') {
244 return range === status;
245 }
246
247 return status >= range[0] && status <= range[1];
248 });
249 }
250
251 /**
252 * Wraps `fetch` function to capture request and response data
253 */
254 _wrapFetch() {
255 if (!supportsNativeFetch()) {
256 return;
257 }
258
259 // eslint-disable-next-line @typescript-eslint/no-this-alias
260 const self = this;
261
262 fill(GLOBAL_OBJ, 'fetch', function (originalFetch) {
263 return function ( ...args) {
264 const [requestInfo, requestInit] = args ;
265 const responsePromise = originalFetch.apply(this, args);
266
267 responsePromise
268 .then((response) => {
269 self._fetchResponseHandler(requestInfo, response, requestInit);
270 return response;
271 })
272 .catch((error) => {
273 throw error;
274 });
275
276 return responsePromise;
277 };
278 });
279 }
280
281 /**
282 * Wraps XMLHttpRequest to capture request and response data
283 */
284 _wrapXHR() {
285 if (!('XMLHttpRequest' in GLOBAL_OBJ)) {
286 return;
287 }
288
289 // eslint-disable-next-line @typescript-eslint/no-this-alias
290 const self = this;
291
292 fill(XMLHttpRequest.prototype, 'open', function (originalOpen) {
293 return function ( ...openArgs) {
294 // eslint-disable-next-line @typescript-eslint/no-this-alias
295 const xhr = this;
296 const method = openArgs[0] ;
297 const headers = {};
298
299 // Intercepting `setRequestHeader` to access the request headers of XHR instance.
300 // This will only work for user/library defined headers, not for the default/browser-assigned headers.
301 // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`.
302 fill(
303 xhr,
304 'setRequestHeader',
305 // eslint-disable-next-line @typescript-eslint/ban-types
306 function (originalSetRequestHeader) {
307 return function (...setRequestHeaderArgs) {
308 const [header, value] = setRequestHeaderArgs ;
309
310 headers[header] = value;
311
312 return originalSetRequestHeader.apply(xhr, setRequestHeaderArgs);
313 };
314 },
315 );
316
317 // eslint-disable-next-line @typescript-eslint/ban-types
318 fill(xhr, 'onloadend', function (original) {
319 return function (...onloadendArgs) {
320 try {
321 self._xhrResponseHandler(xhr, method, headers);
322 } catch (e) {
323 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.warn('Error while extracting response event form XHR response', e);
324 }
325
326 if (original) {
327 return original.apply(xhr, onloadendArgs);
328 }
329 };
330 });
331
332 return originalOpen.apply(this, openArgs);
333 };
334 });
335 }
336
337 /**
338 * Checks whether given url points to Sentry server
339 *
340 * @param url url to verify
341 */
342 _isSentryRequest(url) {
343 const client = this._getCurrentHub && this._getCurrentHub().getClient();
344
345 if (!client) {
346 return false;
347 }
348
349 const dsn = client.getDsn();
350 return dsn ? url.includes(dsn.host) : false;
351 }
352
353 /**
354 * Checks whether to capture given response as an event
355 *
356 * @param status response status code
357 * @param url response url
358 */
359 _shouldCaptureResponse(status, url) {
360 return this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !this._isSentryRequest(url);
361 }
362
363 /**
364 * Creates a synthetic Sentry event from given response data
365 *
366 * @param data response data
367 * @returns event
368 */
369 _createEvent(data
370
371) {
372 const message = `HTTP Client Error with status code: ${data.status}`;
373
374 const event = {
375 message,
376 exception: {
377 values: [
378 {
379 type: 'Error',
380 value: message,
381 },
382 ],
383 },
384 request: {
385 url: data.url,
386 method: data.method,
387 headers: data.requestHeaders,
388 cookies: data.requestCookies,
389 },
390 contexts: {
391 response: {
392 status_code: data.status,
393 headers: data.responseHeaders,
394 cookies: data.responseCookies,
395 body_size: this._getResponseSizeFromHeaders(data.responseHeaders),
396 },
397 },
398 };
399
400 addExceptionMechanism(event, {
401 type: 'http.client',
402 });
403
404 return event;
405 }
406} HttpClient.__initStatic();
407
408export { HttpClient };
409//# sourceMappingURL=httpclient.js.map