@sentry/utils
Version:
Utilities for all Sentry JavaScript SDKs
303 lines (272 loc) • 8.65 kB
JavaScript
import { _optionalChain } from './buildPolyfills';
import { isString, isPlainObject } from './is.js';
import { stripUrlQueryAndFragment } from './misc.js';
import { normalize } from './normalize.js';
var DEFAULT_INCLUDES = {
ip: false,
request: true,
transaction: true,
user: true,
};
var DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url'];
var DEFAULT_USER_INCLUDES = ['id', 'username', 'email'];
/**
* Sets parameterized route as transaction name e.g.: `GET /users/:id`
* Also adds more context data on the transaction from the request
*/
function addRequestDataToTransaction(
transaction,
req,
deps,
) {
if (!transaction) return;
transaction.name = extractPathForTransaction(req, { path: true, method: true });
transaction.setData('url', req.originalUrl || req.url);
if (req.baseUrl) {
transaction.setData('baseUrl', req.baseUrl);
}
transaction.setData('query', extractQueryParams(req, deps));
}
/**
* Extracts complete generalized path from the request object and uses it to construct transaction name.
*
* eg. GET /mountpoint/user/:id
*
* @param req A request object
* @param options What to include in the transaction name (method, path, or both)
*
* @returns The fully constructed transaction name
*/
function extractPathForTransaction(
req,
options = {},
) {
var method = req.method && req.method.toUpperCase();
let path = '';
// Check to see if there's a parameterized route we can use (as there is in Express)
if (req.route) {
path = `${req.baseUrl || ''}${req.route.path}`;
}
// Otherwise, just take the original URL
else if (req.originalUrl || req.url) {
path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
}
let info = '';
if (options.method && method) {
info += method;
}
if (options.method && options.path) {
info += ' ';
}
if (options.path && path) {
info += path;
}
return info;
}
/** JSDoc */
function extractTransaction(req, type) {
switch (type) {
case 'path': {
return extractPathForTransaction(req, { path: true });
}
case 'handler': {
return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || '<anonymous>';
}
case 'methodPath':
default: {
return extractPathForTransaction(req, { path: true, method: true });
}
}
}
/** JSDoc */
function extractUserData(
user
,
keys,
) {
var extractedUser = {};
var 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
*/
function extractRequestData(
req,
options
,
) {
const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {};
var requestData = {};
// headers:
// node, express, koa, nextjs: req.headers
var headers = (req.headers || {})
;
// method:
// node, express, koa, nextjs: req.method
var method = req.method;
// host:
// express: req.hostname in > 4 and req.host in < 4
// koa: req.host
// node, nextjs: req.headers.host
var host = req.hostname || req.host || headers.host || '<no host>';
// protocol:
// node, nextjs: <n/a>
// express, koa: req.protocol
var 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
var originalUrl = req.originalUrl || req.url || '';
// absolute url
var absoluteUrl = `${protocol}://${host}${originalUrl}`;
include.forEach(key => {
switch (key) {
case 'headers': {
requestData.headers = headers;
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 && deps && deps.cookie && deps.cookie.parse(headers.cookie)) || {};
break;
}
case 'query_string': {
// query string:
// node: req.url (raw)
// express, koa, nextjs: req.query
requestData.query_string = extractQueryParams(req, deps);
break;
}
case 'data': {
if (method === 'GET' || method === 'HEAD') {
break;
}
// 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
if (req.body !== undefined) {
requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
}
break;
}
default: {
if ({}.hasOwnProperty.call(req, key)) {
requestData[key] = (req )[key];
}
}
}
});
return requestData;
}
/**
* Options deciding what parts of the request to use when enhancing an event
*/
/**
* 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
* @hidden
*/
function addRequestDataToEvent(
event,
req,
options,
) {
var include = {
...DEFAULT_INCLUDES,
..._optionalChain([options, 'optionalAccess', _ => _.include]),
};
if (include.request) {
var extractedRequestData = Array.isArray(include.request)
? extractRequestData(req, { include: include.request, deps: _optionalChain([options, 'optionalAccess', _2 => _2.deps]) })
: extractRequestData(req, { deps: _optionalChain([options, 'optionalAccess', _3 => _3.deps]) });
event.request = {
...event.request,
...extractedRequestData,
};
}
if (include.user) {
var extractedUser = req.user && 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
if (include.ip) {
var ip = req.ip || (req.socket && req.socket.remoteAddress);
if (ip) {
event.user = {
...event.user,
ip_address: ip,
};
}
}
if (include.transaction && !event.transaction) {
// TODO do we even need this anymore?
// TODO make this work for nextjs
event.transaction = extractTransaction(req, include.transaction);
}
return event;
}
function extractQueryParams(
req,
deps,
) {
// 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}`;
}
return (
req.query ||
(typeof URL !== undefined && new URL(originalUrl).search.replace('?', '')) ||
// In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node
(deps && deps.url && deps.url.parse(originalUrl).query) ||
undefined
);
}
export { addRequestDataToEvent, addRequestDataToTransaction, extractPathForTransaction, extractRequestData };
//# sourceMappingURL=requestdata.js.map