context.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. var {
  2. _optionalChain
  3. } = require('@sentry/utils');
  4. Object.defineProperty(exports, '__esModule', { value: true });
  5. const child_process = require('child_process');
  6. const fs = require('fs');
  7. const os = require('os');
  8. const path = require('path');
  9. const util = require('util');
  10. const core = require('@sentry/core');
  11. /* eslint-disable max-lines */
  12. // TODO: Required until we drop support for Node v8
  13. const readFileAsync = util.promisify(fs.readFile);
  14. const readDirAsync = util.promisify(fs.readdir);
  15. const INTEGRATION_NAME = 'Context';
  16. const _nodeContextIntegration = ((options = {}) => {
  17. let cachedContext;
  18. const _options = {
  19. app: true,
  20. os: true,
  21. device: true,
  22. culture: true,
  23. cloudResource: true,
  24. ...options,
  25. };
  26. /** Add contexts to the event. Caches the context so we only look it up once. */
  27. async function addContext(event) {
  28. if (cachedContext === undefined) {
  29. cachedContext = _getContexts();
  30. }
  31. const updatedContext = _updateContext(await cachedContext);
  32. event.contexts = {
  33. ...event.contexts,
  34. app: { ...updatedContext.app, ..._optionalChain([event, 'access', _ => _.contexts, 'optionalAccess', _2 => _2.app]) },
  35. os: { ...updatedContext.os, ..._optionalChain([event, 'access', _3 => _3.contexts, 'optionalAccess', _4 => _4.os]) },
  36. device: { ...updatedContext.device, ..._optionalChain([event, 'access', _5 => _5.contexts, 'optionalAccess', _6 => _6.device]) },
  37. culture: { ...updatedContext.culture, ..._optionalChain([event, 'access', _7 => _7.contexts, 'optionalAccess', _8 => _8.culture]) },
  38. cloud_resource: { ...updatedContext.cloud_resource, ..._optionalChain([event, 'access', _9 => _9.contexts, 'optionalAccess', _10 => _10.cloud_resource]) },
  39. };
  40. return event;
  41. }
  42. /** Get the contexts from node. */
  43. async function _getContexts() {
  44. const contexts = {};
  45. if (_options.os) {
  46. contexts.os = await getOsContext();
  47. }
  48. if (_options.app) {
  49. contexts.app = getAppContext();
  50. }
  51. if (_options.device) {
  52. contexts.device = getDeviceContext(_options.device);
  53. }
  54. if (_options.culture) {
  55. const culture = getCultureContext();
  56. if (culture) {
  57. contexts.culture = culture;
  58. }
  59. }
  60. if (_options.cloudResource) {
  61. contexts.cloud_resource = getCloudResourceContext();
  62. }
  63. return contexts;
  64. }
  65. return {
  66. name: INTEGRATION_NAME,
  67. // TODO v8: Remove this
  68. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  69. processEvent(event) {
  70. return addContext(event);
  71. },
  72. };
  73. }) ;
  74. const nodeContextIntegration = core.defineIntegration(_nodeContextIntegration);
  75. /**
  76. * Add node modules / packages to the event.
  77. * @deprecated Use `nodeContextIntegration()` instead.
  78. */
  79. // eslint-disable-next-line deprecation/deprecation
  80. const Context = core.convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration)
  81. ;
  82. // eslint-disable-next-line deprecation/deprecation
  83. /**
  84. * Updates the context with dynamic values that can change
  85. */
  86. function _updateContext(contexts) {
  87. // Only update properties if they exist
  88. if (_optionalChain([contexts, 'optionalAccess', _11 => _11.app, 'optionalAccess', _12 => _12.app_memory])) {
  89. contexts.app.app_memory = process.memoryUsage().rss;
  90. }
  91. if (_optionalChain([contexts, 'optionalAccess', _13 => _13.device, 'optionalAccess', _14 => _14.free_memory])) {
  92. contexts.device.free_memory = os.freemem();
  93. }
  94. return contexts;
  95. }
  96. /**
  97. * Returns the operating system context.
  98. *
  99. * Based on the current platform, this uses a different strategy to provide the
  100. * most accurate OS information. Since this might involve spawning subprocesses
  101. * or accessing the file system, this should only be executed lazily and cached.
  102. *
  103. * - On macOS (Darwin), this will execute the `sw_vers` utility. The context
  104. * has a `name`, `version`, `build` and `kernel_version` set.
  105. * - On Linux, this will try to load a distribution release from `/etc` and set
  106. * the `name`, `version` and `kernel_version` fields.
  107. * - On all other platforms, only a `name` and `version` will be returned. Note
  108. * that `version` might actually be the kernel version.
  109. */
  110. async function getOsContext() {
  111. const platformId = os.platform();
  112. switch (platformId) {
  113. case 'darwin':
  114. return getDarwinInfo();
  115. case 'linux':
  116. return getLinuxInfo();
  117. default:
  118. return {
  119. name: PLATFORM_NAMES[platformId] || platformId,
  120. version: os.release(),
  121. };
  122. }
  123. }
  124. function getCultureContext() {
  125. try {
  126. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
  127. if (typeof (process.versions ).icu !== 'string') {
  128. // Node was built without ICU support
  129. return;
  130. }
  131. // Check that node was built with full Intl support. Its possible it was built without support for non-English
  132. // locales which will make resolvedOptions inaccurate
  133. //
  134. // https://nodejs.org/api/intl.html#detecting-internationalization-support
  135. const january = new Date(9e8);
  136. const spanish = new Intl.DateTimeFormat('es', { month: 'long' });
  137. if (spanish.format(january) === 'enero') {
  138. const options = Intl.DateTimeFormat().resolvedOptions();
  139. return {
  140. locale: options.locale,
  141. timezone: options.timeZone,
  142. };
  143. }
  144. } catch (err) {
  145. //
  146. }
  147. return;
  148. }
  149. function getAppContext() {
  150. const app_memory = process.memoryUsage().rss;
  151. const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString();
  152. return { app_start_time, app_memory };
  153. }
  154. /**
  155. * Gets device information from os
  156. */
  157. function getDeviceContext(deviceOpt) {
  158. const device = {};
  159. // Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202
  160. let uptime;
  161. try {
  162. uptime = os.uptime && os.uptime();
  163. } catch (e) {
  164. // noop
  165. }
  166. // os.uptime or its return value seem to be undefined in certain environments (e.g. Azure functions).
  167. // Hence, we only set boot time, if we get a valid uptime value.
  168. // @see https://github.com/getsentry/sentry-javascript/issues/5856
  169. if (typeof uptime === 'number') {
  170. device.boot_time = new Date(Date.now() - uptime * 1000).toISOString();
  171. }
  172. device.arch = os.arch();
  173. if (deviceOpt === true || deviceOpt.memory) {
  174. device.memory_size = os.totalmem();
  175. device.free_memory = os.freemem();
  176. }
  177. if (deviceOpt === true || deviceOpt.cpu) {
  178. const cpuInfo = os.cpus();
  179. if (cpuInfo && cpuInfo.length) {
  180. const firstCpu = cpuInfo[0];
  181. device.processor_count = cpuInfo.length;
  182. device.cpu_description = firstCpu.model;
  183. device.processor_frequency = firstCpu.speed;
  184. }
  185. }
  186. return device;
  187. }
  188. /** Mapping of Node's platform names to actual OS names. */
  189. const PLATFORM_NAMES = {
  190. aix: 'IBM AIX',
  191. freebsd: 'FreeBSD',
  192. openbsd: 'OpenBSD',
  193. sunos: 'SunOS',
  194. win32: 'Windows',
  195. };
  196. /** Linux version file to check for a distribution. */
  197. /** Mapping of linux release files located in /etc to distributions. */
  198. const LINUX_DISTROS = [
  199. { name: 'fedora-release', distros: ['Fedora'] },
  200. { name: 'redhat-release', distros: ['Red Hat Linux', 'Centos'] },
  201. { name: 'redhat_version', distros: ['Red Hat Linux'] },
  202. { name: 'SuSE-release', distros: ['SUSE Linux'] },
  203. { name: 'lsb-release', distros: ['Ubuntu Linux', 'Arch Linux'] },
  204. { name: 'debian_version', distros: ['Debian'] },
  205. { name: 'debian_release', distros: ['Debian'] },
  206. { name: 'arch-release', distros: ['Arch Linux'] },
  207. { name: 'gentoo-release', distros: ['Gentoo Linux'] },
  208. { name: 'novell-release', distros: ['SUSE Linux'] },
  209. { name: 'alpine-release', distros: ['Alpine Linux'] },
  210. ];
  211. /** Functions to extract the OS version from Linux release files. */
  212. const LINUX_VERSIONS
  213. = {
  214. alpine: content => content,
  215. arch: content => matchFirst(/distrib_release=(.*)/, content),
  216. centos: content => matchFirst(/release ([^ ]+)/, content),
  217. debian: content => content,
  218. fedora: content => matchFirst(/release (..)/, content),
  219. mint: content => matchFirst(/distrib_release=(.*)/, content),
  220. red: content => matchFirst(/release ([^ ]+)/, content),
  221. suse: content => matchFirst(/VERSION = (.*)\n/, content),
  222. ubuntu: content => matchFirst(/distrib_release=(.*)/, content),
  223. };
  224. /**
  225. * Executes a regular expression with one capture group.
  226. *
  227. * @param regex A regular expression to execute.
  228. * @param text Content to execute the RegEx on.
  229. * @returns The captured string if matched; otherwise undefined.
  230. */
  231. function matchFirst(regex, text) {
  232. const match = regex.exec(text);
  233. return match ? match[1] : undefined;
  234. }
  235. /** Loads the macOS operating system context. */
  236. async function getDarwinInfo() {
  237. // Default values that will be used in case no operating system information
  238. // can be loaded. The default version is computed via heuristics from the
  239. // kernel version, but the build ID is missing.
  240. const darwinInfo = {
  241. kernel_version: os.release(),
  242. name: 'Mac OS X',
  243. version: `10.${Number(os.release().split('.')[0]) - 4}`,
  244. };
  245. try {
  246. // We try to load the actual macOS version by executing the `sw_vers` tool.
  247. // This tool should be available on every standard macOS installation. In
  248. // case this fails, we stick with the values computed above.
  249. const output = await new Promise((resolve, reject) => {
  250. child_process.execFile('/usr/bin/sw_vers', (error, stdout) => {
  251. if (error) {
  252. reject(error);
  253. return;
  254. }
  255. resolve(stdout);
  256. });
  257. });
  258. darwinInfo.name = matchFirst(/^ProductName:\s+(.*)$/m, output);
  259. darwinInfo.version = matchFirst(/^ProductVersion:\s+(.*)$/m, output);
  260. darwinInfo.build = matchFirst(/^BuildVersion:\s+(.*)$/m, output);
  261. } catch (e) {
  262. // ignore
  263. }
  264. return darwinInfo;
  265. }
  266. /** Returns a distribution identifier to look up version callbacks. */
  267. function getLinuxDistroId(name) {
  268. return name.split(' ')[0].toLowerCase();
  269. }
  270. /** Loads the Linux operating system context. */
  271. async function getLinuxInfo() {
  272. // By default, we cannot assume anything about the distribution or Linux
  273. // version. `os.release()` returns the kernel version and we assume a generic
  274. // "Linux" name, which will be replaced down below.
  275. const linuxInfo = {
  276. kernel_version: os.release(),
  277. name: 'Linux',
  278. };
  279. try {
  280. // We start guessing the distribution by listing files in the /etc
  281. // directory. This is were most Linux distributions (except Knoppix) store
  282. // release files with certain distribution-dependent meta data. We search
  283. // for exactly one known file defined in `LINUX_DISTROS` and exit if none
  284. // are found. In case there are more than one file, we just stick with the
  285. // first one.
  286. const etcFiles = await readDirAsync('/etc');
  287. const distroFile = LINUX_DISTROS.find(file => etcFiles.includes(file.name));
  288. if (!distroFile) {
  289. return linuxInfo;
  290. }
  291. // Once that file is known, load its contents. To make searching in those
  292. // files easier, we lowercase the file contents. Since these files are
  293. // usually quite small, this should not allocate too much memory and we only
  294. // hold on to it for a very short amount of time.
  295. const distroPath = path.join('/etc', distroFile.name);
  296. const contents = ((await readFileAsync(distroPath, { encoding: 'utf-8' })) ).toLowerCase();
  297. // Some Linux distributions store their release information in the same file
  298. // (e.g. RHEL and Centos). In those cases, we scan the file for an
  299. // identifier, that basically consists of the first word of the linux
  300. // distribution name (e.g. "red" for Red Hat). In case there is no match, we
  301. // just assume the first distribution in our list.
  302. const { distros } = distroFile;
  303. linuxInfo.name = distros.find(d => contents.indexOf(getLinuxDistroId(d)) >= 0) || distros[0];
  304. // Based on the found distribution, we can now compute the actual version
  305. // number. This is different for every distribution, so several strategies
  306. // are computed in `LINUX_VERSIONS`.
  307. const id = getLinuxDistroId(linuxInfo.name);
  308. linuxInfo.version = LINUX_VERSIONS[id](contents);
  309. } catch (e) {
  310. // ignore
  311. }
  312. return linuxInfo;
  313. }
  314. /**
  315. * Grabs some information about hosting provider based on best effort.
  316. */
  317. function getCloudResourceContext() {
  318. if (process.env.VERCEL) {
  319. // https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#system-environment-variables
  320. return {
  321. 'cloud.provider': 'vercel',
  322. 'cloud.region': process.env.VERCEL_REGION,
  323. };
  324. } else if (process.env.AWS_REGION) {
  325. // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
  326. return {
  327. 'cloud.provider': 'aws',
  328. 'cloud.region': process.env.AWS_REGION,
  329. 'cloud.platform': process.env.AWS_EXECUTION_ENV,
  330. };
  331. } else if (process.env.GCP_PROJECT) {
  332. // https://cloud.google.com/composer/docs/how-to/managing/environment-variables#reserved_variables
  333. return {
  334. 'cloud.provider': 'gcp',
  335. };
  336. } else if (process.env.ALIYUN_REGION_ID) {
  337. // TODO: find where I found these environment variables - at least gc.github.com returns something
  338. return {
  339. 'cloud.provider': 'alibaba_cloud',
  340. 'cloud.region': process.env.ALIYUN_REGION_ID,
  341. };
  342. } else if (process.env.WEBSITE_SITE_NAME && process.env.REGION_NAME) {
  343. // https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings?tabs=kudu%2Cdotnet#app-environment
  344. return {
  345. 'cloud.provider': 'azure',
  346. 'cloud.region': process.env.REGION_NAME,
  347. };
  348. } else if (process.env.IBM_CLOUD_REGION) {
  349. // TODO: find where I found these environment variables - at least gc.github.com returns something
  350. return {
  351. 'cloud.provider': 'ibm_cloud',
  352. 'cloud.region': process.env.IBM_CLOUD_REGION,
  353. };
  354. } else if (process.env.TENCENTCLOUD_REGION) {
  355. // https://www.tencentcloud.com/document/product/583/32748
  356. return {
  357. 'cloud.provider': 'tencent_cloud',
  358. 'cloud.region': process.env.TENCENTCLOUD_REGION,
  359. 'cloud.account.id': process.env.TENCENTCLOUD_APPID,
  360. 'cloud.availability_zone': process.env.TENCENTCLOUD_ZONE,
  361. };
  362. } else if (process.env.NETLIFY) {
  363. // https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables
  364. return {
  365. 'cloud.provider': 'netlify',
  366. };
  367. } else if (process.env.FLY_REGION) {
  368. // https://fly.io/docs/reference/runtime-environment/
  369. return {
  370. 'cloud.provider': 'fly.io',
  371. 'cloud.region': process.env.FLY_REGION,
  372. };
  373. } else if (process.env.DYNO) {
  374. // https://devcenter.heroku.com/articles/dynos#local-environment-variables
  375. return {
  376. 'cloud.provider': 'heroku',
  377. };
  378. } else {
  379. return undefined;
  380. }
  381. }
  382. exports.Context = Context;
  383. exports.getDeviceContext = getDeviceContext;
  384. exports.nodeContextIntegration = nodeContextIntegration;
  385. exports.readDirAsync = readDirAsync;
  386. exports.readFileAsync = readFileAsync;
  387. //# sourceMappingURL=context.js.map