@sentry/integrations
Version:
Pluggable integrations that can be used to enhance JS SDKs
385 lines (326 loc) • 10 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
const core = require('@sentry/core');
const utils = require('@sentry/utils');
/** HTTPClient integration creates events for failed client side HTTP requests. */
class HttpClient {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'HttpClient';}
/**
* @inheritDoc
*/
/**
* Returns current hub.
*/
/**
* @inheritDoc
*
* @param options
*/
constructor(options) {
this.name = HttpClient.id;
this._options = {
failedRequestStatusCodes: [[500, 599]],
failedRequestTargets: [/.*/],
...options,
};
}
/**
* @inheritDoc
*
* @param options
*/
setupOnce(_, getCurrentHub) {
this._getCurrentHub = getCurrentHub;
this._wrapFetch();
this._wrapXHR();
}
/**
* Interceptor function for fetch requests
*
* @param requestInfo The Fetch API request info
* @param response The Fetch API response
* @param requestInit The request init object
*/
_fetchResponseHandler(requestInfo, response, requestInit) {
if (this._getCurrentHub && this._shouldCaptureResponse(response.status, response.url)) {
const request = _getRequest(requestInfo, requestInit);
const hub = this._getCurrentHub();
let requestHeaders, responseHeaders, requestCookies, responseCookies;
if (hub.shouldSendDefaultPii()) {
[{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] =
[
{ cookieHeader: 'Cookie', obj: request },
{ cookieHeader: 'Set-Cookie', obj: response },
].map(({ cookieHeader, obj }) => {
const headers = this._extractFetchHeaders(obj.headers);
let cookies;
try {
const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined;
if (cookieString) {
cookies = this._parseCookieString(cookieString);
}
} catch (e) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log(`Could not extract cookies from header ${cookieHeader}`);
}
return {
headers,
cookies,
};
});
}
const event = this._createEvent({
url: request.url,
method: request.method,
status: response.status,
requestHeaders,
responseHeaders,
requestCookies,
responseCookies,
});
hub.captureEvent(event);
}
}
/**
* Interceptor function for XHR requests
*
* @param xhr The XHR request
* @param method The HTTP method
* @param headers The HTTP headers
*/
_xhrResponseHandler(xhr, method, headers) {
if (this._getCurrentHub && this._shouldCaptureResponse(xhr.status, xhr.responseURL)) {
let requestHeaders, responseCookies, responseHeaders;
const hub = this._getCurrentHub();
if (hub.shouldSendDefaultPii()) {
try {
const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined;
if (cookieString) {
responseCookies = this._parseCookieString(cookieString);
}
} catch (e) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log('Could not extract cookies from response headers');
}
try {
responseHeaders = this._getXHRResponseHeaders(xhr);
} catch (e) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.logger.log('Could not extract headers from response');
}
requestHeaders = headers;
}
const event = this._createEvent({
url: xhr.responseURL,
method: method,
status: xhr.status,
requestHeaders,
// Can't access request cookies from XHR
responseHeaders,
responseCookies,
});
hub.captureEvent(event);
}
}
/**
* Extracts response size from `Content-Length` header when possible
*
* @param headers
* @returns The response size in bytes or undefined
*/
_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
*/
_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
*/
_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
*/
_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
*/
_isInGivenRequestTargets(target) {
if (!this._options.failedRequestTargets) {
return false;
}
return this._options.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
*/
_isInGivenStatusRanges(status) {
if (!this._options.failedRequestStatusCodes) {
return false;
}
return this._options.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
*/
_wrapFetch() {
if (!utils.supportsNativeFetch()) {
return;
}
utils.addInstrumentationHandler('fetch', (handlerData) => {
const { response, args } = handlerData;
const [requestInfo, requestInit] = args ;
if (!response) {
return;
}
this._fetchResponseHandler(requestInfo, response, requestInit);
});
}
/**
* Wraps XMLHttpRequest to capture request and response data
*/
_wrapXHR() {
if (!('XMLHttpRequest' in utils.GLOBAL_OBJ)) {
return;
}
utils.addInstrumentationHandler(
'xhr',
(handlerData) => {
const { xhr } = handlerData;
const sentryXhrData = xhr[utils.SENTRY_XHR_DATA_KEY];
if (!sentryXhrData) {
return;
}
const { method, request_headers: headers } = sentryXhrData;
if (!method) {
return;
}
try {
this._xhrResponseHandler(xhr, method, headers);
} catch (e) {
(typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && utils.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
*/
_shouldCaptureResponse(status, url) {
return (
this._isInGivenStatusRanges(status) &&
this._isInGivenRequestTargets(url) &&
!core.isSentryRequestUrl(url, core.getCurrentHub())
);
}
/**
* Creates a synthetic Sentry event from given response data
*
* @param data response data
* @returns event
*/
_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: this._getResponseSizeFromHeaders(data.responseHeaders),
},
},
};
utils.addExceptionMechanism(event, {
type: 'http.client',
handled: false,
});
return event;
}
} HttpClient.__initStatic();
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);
}
exports.HttpClient = HttpClient;
//# sourceMappingURL=httpclient.js.map