
package.build.cjs.utils-hoist.requestdata.js Maven / Gradle / Ivy
Object.defineProperty(exports, '__esModule', { value: true });
const cookie = require('./cookie.js');
const debugBuild = require('./debug-build.js');
const is = require('./is.js');
const logger = require('./logger.js');
const normalize = require('./normalize.js');
const object = require('./object.js');
const string = require('./string.js');
const url = require('./url.js');
const getIpAddress = require('./vendor/getIpAddress.js');
const DEFAULT_INCLUDES = {
ip: false,
request: true,
user: true,
};
const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url'];
const DEFAULT_USER_INCLUDES = ['id', 'username', 'email'];
/**
* Options deciding what parts of the request to use when enhancing an event
*/
/**
* Extracts a complete and parameterized path from the request object and uses it to construct transaction name.
* If the parameterized transaction name cannot be extracted, we fall back to the raw URL.
*
* Additionally, this function determines and returns the transaction name source
*
* eg. GET /mountpoint/user/:id
*
* @param req A request object
* @param options What to include in the transaction name (method, path, or a custom route name to be
* used instead of the request's route)
*
* @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url')
* @deprecated This method will be removed in v9. It is not in use anymore.
*/
function extractPathForTransaction(
req,
options = {},
) {
const method = req.method && req.method.toUpperCase();
let path = '';
let source = 'url';
// Check to see if there's a parameterized route we can use (as there is in Express)
if (options.customRoute || req.route) {
path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`;
source = 'route';
}
// Otherwise, just take the original URL
else if (req.originalUrl || req.url) {
path = url.stripUrlQueryAndFragment(req.originalUrl || req.url || '');
}
let name = '';
if (options.method && method) {
name += method;
}
if (options.method && options.path) {
name += ' ';
}
if (options.path && path) {
name += path;
}
return [name, source];
}
function extractUserData(
user
,
keys,
) {
const extractedUser = {};
const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES;
attributes.forEach(key => {
if (user && key in user) {
extractedUser[key] = user[key];
}
});
return extractedUser;
}
/**
* Normalize data from the request object, accounting for framework differences.
*
* @param req The request object from which to extract data
* @param options.include An optional array of keys to include in the normalized data. Defaults to
* DEFAULT_REQUEST_INCLUDES if not provided.
* @param options.deps Injected, platform-specific dependencies
* @returns An object containing normalized request data
*
* @deprecated Instead manually normalize the request data into a format that fits `addNormalizedRequestDataToEvent`.
*/
function extractRequestData(
req,
options
= {},
) {
const { include = DEFAULT_REQUEST_INCLUDES } = options;
const requestData = {};
// headers:
// node, express, koa, nextjs: req.headers
const headers = (req.headers || {})
;
// method:
// node, express, koa, nextjs: req.method
const method = req.method;
// host:
// express: req.hostname in > 4 and req.host in < 4
// koa: req.host
// node, nextjs: req.headers.host
// Express 4 mistakenly strips off port number from req.host / req.hostname so we can't rely on them
// See: https://github.com/expressjs/express/issues/3047#issuecomment-236653223
// Also: https://github.com/getsentry/sentry-javascript/issues/1917
const host = headers.host || req.hostname || req.host || '';
// protocol:
// node, nextjs:
// express, koa: req.protocol
const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http';
// url (including path and query string):
// node, express: req.originalUrl
// koa, nextjs: req.url
const originalUrl = req.originalUrl || req.url || '';
// absolute url
const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;
include.forEach(key => {
switch (key) {
case 'headers': {
requestData.headers = headers;
// Remove the Cookie header in case cookie data should not be included in the event
if (!include.includes('cookies')) {
delete (requestData.headers ).cookie;
}
// Remove IP headers in case IP data should not be included in the event
if (!include.includes('ip')) {
getIpAddress.ipHeaderNames.forEach(ipHeaderName => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (requestData.headers )[ipHeaderName];
});
}
break;
}
case 'method': {
requestData.method = method;
break;
}
case 'url': {
requestData.url = absoluteUrl;
break;
}
case 'cookies': {
// cookies:
// node, express, koa: req.headers.cookie
// vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies
requestData.cookies =
// TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can
// come off in v8
req.cookies || (headers.cookie && cookie.parseCookie(headers.cookie)) || {};
break;
}
case 'query_string': {
// query string:
// node: req.url (raw)
// express, koa, nextjs: req.query
requestData.query_string = extractQueryParams(req);
break;
}
case 'data': {
if (method === 'GET' || method === 'HEAD') {
break;
}
// NOTE: As of v8, request is (unless a user sets this manually) ALWAYS a http request
// Which does not have a body by default
// However, in our http instrumentation, we patch the request to capture the body and store it on the
// request as `.body` anyhow
// In v9, we may update requestData to only work with plain http requests
// body data:
// express, koa, nextjs: req.body
//
// when using node by itself, you have to read the incoming stream(see
// https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
// where they're going to store the final result, so they'll have to capture this data themselves
const body = req.body;
if (body !== undefined) {
const stringBody = is.isString(body)
? body
: is.isPlainObject(body)
? JSON.stringify(normalize.normalize(body))
: string.truncate(`${body}`, 1024);
if (stringBody) {
requestData.data = stringBody;
}
}
break;
}
default: {
if ({}.hasOwnProperty.call(req, key)) {
requestData[key] = (req )[key];
}
}
}
});
return requestData;
}
/**
* Add already normalized request data to an event.
* This mutates the passed in event.
*/
function addNormalizedRequestDataToEvent(
event,
req,
// This is non-standard data that is not part of the regular HTTP request
additionalData,
options,
) {
const include = {
...DEFAULT_INCLUDES,
...(options && options.include),
};
if (include.request) {
const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES];
if (include.ip) {
includeRequest.push('ip');
}
const extractedRequestData = extractNormalizedRequestData(req, { include: includeRequest });
event.request = {
...event.request,
...extractedRequestData,
};
}
if (include.user) {
const extractedUser =
additionalData.user && is.isPlainObject(additionalData.user)
? extractUserData(additionalData.user, include.user)
: {};
if (Object.keys(extractedUser).length) {
event.user = {
...event.user,
...extractedUser,
};
}
}
if (include.ip) {
const ip = (req.headers && getIpAddress.getClientIPAddress(req.headers)) || additionalData.ipAddress;
if (ip) {
event.user = {
...event.user,
ip_address: ip,
};
}
}
}
/**
* Add data from the given request to the given event
*
* @param event The event to which the request data will be added
* @param req Request object
* @param options.include Flags to control what data is included
* @param options.deps Injected platform-specific dependencies
* @returns The mutated `Event` object
*
* @deprecated Use `addNormalizedRequestDataToEvent` instead.
*/
function addRequestDataToEvent(
event,
req,
options,
) {
const include = {
...DEFAULT_INCLUDES,
...(options && options.include),
};
if (include.request) {
const includeRequest = Array.isArray(include.request) ? [...include.request] : [...DEFAULT_REQUEST_INCLUDES];
if (include.ip) {
includeRequest.push('ip');
}
// eslint-disable-next-line deprecation/deprecation
const extractedRequestData = extractRequestData(req, { include: includeRequest });
event.request = {
...event.request,
...extractedRequestData,
};
}
if (include.user) {
const extractedUser = req.user && is.isPlainObject(req.user) ? extractUserData(req.user, include.user) : {};
if (Object.keys(extractedUser).length) {
event.user = {
...event.user,
...extractedUser,
};
}
}
// client ip:
// node, nextjs: req.socket.remoteAddress
// express, koa: req.ip
// It may also be sent by proxies as specified in X-Forwarded-For or similar headers
if (include.ip) {
const ip = (req.headers && getIpAddress.getClientIPAddress(req.headers)) || req.ip || (req.socket && req.socket.remoteAddress);
if (ip) {
event.user = {
...event.user,
ip_address: ip,
};
}
}
return event;
}
function extractQueryParams(req) {
// url (including path and query string):
// node, express: req.originalUrl
// koa, nextjs: req.url
let originalUrl = req.originalUrl || req.url || '';
if (!originalUrl) {
return;
}
// The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
// hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use.
if (originalUrl.startsWith('/')) {
originalUrl = `http://dogs.are.great${originalUrl}`;
}
try {
const queryParams = req.query || new URL(originalUrl).search.slice(1);
return queryParams.length ? queryParams : undefined;
} catch (e2) {
return undefined;
}
}
/**
* Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict.
* The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type".
*/
// TODO(v8): Make this function return undefined when the extraction fails.
function winterCGHeadersToDict(winterCGHeaders) {
const headers = {};
try {
winterCGHeaders.forEach((value, key) => {
if (typeof value === 'string') {
// We check that value is a string even though it might be redundant to make sure prototype pollution is not possible.
headers[key] = value;
}
});
} catch (e) {
debugBuild.DEBUG_BUILD &&
logger.logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.');
}
return headers;
}
/**
* Convert common request headers to a simple dictionary.
*/
function headersToDict(reqHeaders) {
const headers = Object.create(null);
try {
Object.entries(reqHeaders).forEach(([key, value]) => {
if (typeof value === 'string') {
headers[key] = value;
}
});
} catch (e) {
debugBuild.DEBUG_BUILD &&
logger.logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.');
}
return headers;
}
/**
* Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands.
*/
function winterCGRequestToRequestData(req) {
const headers = winterCGHeadersToDict(req.headers);
return {
method: req.method,
url: req.url,
query_string: extractQueryParamsFromUrl(req.url),
headers,
// TODO: Can we extract body data from the request?
};
}
/**
* Convert a HTTP request object to RequestEventData to be passed as normalizedRequest.
* Instead of allowing `PolymorphicRequest` to be passed,
* we want to be more specific and generally require a http.IncomingMessage-like object.
*/
function httpRequestToRequestData(request
) {
const headers = request.headers || {};
const host = headers.host || '';
const protocol = request.socket && (request.socket ).encrypted ? 'https' : 'http';
const originalUrl = request.url || '';
const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;
// This is non-standard, but may be sometimes set
// It may be overwritten later by our own body handling
const data = (request ).body || undefined;
// This is non-standard, but may be set on e.g. Next.js or Express requests
const cookies = (request ).cookies;
return object.dropUndefinedKeys({
url: absoluteUrl,
method: request.method,
query_string: extractQueryParamsFromUrl(originalUrl),
headers: headersToDict(headers),
cookies,
data,
});
}
/** Extract the query params from an URL. */
function extractQueryParamsFromUrl(url) {
// url is path and query string
if (!url) {
return;
}
try {
// The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
// hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use.
const queryParams = new URL(url, 'http://dogs.are.great').search.slice(1);
return queryParams.length ? queryParams : undefined;
} catch (e3) {
return undefined;
}
}
function extractNormalizedRequestData(
normalizedRequest,
{ include },
) {
const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : [];
const requestData = {};
const headers = { ...normalizedRequest.headers };
if (includeKeys.includes('headers')) {
requestData.headers = headers;
// Remove the Cookie header in case cookie data should not be included in the event
if (!include.includes('cookies')) {
delete (headers ).cookie;
}
// Remove IP headers in case IP data should not be included in the event
if (!include.includes('ip')) {
getIpAddress.ipHeaderNames.forEach(ipHeaderName => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (headers )[ipHeaderName];
});
}
}
if (includeKeys.includes('method')) {
requestData.method = normalizedRequest.method;
}
if (includeKeys.includes('url')) {
requestData.url = normalizedRequest.url;
}
if (includeKeys.includes('cookies')) {
const cookies = normalizedRequest.cookies || (headers && headers.cookie ? cookie.parseCookie(headers.cookie) : undefined);
requestData.cookies = cookies || {};
}
if (includeKeys.includes('query_string')) {
requestData.query_string = normalizedRequest.query_string;
}
if (includeKeys.includes('data')) {
requestData.data = normalizedRequest.data;
}
return requestData;
}
exports.DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES;
exports.addNormalizedRequestDataToEvent = addNormalizedRequestDataToEvent;
exports.addRequestDataToEvent = addRequestDataToEvent;
exports.extractPathForTransaction = extractPathForTransaction;
exports.extractQueryParamsFromUrl = extractQueryParamsFromUrl;
exports.extractRequestData = extractRequestData;
exports.headersToDict = headersToDict;
exports.httpRequestToRequestData = httpRequestToRequestData;
exports.winterCGHeadersToDict = winterCGHeadersToDict;
exports.winterCGRequestToRequestData = winterCGRequestToRequestData;
//# sourceMappingURL=requestdata.js.map
© 2015 - 2025 Weber Informatics LLC | Privacy Policy