requestdata.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import { parseCookie } from './cookie.js';
  2. import { DEBUG_BUILD } from './debug-build.js';
  3. import { isString, isPlainObject } from './is.js';
  4. import { logger } from './logger.js';
  5. import { normalize } from './normalize.js';
  6. import { stripUrlQueryAndFragment } from './url.js';
  7. const DEFAULT_INCLUDES = {
  8. ip: false,
  9. request: true,
  10. transaction: true,
  11. user: true,
  12. };
  13. const DEFAULT_REQUEST_INCLUDES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url'];
  14. const DEFAULT_USER_INCLUDES = ['id', 'username', 'email'];
  15. /**
  16. * Sets parameterized route as transaction name e.g.: `GET /users/:id`
  17. * Also adds more context data on the transaction from the request
  18. */
  19. function addRequestDataToTransaction(
  20. transaction,
  21. req,
  22. deps,
  23. ) {
  24. if (!transaction) return;
  25. // eslint-disable-next-line deprecation/deprecation
  26. if (!transaction.metadata.source || transaction.metadata.source === 'url') {
  27. // Attempt to grab a parameterized route off of the request
  28. const [name, source] = extractPathForTransaction(req, { path: true, method: true });
  29. transaction.updateName(name);
  30. // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core
  31. // eslint-disable-next-line deprecation/deprecation
  32. transaction.setMetadata({ source });
  33. }
  34. transaction.setAttribute('url', req.originalUrl || req.url);
  35. if (req.baseUrl) {
  36. transaction.setAttribute('baseUrl', req.baseUrl);
  37. }
  38. // TODO: We need to rewrite this to a flat format?
  39. // eslint-disable-next-line deprecation/deprecation
  40. transaction.setData('query', extractQueryParams(req, deps));
  41. }
  42. /**
  43. * Extracts a complete and parameterized path from the request object and uses it to construct transaction name.
  44. * If the parameterized transaction name cannot be extracted, we fall back to the raw URL.
  45. *
  46. * Additionally, this function determines and returns the transaction name source
  47. *
  48. * eg. GET /mountpoint/user/:id
  49. *
  50. * @param req A request object
  51. * @param options What to include in the transaction name (method, path, or a custom route name to be
  52. * used instead of the request's route)
  53. *
  54. * @returns A tuple of the fully constructed transaction name [0] and its source [1] (can be either 'route' or 'url')
  55. */
  56. function extractPathForTransaction(
  57. req,
  58. options = {},
  59. ) {
  60. const method = req.method && req.method.toUpperCase();
  61. let path = '';
  62. let source = 'url';
  63. // Check to see if there's a parameterized route we can use (as there is in Express)
  64. if (options.customRoute || req.route) {
  65. path = options.customRoute || `${req.baseUrl || ''}${req.route && req.route.path}`;
  66. source = 'route';
  67. }
  68. // Otherwise, just take the original URL
  69. else if (req.originalUrl || req.url) {
  70. path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
  71. }
  72. let name = '';
  73. if (options.method && method) {
  74. name += method;
  75. }
  76. if (options.method && options.path) {
  77. name += ' ';
  78. }
  79. if (options.path && path) {
  80. name += path;
  81. }
  82. return [name, source];
  83. }
  84. /** JSDoc */
  85. function extractTransaction(req, type) {
  86. switch (type) {
  87. case 'path': {
  88. return extractPathForTransaction(req, { path: true })[0];
  89. }
  90. case 'handler': {
  91. return (req.route && req.route.stack && req.route.stack[0] && req.route.stack[0].name) || '<anonymous>';
  92. }
  93. case 'methodPath':
  94. default: {
  95. // if exist _reconstructedRoute return that path instead of route.path
  96. const customRoute = req._reconstructedRoute ? req._reconstructedRoute : undefined;
  97. return extractPathForTransaction(req, { path: true, method: true, customRoute })[0];
  98. }
  99. }
  100. }
  101. /** JSDoc */
  102. function extractUserData(
  103. user
  104. ,
  105. keys,
  106. ) {
  107. const extractedUser = {};
  108. const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_INCLUDES;
  109. attributes.forEach(key => {
  110. if (user && key in user) {
  111. extractedUser[key] = user[key];
  112. }
  113. });
  114. return extractedUser;
  115. }
  116. /**
  117. * Normalize data from the request object, accounting for framework differences.
  118. *
  119. * @param req The request object from which to extract data
  120. * @param options.include An optional array of keys to include in the normalized data. Defaults to
  121. * DEFAULT_REQUEST_INCLUDES if not provided.
  122. * @param options.deps Injected, platform-specific dependencies
  123. * @returns An object containing normalized request data
  124. */
  125. function extractRequestData(
  126. req,
  127. options
  128. ,
  129. ) {
  130. const { include = DEFAULT_REQUEST_INCLUDES, deps } = options || {};
  131. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  132. const requestData = {};
  133. // headers:
  134. // node, express, koa, nextjs: req.headers
  135. const headers = (req.headers || {})
  136. ;
  137. // method:
  138. // node, express, koa, nextjs: req.method
  139. const method = req.method;
  140. // host:
  141. // express: req.hostname in > 4 and req.host in < 4
  142. // koa: req.host
  143. // node, nextjs: req.headers.host
  144. // Express 4 mistakenly strips off port number from req.host / req.hostname so we can't rely on them
  145. // See: https://github.com/expressjs/express/issues/3047#issuecomment-236653223
  146. // Also: https://github.com/getsentry/sentry-javascript/issues/1917
  147. const host = headers.host || req.hostname || req.host || '<no host>';
  148. // protocol:
  149. // node, nextjs: <n/a>
  150. // express, koa: req.protocol
  151. const protocol = req.protocol === 'https' || (req.socket && req.socket.encrypted) ? 'https' : 'http';
  152. // url (including path and query string):
  153. // node, express: req.originalUrl
  154. // koa, nextjs: req.url
  155. const originalUrl = req.originalUrl || req.url || '';
  156. // absolute url
  157. const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;
  158. include.forEach(key => {
  159. switch (key) {
  160. case 'headers': {
  161. requestData.headers = headers;
  162. // Remove the Cookie header in case cookie data should not be included in the event
  163. if (!include.includes('cookies')) {
  164. delete (requestData.headers ).cookie;
  165. }
  166. break;
  167. }
  168. case 'method': {
  169. requestData.method = method;
  170. break;
  171. }
  172. case 'url': {
  173. requestData.url = absoluteUrl;
  174. break;
  175. }
  176. case 'cookies': {
  177. // cookies:
  178. // node, express, koa: req.headers.cookie
  179. // vercel, sails.js, express (w/ cookie middleware), nextjs: req.cookies
  180. requestData.cookies =
  181. // TODO (v8 / #5257): We're only sending the empty object for backwards compatibility, so the last bit can
  182. // come off in v8
  183. req.cookies || (headers.cookie && parseCookie(headers.cookie)) || {};
  184. break;
  185. }
  186. case 'query_string': {
  187. // query string:
  188. // node: req.url (raw)
  189. // express, koa, nextjs: req.query
  190. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  191. requestData.query_string = extractQueryParams(req, deps);
  192. break;
  193. }
  194. case 'data': {
  195. if (method === 'GET' || method === 'HEAD') {
  196. break;
  197. }
  198. // body data:
  199. // express, koa, nextjs: req.body
  200. //
  201. // when using node by itself, you have to read the incoming stream(see
  202. // https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
  203. // where they're going to store the final result, so they'll have to capture this data themselves
  204. if (req.body !== undefined) {
  205. requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
  206. }
  207. break;
  208. }
  209. default: {
  210. if ({}.hasOwnProperty.call(req, key)) {
  211. requestData[key] = (req )[key];
  212. }
  213. }
  214. }
  215. });
  216. return requestData;
  217. }
  218. /**
  219. * Add data from the given request to the given event
  220. *
  221. * @param event The event to which the request data will be added
  222. * @param req Request object
  223. * @param options.include Flags to control what data is included
  224. * @param options.deps Injected platform-specific dependencies
  225. * @returns The mutated `Event` object
  226. */
  227. function addRequestDataToEvent(
  228. event,
  229. req,
  230. options,
  231. ) {
  232. const include = {
  233. ...DEFAULT_INCLUDES,
  234. ...(options && options.include),
  235. };
  236. if (include.request) {
  237. const extractedRequestData = Array.isArray(include.request)
  238. ? extractRequestData(req, { include: include.request, deps: options && options.deps })
  239. : extractRequestData(req, { deps: options && options.deps });
  240. event.request = {
  241. ...event.request,
  242. ...extractedRequestData,
  243. };
  244. }
  245. if (include.user) {
  246. const extractedUser = req.user && isPlainObject(req.user) ? extractUserData(req.user, include.user) : {};
  247. if (Object.keys(extractedUser).length) {
  248. event.user = {
  249. ...event.user,
  250. ...extractedUser,
  251. };
  252. }
  253. }
  254. // client ip:
  255. // node, nextjs: req.socket.remoteAddress
  256. // express, koa: req.ip
  257. if (include.ip) {
  258. const ip = req.ip || (req.socket && req.socket.remoteAddress);
  259. if (ip) {
  260. event.user = {
  261. ...event.user,
  262. ip_address: ip,
  263. };
  264. }
  265. }
  266. if (include.transaction && !event.transaction) {
  267. // TODO do we even need this anymore?
  268. // TODO make this work for nextjs
  269. event.transaction = extractTransaction(req, include.transaction);
  270. }
  271. return event;
  272. }
  273. function extractQueryParams(
  274. req,
  275. deps,
  276. ) {
  277. // url (including path and query string):
  278. // node, express: req.originalUrl
  279. // koa, nextjs: req.url
  280. let originalUrl = req.originalUrl || req.url || '';
  281. if (!originalUrl) {
  282. return;
  283. }
  284. // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and
  285. // hostname on the beginning. Since the point here is just to grab the query string, it doesn't matter what we use.
  286. if (originalUrl.startsWith('/')) {
  287. originalUrl = `http://dogs.are.great${originalUrl}`;
  288. }
  289. try {
  290. return (
  291. req.query ||
  292. (typeof URL !== 'undefined' && new URL(originalUrl).search.slice(1)) ||
  293. // In Node 8, `URL` isn't in the global scope, so we have to use the built-in module from Node
  294. (deps && deps.url && deps.url.parse(originalUrl).query) ||
  295. undefined
  296. );
  297. } catch (e2) {
  298. return undefined;
  299. }
  300. }
  301. /**
  302. * 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.
  303. * The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type".
  304. */
  305. // TODO(v8): Make this function return undefined when the extraction fails.
  306. function winterCGHeadersToDict(winterCGHeaders) {
  307. const headers = {};
  308. try {
  309. winterCGHeaders.forEach((value, key) => {
  310. if (typeof value === 'string') {
  311. // We check that value is a string even though it might be redundant to make sure prototype pollution is not possible.
  312. headers[key] = value;
  313. }
  314. });
  315. } catch (e) {
  316. DEBUG_BUILD &&
  317. logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.');
  318. }
  319. return headers;
  320. }
  321. /**
  322. * 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.
  323. */
  324. function winterCGRequestToRequestData(req) {
  325. const headers = winterCGHeadersToDict(req.headers);
  326. return {
  327. method: req.method,
  328. url: req.url,
  329. headers,
  330. };
  331. }
  332. export { DEFAULT_USER_INCLUDES, addRequestDataToEvent, addRequestDataToTransaction, extractPathForTransaction, extractRequestData, winterCGHeadersToDict, winterCGRequestToRequestData };
  333. //# sourceMappingURL=requestdata.js.map