450 lines
15 KiB
JavaScript
450 lines
15 KiB
JavaScript
|
var {
|
||
|
_optionalChain
|
||
|
} = require('@sentry/utils');
|
||
|
|
||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
|
||
|
const child_process = require('child_process');
|
||
|
const fs = require('fs');
|
||
|
const os = require('os');
|
||
|
const path = require('path');
|
||
|
const util = require('util');
|
||
|
const core = require('@sentry/core');
|
||
|
|
||
|
/* eslint-disable max-lines */
|
||
|
|
||
|
// TODO: Required until we drop support for Node v8
|
||
|
const readFileAsync = util.promisify(fs.readFile);
|
||
|
const readDirAsync = util.promisify(fs.readdir);
|
||
|
|
||
|
const INTEGRATION_NAME = 'Context';
|
||
|
|
||
|
const _nodeContextIntegration = ((options = {}) => {
|
||
|
let cachedContext;
|
||
|
|
||
|
const _options = {
|
||
|
app: true,
|
||
|
os: true,
|
||
|
device: true,
|
||
|
culture: true,
|
||
|
cloudResource: true,
|
||
|
...options,
|
||
|
};
|
||
|
|
||
|
/** Add contexts to the event. Caches the context so we only look it up once. */
|
||
|
async function addContext(event) {
|
||
|
if (cachedContext === undefined) {
|
||
|
cachedContext = _getContexts();
|
||
|
}
|
||
|
|
||
|
const updatedContext = _updateContext(await cachedContext);
|
||
|
|
||
|
event.contexts = {
|
||
|
...event.contexts,
|
||
|
app: { ...updatedContext.app, ..._optionalChain([event, 'access', _ => _.contexts, 'optionalAccess', _2 => _2.app]) },
|
||
|
os: { ...updatedContext.os, ..._optionalChain([event, 'access', _3 => _3.contexts, 'optionalAccess', _4 => _4.os]) },
|
||
|
device: { ...updatedContext.device, ..._optionalChain([event, 'access', _5 => _5.contexts, 'optionalAccess', _6 => _6.device]) },
|
||
|
culture: { ...updatedContext.culture, ..._optionalChain([event, 'access', _7 => _7.contexts, 'optionalAccess', _8 => _8.culture]) },
|
||
|
cloud_resource: { ...updatedContext.cloud_resource, ..._optionalChain([event, 'access', _9 => _9.contexts, 'optionalAccess', _10 => _10.cloud_resource]) },
|
||
|
};
|
||
|
|
||
|
return event;
|
||
|
}
|
||
|
|
||
|
/** Get the contexts from node. */
|
||
|
async function _getContexts() {
|
||
|
const contexts = {};
|
||
|
|
||
|
if (_options.os) {
|
||
|
contexts.os = await getOsContext();
|
||
|
}
|
||
|
|
||
|
if (_options.app) {
|
||
|
contexts.app = getAppContext();
|
||
|
}
|
||
|
|
||
|
if (_options.device) {
|
||
|
contexts.device = getDeviceContext(_options.device);
|
||
|
}
|
||
|
|
||
|
if (_options.culture) {
|
||
|
const culture = getCultureContext();
|
||
|
|
||
|
if (culture) {
|
||
|
contexts.culture = culture;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (_options.cloudResource) {
|
||
|
contexts.cloud_resource = getCloudResourceContext();
|
||
|
}
|
||
|
|
||
|
return contexts;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
name: INTEGRATION_NAME,
|
||
|
// TODO v8: Remove this
|
||
|
setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
|
||
|
processEvent(event) {
|
||
|
return addContext(event);
|
||
|
},
|
||
|
};
|
||
|
}) ;
|
||
|
|
||
|
const nodeContextIntegration = core.defineIntegration(_nodeContextIntegration);
|
||
|
|
||
|
/**
|
||
|
* Add node modules / packages to the event.
|
||
|
* @deprecated Use `nodeContextIntegration()` instead.
|
||
|
*/
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
const Context = core.convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration)
|
||
|
|
||
|
;
|
||
|
|
||
|
// eslint-disable-next-line deprecation/deprecation
|
||
|
|
||
|
/**
|
||
|
* Updates the context with dynamic values that can change
|
||
|
*/
|
||
|
function _updateContext(contexts) {
|
||
|
// Only update properties if they exist
|
||
|
if (_optionalChain([contexts, 'optionalAccess', _11 => _11.app, 'optionalAccess', _12 => _12.app_memory])) {
|
||
|
contexts.app.app_memory = process.memoryUsage().rss;
|
||
|
}
|
||
|
|
||
|
if (_optionalChain([contexts, 'optionalAccess', _13 => _13.device, 'optionalAccess', _14 => _14.free_memory])) {
|
||
|
contexts.device.free_memory = os.freemem();
|
||
|
}
|
||
|
|
||
|
return contexts;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the operating system context.
|
||
|
*
|
||
|
* Based on the current platform, this uses a different strategy to provide the
|
||
|
* most accurate OS information. Since this might involve spawning subprocesses
|
||
|
* or accessing the file system, this should only be executed lazily and cached.
|
||
|
*
|
||
|
* - On macOS (Darwin), this will execute the `sw_vers` utility. The context
|
||
|
* has a `name`, `version`, `build` and `kernel_version` set.
|
||
|
* - On Linux, this will try to load a distribution release from `/etc` and set
|
||
|
* the `name`, `version` and `kernel_version` fields.
|
||
|
* - On all other platforms, only a `name` and `version` will be returned. Note
|
||
|
* that `version` might actually be the kernel version.
|
||
|
*/
|
||
|
async function getOsContext() {
|
||
|
const platformId = os.platform();
|
||
|
switch (platformId) {
|
||
|
case 'darwin':
|
||
|
return getDarwinInfo();
|
||
|
case 'linux':
|
||
|
return getLinuxInfo();
|
||
|
default:
|
||
|
return {
|
||
|
name: PLATFORM_NAMES[platformId] || platformId,
|
||
|
version: os.release(),
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getCultureContext() {
|
||
|
try {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||
|
if (typeof (process.versions ).icu !== 'string') {
|
||
|
// Node was built without ICU support
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check that node was built with full Intl support. Its possible it was built without support for non-English
|
||
|
// locales which will make resolvedOptions inaccurate
|
||
|
//
|
||
|
// https://nodejs.org/api/intl.html#detecting-internationalization-support
|
||
|
const january = new Date(9e8);
|
||
|
const spanish = new Intl.DateTimeFormat('es', { month: 'long' });
|
||
|
if (spanish.format(january) === 'enero') {
|
||
|
const options = Intl.DateTimeFormat().resolvedOptions();
|
||
|
|
||
|
return {
|
||
|
locale: options.locale,
|
||
|
timezone: options.timeZone,
|
||
|
};
|
||
|
}
|
||
|
} catch (err) {
|
||
|
//
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
function getAppContext() {
|
||
|
const app_memory = process.memoryUsage().rss;
|
||
|
const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString();
|
||
|
|
||
|
return { app_start_time, app_memory };
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets device information from os
|
||
|
*/
|
||
|
function getDeviceContext(deviceOpt) {
|
||
|
const device = {};
|
||
|
|
||
|
// Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202
|
||
|
let uptime;
|
||
|
try {
|
||
|
uptime = os.uptime && os.uptime();
|
||
|
} catch (e) {
|
||
|
// noop
|
||
|
}
|
||
|
|
||
|
// os.uptime or its return value seem to be undefined in certain environments (e.g. Azure functions).
|
||
|
// Hence, we only set boot time, if we get a valid uptime value.
|
||
|
// @see https://github.com/getsentry/sentry-javascript/issues/5856
|
||
|
if (typeof uptime === 'number') {
|
||
|
device.boot_time = new Date(Date.now() - uptime * 1000).toISOString();
|
||
|
}
|
||
|
|
||
|
device.arch = os.arch();
|
||
|
|
||
|
if (deviceOpt === true || deviceOpt.memory) {
|
||
|
device.memory_size = os.totalmem();
|
||
|
device.free_memory = os.freemem();
|
||
|
}
|
||
|
|
||
|
if (deviceOpt === true || deviceOpt.cpu) {
|
||
|
const cpuInfo = os.cpus();
|
||
|
if (cpuInfo && cpuInfo.length) {
|
||
|
const firstCpu = cpuInfo[0];
|
||
|
|
||
|
device.processor_count = cpuInfo.length;
|
||
|
device.cpu_description = firstCpu.model;
|
||
|
device.processor_frequency = firstCpu.speed;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return device;
|
||
|
}
|
||
|
|
||
|
/** Mapping of Node's platform names to actual OS names. */
|
||
|
const PLATFORM_NAMES = {
|
||
|
aix: 'IBM AIX',
|
||
|
freebsd: 'FreeBSD',
|
||
|
openbsd: 'OpenBSD',
|
||
|
sunos: 'SunOS',
|
||
|
win32: 'Windows',
|
||
|
};
|
||
|
|
||
|
/** Linux version file to check for a distribution. */
|
||
|
|
||
|
/** Mapping of linux release files located in /etc to distributions. */
|
||
|
const LINUX_DISTROS = [
|
||
|
{ name: 'fedora-release', distros: ['Fedora'] },
|
||
|
{ name: 'redhat-release', distros: ['Red Hat Linux', 'Centos'] },
|
||
|
{ name: 'redhat_version', distros: ['Red Hat Linux'] },
|
||
|
{ name: 'SuSE-release', distros: ['SUSE Linux'] },
|
||
|
{ name: 'lsb-release', distros: ['Ubuntu Linux', 'Arch Linux'] },
|
||
|
{ name: 'debian_version', distros: ['Debian'] },
|
||
|
{ name: 'debian_release', distros: ['Debian'] },
|
||
|
{ name: 'arch-release', distros: ['Arch Linux'] },
|
||
|
{ name: 'gentoo-release', distros: ['Gentoo Linux'] },
|
||
|
{ name: 'novell-release', distros: ['SUSE Linux'] },
|
||
|
{ name: 'alpine-release', distros: ['Alpine Linux'] },
|
||
|
];
|
||
|
|
||
|
/** Functions to extract the OS version from Linux release files. */
|
||
|
const LINUX_VERSIONS
|
||
|
|
||
|
= {
|
||
|
alpine: content => content,
|
||
|
arch: content => matchFirst(/distrib_release=(.*)/, content),
|
||
|
centos: content => matchFirst(/release ([^ ]+)/, content),
|
||
|
debian: content => content,
|
||
|
fedora: content => matchFirst(/release (..)/, content),
|
||
|
mint: content => matchFirst(/distrib_release=(.*)/, content),
|
||
|
red: content => matchFirst(/release ([^ ]+)/, content),
|
||
|
suse: content => matchFirst(/VERSION = (.*)\n/, content),
|
||
|
ubuntu: content => matchFirst(/distrib_release=(.*)/, content),
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Executes a regular expression with one capture group.
|
||
|
*
|
||
|
* @param regex A regular expression to execute.
|
||
|
* @param text Content to execute the RegEx on.
|
||
|
* @returns The captured string if matched; otherwise undefined.
|
||
|
*/
|
||
|
function matchFirst(regex, text) {
|
||
|
const match = regex.exec(text);
|
||
|
return match ? match[1] : undefined;
|
||
|
}
|
||
|
|
||
|
/** Loads the macOS operating system context. */
|
||
|
async function getDarwinInfo() {
|
||
|
// Default values that will be used in case no operating system information
|
||
|
// can be loaded. The default version is computed via heuristics from the
|
||
|
// kernel version, but the build ID is missing.
|
||
|
const darwinInfo = {
|
||
|
kernel_version: os.release(),
|
||
|
name: 'Mac OS X',
|
||
|
version: `10.${Number(os.release().split('.')[0]) - 4}`,
|
||
|
};
|
||
|
|
||
|
try {
|
||
|
// We try to load the actual macOS version by executing the `sw_vers` tool.
|
||
|
// This tool should be available on every standard macOS installation. In
|
||
|
// case this fails, we stick with the values computed above.
|
||
|
|
||
|
const output = await new Promise((resolve, reject) => {
|
||
|
child_process.execFile('/usr/bin/sw_vers', (error, stdout) => {
|
||
|
if (error) {
|
||
|
reject(error);
|
||
|
return;
|
||
|
}
|
||
|
resolve(stdout);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
darwinInfo.name = matchFirst(/^ProductName:\s+(.*)$/m, output);
|
||
|
darwinInfo.version = matchFirst(/^ProductVersion:\s+(.*)$/m, output);
|
||
|
darwinInfo.build = matchFirst(/^BuildVersion:\s+(.*)$/m, output);
|
||
|
} catch (e) {
|
||
|
// ignore
|
||
|
}
|
||
|
|
||
|
return darwinInfo;
|
||
|
}
|
||
|
|
||
|
/** Returns a distribution identifier to look up version callbacks. */
|
||
|
function getLinuxDistroId(name) {
|
||
|
return name.split(' ')[0].toLowerCase();
|
||
|
}
|
||
|
|
||
|
/** Loads the Linux operating system context. */
|
||
|
async function getLinuxInfo() {
|
||
|
// By default, we cannot assume anything about the distribution or Linux
|
||
|
// version. `os.release()` returns the kernel version and we assume a generic
|
||
|
// "Linux" name, which will be replaced down below.
|
||
|
const linuxInfo = {
|
||
|
kernel_version: os.release(),
|
||
|
name: 'Linux',
|
||
|
};
|
||
|
|
||
|
try {
|
||
|
// We start guessing the distribution by listing files in the /etc
|
||
|
// directory. This is were most Linux distributions (except Knoppix) store
|
||
|
// release files with certain distribution-dependent meta data. We search
|
||
|
// for exactly one known file defined in `LINUX_DISTROS` and exit if none
|
||
|
// are found. In case there are more than one file, we just stick with the
|
||
|
// first one.
|
||
|
const etcFiles = await readDirAsync('/etc');
|
||
|
const distroFile = LINUX_DISTROS.find(file => etcFiles.includes(file.name));
|
||
|
if (!distroFile) {
|
||
|
return linuxInfo;
|
||
|
}
|
||
|
|
||
|
// Once that file is known, load its contents. To make searching in those
|
||
|
// files easier, we lowercase the file contents. Since these files are
|
||
|
// usually quite small, this should not allocate too much memory and we only
|
||
|
// hold on to it for a very short amount of time.
|
||
|
const distroPath = path.join('/etc', distroFile.name);
|
||
|
const contents = ((await readFileAsync(distroPath, { encoding: 'utf-8' })) ).toLowerCase();
|
||
|
|
||
|
// Some Linux distributions store their release information in the same file
|
||
|
// (e.g. RHEL and Centos). In those cases, we scan the file for an
|
||
|
// identifier, that basically consists of the first word of the linux
|
||
|
// distribution name (e.g. "red" for Red Hat). In case there is no match, we
|
||
|
// just assume the first distribution in our list.
|
||
|
const { distros } = distroFile;
|
||
|
linuxInfo.name = distros.find(d => contents.indexOf(getLinuxDistroId(d)) >= 0) || distros[0];
|
||
|
|
||
|
// Based on the found distribution, we can now compute the actual version
|
||
|
// number. This is different for every distribution, so several strategies
|
||
|
// are computed in `LINUX_VERSIONS`.
|
||
|
const id = getLinuxDistroId(linuxInfo.name);
|
||
|
linuxInfo.version = LINUX_VERSIONS[id](contents);
|
||
|
} catch (e) {
|
||
|
// ignore
|
||
|
}
|
||
|
|
||
|
return linuxInfo;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Grabs some information about hosting provider based on best effort.
|
||
|
*/
|
||
|
function getCloudResourceContext() {
|
||
|
if (process.env.VERCEL) {
|
||
|
// https://vercel.com/docs/concepts/projects/environment-variables/system-environment-variables#system-environment-variables
|
||
|
return {
|
||
|
'cloud.provider': 'vercel',
|
||
|
'cloud.region': process.env.VERCEL_REGION,
|
||
|
};
|
||
|
} else if (process.env.AWS_REGION) {
|
||
|
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
|
||
|
return {
|
||
|
'cloud.provider': 'aws',
|
||
|
'cloud.region': process.env.AWS_REGION,
|
||
|
'cloud.platform': process.env.AWS_EXECUTION_ENV,
|
||
|
};
|
||
|
} else if (process.env.GCP_PROJECT) {
|
||
|
// https://cloud.google.com/composer/docs/how-to/managing/environment-variables#reserved_variables
|
||
|
return {
|
||
|
'cloud.provider': 'gcp',
|
||
|
};
|
||
|
} else if (process.env.ALIYUN_REGION_ID) {
|
||
|
// TODO: find where I found these environment variables - at least gc.github.com returns something
|
||
|
return {
|
||
|
'cloud.provider': 'alibaba_cloud',
|
||
|
'cloud.region': process.env.ALIYUN_REGION_ID,
|
||
|
};
|
||
|
} else if (process.env.WEBSITE_SITE_NAME && process.env.REGION_NAME) {
|
||
|
// https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings?tabs=kudu%2Cdotnet#app-environment
|
||
|
return {
|
||
|
'cloud.provider': 'azure',
|
||
|
'cloud.region': process.env.REGION_NAME,
|
||
|
};
|
||
|
} else if (process.env.IBM_CLOUD_REGION) {
|
||
|
// TODO: find where I found these environment variables - at least gc.github.com returns something
|
||
|
return {
|
||
|
'cloud.provider': 'ibm_cloud',
|
||
|
'cloud.region': process.env.IBM_CLOUD_REGION,
|
||
|
};
|
||
|
} else if (process.env.TENCENTCLOUD_REGION) {
|
||
|
// https://www.tencentcloud.com/document/product/583/32748
|
||
|
return {
|
||
|
'cloud.provider': 'tencent_cloud',
|
||
|
'cloud.region': process.env.TENCENTCLOUD_REGION,
|
||
|
'cloud.account.id': process.env.TENCENTCLOUD_APPID,
|
||
|
'cloud.availability_zone': process.env.TENCENTCLOUD_ZONE,
|
||
|
};
|
||
|
} else if (process.env.NETLIFY) {
|
||
|
// https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables
|
||
|
return {
|
||
|
'cloud.provider': 'netlify',
|
||
|
};
|
||
|
} else if (process.env.FLY_REGION) {
|
||
|
// https://fly.io/docs/reference/runtime-environment/
|
||
|
return {
|
||
|
'cloud.provider': 'fly.io',
|
||
|
'cloud.region': process.env.FLY_REGION,
|
||
|
};
|
||
|
} else if (process.env.DYNO) {
|
||
|
// https://devcenter.heroku.com/articles/dynos#local-environment-variables
|
||
|
return {
|
||
|
'cloud.provider': 'heroku',
|
||
|
};
|
||
|
} else {
|
||
|
return undefined;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.Context = Context;
|
||
|
exports.getDeviceContext = getDeviceContext;
|
||
|
exports.nodeContextIntegration = nodeContextIntegration;
|
||
|
exports.readDirAsync = readDirAsync;
|
||
|
exports.readFileAsync = readFileAsync;
|
||
|
//# sourceMappingURL=context.js.map
|