requestdata.js 12 KB

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