hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
479 lines (418 loc) • 16 kB
text/typescript
/* eslint-disable -- This file is vendored from https://github.com/getsentry/sentry-javascript/blob/9.4.0/packages/node/src/integrations/context.ts */
import { execFile } from 'node:child_process';
import { readFile, readdir } from 'node:fs';
import * as os from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import type {
AppContext,
CloudResourceContext,
Contexts,
CultureContext,
DeviceContext,
Event,
IntegrationFn,
OsContext,
} from '@sentry/core';
import { defineIntegration } from '@sentry/core';
export const readFileAsync: any = promisify(readFile);
export const readDirAsync: any = promisify(readdir);
// Process enhanced with methods from Node 18, 20, 22 as @types/node
// is on `14.18.0` to match minimum version requirements of the SDK
interface ProcessWithCurrentValues extends NodeJS.Process {
availableMemory(): number;
}
const INTEGRATION_NAME = 'Context';
interface DeviceContextOptions {
cpu?: boolean;
memory?: boolean;
}
interface ContextOptions {
app?: boolean;
os?: boolean;
device?: DeviceContextOptions | boolean;
culture?: boolean;
cloudResource?: boolean;
}
const _nodeContextIntegration = ((options: ContextOptions = {}) => {
let cachedContext: Promise<Contexts> | undefined;
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: Event): Promise<Event> {
if (cachedContext === undefined) {
cachedContext = _getContexts();
}
const updatedContext = _updateContext(await cachedContext);
event.contexts = {
...event.contexts,
app: { ...updatedContext.app, ...event.contexts?.app },
os: { ...updatedContext.os, ...event.contexts?.os },
device: { ...updatedContext.device, ...event.contexts?.device },
culture: { ...updatedContext.culture, ...event.contexts?.culture },
cloud_resource: { ...updatedContext.cloud_resource, ...event.contexts?.cloud_resource },
};
return event;
}
/** Get the contexts from node. */
async function _getContexts(): Promise<Contexts> {
const contexts: 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,
processEvent(event) {
return addContext(event);
},
};
}) satisfies IntegrationFn;
/**
* Capture context about the environment and the device that the client is running on, to events.
*/
export const nodeContextIntegration: any = defineIntegration(_nodeContextIntegration);
/**
* Updates the context with dynamic values that can change
*/
function _updateContext(contexts: Contexts): Contexts {
// Only update properties if they exist
if (contexts.app?.app_memory) {
contexts.app.app_memory = process.memoryUsage().rss;
}
if (contexts.app?.free_memory && typeof (process as ProcessWithCurrentValues).availableMemory === 'function') {
const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.();
if (freeMemory != null) {
contexts.app.free_memory = freeMemory;
}
}
if (contexts.device?.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(): Promise<OsContext> {
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(): CultureContext | undefined {
try {
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;
}
/**
* Get app context information from process
*/
export function getAppContext(): AppContext {
const app_memory = process.memoryUsage().rss;
const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString();
// https://nodejs.org/api/process.html#processavailablememory
const appContext: AppContext = { app_start_time, app_memory };
if (typeof (process as ProcessWithCurrentValues).availableMemory === 'function') {
const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.();
if (freeMemory != null) {
appContext.free_memory = freeMemory;
}
}
return appContext;
}
/**
* Gets device information from os
*/
export function getDeviceContext(deviceOpt: DeviceContextOptions | true): DeviceContext {
const device: DeviceContext = {};
// Sometimes os.uptime() throws due to lacking permissions: https://github.com/getsentry/sentry-javascript/issues/8202
let uptime;
try {
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() as os.CpuInfo[] | undefined;
const firstCpu = cpuInfo?.[0];
if (firstCpu) {
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: { [platform: string]: string } = {
aix: 'IBM AIX',
freebsd: 'FreeBSD',
openbsd: 'OpenBSD',
sunos: 'SunOS',
win32: 'Windows',
};
/** Linux version file to check for a distribution. */
interface DistroFile {
/** The file name, located in `/etc`. */
name: string;
/** Potential distributions to check. */
distros: [string, ...string[]];
}
/** Mapping of linux release files located in /etc to distributions. */
const LINUX_DISTROS: DistroFile[] = [
{ 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: {
[identifier: string]: (content: string) => string | undefined;
} = {
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: RegExp, text: string): string | undefined {
const match = regex.exec(text);
return match ? match[1] : undefined;
}
/** Loads the macOS operating system context. */
async function getDarwinInfo(): Promise<OsContext> {
// 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: OsContext = {
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<string>((resolve, reject) => {
execFile('/usr/bin/sw_vers', (error: Error | null, stdout: string) => {
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: string): string {
return (name.split(' ') as [string])[0].toLowerCase();
}
/** Loads the Linux operating system context. */
async function getLinuxInfo(): Promise<OsContext> {
// 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: OsContext = {
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 = join('/etc', distroFile.name);
const contents = ((await readFileAsync(distroPath, { encoding: 'utf-8' })) as string).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(): CloudResourceContext | undefined {
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;
}
}