import { defineIntegration, convertIntegrationFnToClass, getClient, captureEvent, isSentryRequestUrl } from '@sentry/core'; import { supportsNativeFetch, addFetchInstrumentationHandler, GLOBAL_OBJ, addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY, logger, addExceptionMechanism } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build.js'; const INTEGRATION_NAME = 'HttpClient'; const _httpClientIntegration = ((options = {}) => { const _options = { failedRequestStatusCodes: [[500, 599]], failedRequestTargets: [/.*/], ...options, }; return { name: INTEGRATION_NAME, // TODO v8: Remove this setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { _wrapFetch(client, _options); _wrapXHR(client, _options); }, }; }) ; const httpClientIntegration = defineIntegration(_httpClientIntegration); /** * Create events for failed client side HTTP requests. * @deprecated Use `httpClientIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation const HttpClient = convertIntegrationFnToClass(INTEGRATION_NAME, httpClientIntegration) ; /** * Interceptor function for fetch requests * * @param requestInfo The Fetch API request info * @param response The Fetch API response * @param requestInit The request init object */ function _fetchResponseHandler( options, requestInfo, response, requestInit, ) { if (_shouldCaptureResponse(options, response.status, response.url)) { const request = _getRequest(requestInfo, requestInit); let requestHeaders, responseHeaders, requestCookies, responseCookies; if (_shouldSendDefaultPii()) { [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [ { cookieHeader: 'Cookie', obj: request }, { cookieHeader: 'Set-Cookie', obj: response }, ].map(({ cookieHeader, obj }) => { const headers = _extractFetchHeaders(obj.headers); let cookies; try { const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; if (cookieString) { cookies = _parseCookieString(cookieString); } } catch (e) { DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); } return { headers, cookies, }; }); } const event = _createEvent({ url: request.url, method: request.method, status: response.status, requestHeaders, responseHeaders, requestCookies, responseCookies, }); captureEvent(event); } } /** * Interceptor function for XHR requests * * @param xhr The XHR request * @param method The HTTP method * @param headers The HTTP headers */ function _xhrResponseHandler( options, xhr, method, headers, ) { if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) { let requestHeaders, responseCookies, responseHeaders; if (_shouldSendDefaultPii()) { try { const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; if (cookieString) { responseCookies = _parseCookieString(cookieString); } } catch (e) { DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); } try { responseHeaders = _getXHRResponseHeaders(xhr); } catch (e) { DEBUG_BUILD && logger.log('Could not extract headers from response'); } requestHeaders = headers; } const event = _createEvent({ url: xhr.responseURL, method, status: xhr.status, requestHeaders, // Can't access request cookies from XHR responseHeaders, responseCookies, }); captureEvent(event); } } /** * Extracts response size from `Content-Length` header when possible * * @param headers * @returns The response size in bytes or undefined */ function _getResponseSizeFromHeaders(headers) { if (headers) { const contentLength = headers['Content-Length'] || headers['content-length']; if (contentLength) { return parseInt(contentLength, 10); } } return undefined; } /** * Creates an object containing cookies from the given cookie string * * @param cookieString The cookie string to parse * @returns The parsed cookies */ function _parseCookieString(cookieString) { return cookieString.split('; ').reduce((acc, cookie) => { const [key, value] = cookie.split('='); acc[key] = value; return acc; }, {}); } /** * Extracts the headers as an object from the given Fetch API request or response object * * @param headers The headers to extract * @returns The extracted headers as an object */ function _extractFetchHeaders(headers) { const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } /** * Extracts the response headers as an object from the given XHR object * * @param xhr The XHR object to extract the response headers from * @returns The response headers as an object */ function _getXHRResponseHeaders(xhr) { const headers = xhr.getAllResponseHeaders(); if (!headers) { return {}; } return headers.split('\r\n').reduce((acc, line) => { const [key, value] = line.split(': '); acc[key] = value; return acc; }, {}); } /** * Checks if the given target url is in the given list of targets * * @param target The target url to check * @returns true if the target url is in the given list of targets, false otherwise */ function _isInGivenRequestTargets( failedRequestTargets, target, ) { return failedRequestTargets.some((givenRequestTarget) => { if (typeof givenRequestTarget === 'string') { return target.includes(givenRequestTarget); } return givenRequestTarget.test(target); }); } /** * Checks if the given status code is in the given range * * @param status The status code to check * @returns true if the status code is in the given range, false otherwise */ function _isInGivenStatusRanges( failedRequestStatusCodes, status, ) { return failedRequestStatusCodes.some((range) => { if (typeof range === 'number') { return range === status; } return status >= range[0] && status <= range[1]; }); } /** * Wraps `fetch` function to capture request and response data */ function _wrapFetch(client, options) { if (!supportsNativeFetch()) { return; } addFetchInstrumentationHandler(handlerData => { if (getClient() !== client) { return; } const { response, args } = handlerData; const [requestInfo, requestInit] = args ; if (!response) { return; } _fetchResponseHandler(options, requestInfo, response , requestInit); }); } /** * Wraps XMLHttpRequest to capture request and response data */ function _wrapXHR(client, options) { if (!('XMLHttpRequest' in GLOBAL_OBJ)) { return; } addXhrInstrumentationHandler(handlerData => { if (getClient() !== client) { return; } const xhr = handlerData.xhr ; const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; if (!sentryXhrData) { return; } const { method, request_headers: headers } = sentryXhrData; try { _xhrResponseHandler(options, xhr, method, headers); } catch (e) { DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); } }); } /** * Checks whether to capture given response as an event * * @param status response status code * @param url response url */ function _shouldCaptureResponse(options, status, url) { return ( _isInGivenStatusRanges(options.failedRequestStatusCodes, status) && _isInGivenRequestTargets(options.failedRequestTargets, url) && !isSentryRequestUrl(url, getClient()) ); } /** * Creates a synthetic Sentry event from given response data * * @param data response data * @returns event */ function _createEvent(data ) { const message = `HTTP Client Error with status code: ${data.status}`; const event = { message, exception: { values: [ { type: 'Error', value: message, }, ], }, request: { url: data.url, method: data.method, headers: data.requestHeaders, cookies: data.requestCookies, }, contexts: { response: { status_code: data.status, headers: data.responseHeaders, cookies: data.responseCookies, body_size: _getResponseSizeFromHeaders(data.responseHeaders), }, }, }; addExceptionMechanism(event, { type: 'http.client', handled: false, }); return event; } function _getRequest(requestInfo, requestInit) { if (!requestInit && requestInfo instanceof Request) { return requestInfo; } // If both are set, we try to construct a new Request with the given arguments // However, if e.g. the original request has a `body`, this will throw an error because it was already accessed // In this case, as a fallback, we just use the original request - using both is rather an edge case if (requestInfo instanceof Request && requestInfo.bodyUsed) { return requestInfo; } return new Request(requestInfo, requestInit); } function _shouldSendDefaultPii() { const client = getClient(); return client ? Boolean(client.getOptions().sendDefaultPii) : false; } export { HttpClient, httpClientIntegration }; //# sourceMappingURL=httpclient.js.map