1 | import { logger, supportsNativeFetch, fill, GLOBAL_OBJ, addExceptionMechanism } from '@sentry/utils';
|
2 |
|
3 |
|
4 | class HttpClient {
|
5 | |
6 |
|
7 |
|
8 | static __initStatic() {this.id = 'HttpClient';}
|
9 |
|
10 | |
11 |
|
12 |
|
13 | __init() {this.name = HttpClient.id;}
|
14 |
|
15 | |
16 |
|
17 |
|
18 |
|
19 | |
20 |
|
21 |
|
22 |
|
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 |
|
34 |
|
35 |
|
36 |
|
37 | setupOnce(_, getCurrentHub) {
|
38 | this._getCurrentHub = getCurrentHub;
|
39 | this._wrapFetch();
|
40 | this._wrapXHR();
|
41 | }
|
42 |
|
43 | |
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
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 |
|
99 |
|
100 |
|
101 |
|
102 |
|
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 |
|
135 | responseHeaders,
|
136 | responseCookies,
|
137 | });
|
138 |
|
139 | hub.captureEvent(event);
|
140 | }
|
141 | }
|
142 |
|
143 | |
144 |
|
145 |
|
146 |
|
147 |
|
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 |
|
163 |
|
164 |
|
165 |
|
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 |
|
177 |
|
178 |
|
179 |
|
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 |
|
193 |
|
194 |
|
195 |
|
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 |
|
213 |
|
214 |
|
215 |
|
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 |
|
233 |
|
234 |
|
235 |
|
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 |
|
253 |
|
254 | _wrapFetch() {
|
255 | if (!supportsNativeFetch()) {
|
256 | return;
|
257 | }
|
258 |
|
259 |
|
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 |
|
283 |
|
284 | _wrapXHR() {
|
285 | if (!('XMLHttpRequest' in GLOBAL_OBJ)) {
|
286 | return;
|
287 | }
|
288 |
|
289 |
|
290 | const self = this;
|
291 |
|
292 | fill(XMLHttpRequest.prototype, 'open', function (originalOpen) {
|
293 | return function ( ...openArgs) {
|
294 |
|
295 | const xhr = this;
|
296 | const method = openArgs[0] ;
|
297 | const headers = {};
|
298 |
|
299 |
|
300 |
|
301 |
|
302 | fill(
|
303 | xhr,
|
304 | 'setRequestHeader',
|
305 |
|
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 |
|
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 |
|
339 |
|
340 |
|
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 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | _shouldCaptureResponse(status, url) {
|
360 | return this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !this._isSentryRequest(url);
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 |
|
367 |
|
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 |
|
408 | export { HttpClient };
|
409 |
|