diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index b27f8a0030ae..841b7ca0b0ba 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -12,6 +12,9 @@ "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{suites,utils}/**/*.ts\"", "type-check": "tsc", "pretest": "run-s --silent prisma:init", "test": "jest --runInBand --forceExit", diff --git a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts index 8dd0779ea8f9..9f5ca599f891 100644 --- a/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts +++ b/packages/node-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -1,10 +1,10 @@ -import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; test('should add multiple breadcrumbs', async () => { const config = await runServer(__dirname); - const events = await getMultipleEnvelopeRequest(config, { count: 1 }); + const events = await getEnvelopeRequest(config); - assertSentryEvent(events[0][2], { + assertSentryEvent(events[2], { message: 'test_multi_breadcrumbs', breadcrumbs: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts b/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts index 02a4773181eb..e09f29fae48a 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/catched-error/test.ts @@ -1,10 +1,10 @@ -import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; test('should work inside catch block', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getMultipleEnvelopeRequest(config, { count: 1 }); - assertSentryEvent(event[2], { + assertSentryEvent(events[0][2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts b/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts index f11bbaad9527..d0fca28137c6 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/empty-obj/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should capture an empty object', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts b/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts index df373fa0e441..f09d75769385 100644 --- a/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureException/simple-error/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should capture a simple error with message', async () => { const config = await runServer(__dirname); - const envelope = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(envelope[2], { + assertSentryEvent(events[2], { exception: { values: [ { diff --git a/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts index a718e083a26d..bc940f99eafc 100644 --- a/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/packages/node-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should capture a simple message string', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'Message', level: 'info', }); diff --git a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts index 271d47c74238..a365f63af9fa 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts @@ -4,9 +4,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should record multiple contexts', async () => { const config = await runServer(__dirname); - const envelope = await getEnvelopeRequest(config, { count: 1 }); + const events = await getEnvelopeRequest(config); - assertSentryEvent(envelope[2], { + assertSentryEvent(events[2], { message: 'multiple_contexts', contexts: { context_1: { @@ -17,5 +17,5 @@ test('should record multiple contexts', async () => { }, }); - expect((envelope[2] as Event).contexts?.context_3).not.toBeDefined(); + expect((events[0] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts index 15e1b9456e8d..d685215aa0c7 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts @@ -4,12 +4,12 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should normalize non-serializable context', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'non_serializable', contexts: {}, }); - expect((event as Event).contexts?.context_3).not.toBeDefined(); + expect((events[0] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts index ea787fbea7c7..16a4ab8486d3 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts @@ -1,12 +1,12 @@ import { Event } from '@sentry/node'; -import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; test('should set a simple context', async () => { const config = await runServer(__dirname); - const envelopes = await getMultipleEnvelopeRequest(config, { count: 1 }); + const envelopes = await getEnvelopeRequest(config); - assertSentryEvent(envelopes[0][2], { + assertSentryEvent(envelopes[2], { message: 'simple_context_object', contexts: { foo: { @@ -15,5 +15,5 @@ test('should set a simple context', async () => { }, }); - expect((envelopes[0][2] as Event).contexts?.context_3).not.toBeDefined(); + expect((envelopes[2] as Event).contexts?.context_3).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts index b0258e87abb2..0bad46b894de 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should record multiple extras of different types', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'multiple_extras', extra: { extra_1: { foo: 'bar', baz: { qux: 'quux' } }, diff --git a/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts index 5e81a22d8722..9af242805d36 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should normalize non-serializable extra', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'non_serializable', extra: {}, }); diff --git a/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts b/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts index 59fb5f8a6867..4441f0939000 100644 --- a/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtra/simple-extra/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should set a simple extra', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'simple_extra', extra: { foo: { diff --git a/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts b/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts index 277361de82bb..87a5f10994d2 100644 --- a/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts @@ -1,10 +1,10 @@ -import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; test('should set extras from multiple consecutive calls', async () => { const config = await runServer(__dirname); - const envelopes = await getMultipleEnvelopeRequest(config, { count: 1 }); + const envelopes = await getEnvelopeRequest(config); - assertSentryEvent(envelopes[0][2], { + assertSentryEvent(envelopes[2], { message: 'consecutive_calls', extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } }, }); diff --git a/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts b/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts index 23b73c221fc3..4174ca98df93 100644 --- a/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts +++ b/packages/node-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts @@ -1,10 +1,10 @@ -import { assertSentryEvent, getMultipleEnvelopeRequest, runServer } from '../../../../utils'; +import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../utils'; test('should record an extras object', async () => { const config = await runServer(__dirname); - const events = await getMultipleEnvelopeRequest(config, { count: 1 }); + const events = await getEnvelopeRequest(config); - assertSentryEvent(events[0][2], { + assertSentryEvent(events[2], { message: 'multiple_extras', extra: { extra_1: [1, ['foo'], 'bar'], diff --git a/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts b/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts index bae5b8b4b96d..16c81c323a58 100644 --- a/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts +++ b/packages/node-integration-tests/suites/public-api/setTag/with-primitives/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should set primitive tags', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'primitive_tags', tags: { tag_1: 'foo', diff --git a/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts b/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts index bae5b8b4b96d..16c81c323a58 100644 --- a/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts +++ b/packages/node-integration-tests/suites/public-api/setTags/with-primitives/test.ts @@ -2,9 +2,9 @@ import { assertSentryEvent, getEnvelopeRequest, runServer } from '../../../../ut test('should set primitive tags', async () => { const config = await runServer(__dirname); - const event = await getEnvelopeRequest(config); + const events = await getEnvelopeRequest(config); - assertSentryEvent(event[2], { + assertSentryEvent(events[2], { message: 'primitive_tags', tags: { tag_1: 'foo', diff --git a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts index bc552e0951ad..2f9934bd87e7 100644 --- a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -10,7 +10,7 @@ test('should unset user', async () => { message: 'no_user', }); - expect((events[0][2] as Event).user).not.toBeDefined(); + expect((events[0] as Event).user).not.toBeDefined(); assertSentryEvent(events[1][2], { message: 'user', @@ -25,5 +25,5 @@ test('should unset user', async () => { message: 'unset_user', }); - expect((events[2][2] as Event).user).not.toBeDefined(); + expect((events[2] as Event).user).not.toBeDefined(); }); diff --git a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts index 8d12c495c203..daea172170c9 100644 --- a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts +++ b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts @@ -31,7 +31,7 @@ test('should allow nested scoping', async () => { }, }); - expect((events[2][2] as Event).user).toBeUndefined(); + expect((events[2] as Event).user).toBeUndefined(); assertSentryEvent(events[3][2], { message: 'outer_after', diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts index 23f15c7e4307..44a6d9543642 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/test.ts @@ -14,7 +14,9 @@ conditionalTest({ min: 12 })('MongoDB Test', () => { }, 10000); afterAll(async () => { - await mongoServer.stop(); + if (mongoServer) { + await mongoServer.stop(); + } }); test('should auto-instrument `mongodb` package.', async () => { diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts new file mode 100644 index 000000000000..62c2b4df8f68 --- /dev/null +++ b/packages/node/src/integrations/context.ts @@ -0,0 +1,357 @@ +/* eslint-disable max-lines */ +import { + AppContext, + Contexts, + CultureContext, + DeviceContext, + Event, + EventProcessor, + Integration, + OsContext, +} from '@sentry/types'; +import { execFile } from 'child_process'; +import { readdir, readFile } from 'fs'; +import * as os from 'os'; +import { join } from 'path'; +import { promisify } from 'util'; + +// TODO: Required until we drop support for Node v8 +export const readFileAsync = promisify(readFile); +export const readDirAsync = promisify(readdir); + +interface DeviceContextOptions { + cpu?: boolean; + memory?: boolean; +} + +interface ContextOptions { + app?: boolean; + os?: boolean; + device?: DeviceContextOptions | boolean; + culture?: boolean; +} + +/** Add node modules / packages to the event */ +export class Context implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Context'; + + /** + * @inheritDoc + */ + public name: string = Context.id; + + /** + * Caches context so it's only evaluated once + */ + private _cachedContext: Promise | undefined; + + public constructor(private readonly _options: ContextOptions = { app: true, os: true, device: true, culture: true }) { + // + } + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + addGlobalEventProcessor(event => this.addContext(event)); + } + + /** Processes an event and adds context */ + public async addContext(event: Event): Promise { + if (this._cachedContext === undefined) { + this._cachedContext = this._getContexts(); + } + + event.contexts = { ...event.contexts, ...this._updateContext(await this._cachedContext) }; + + return event; + } + + /** + * Updates the context with dynamic values that can change + */ + private _updateContext(contexts: Contexts): Contexts { + // Only update properties if they exist + if (contexts?.app?.app_memory) { + contexts.app.app_memory = process.memoryUsage().rss; + } + + if (contexts?.device?.free_memory) { + contexts.device.free_memory = os.freemem(); + } + + return contexts; + } + + /** + * Gets the contexts for the current environment + */ + private async _getContexts(): Promise { + const contexts: Contexts = {}; + + if (this._options.os) { + contexts.os = await getOsContext(); + } + + if (this._options.app) { + contexts.app = getAppContext(); + } + + if (this._options.device) { + contexts.device = getDeviceContext(this._options.device); + } + + if (this._options.culture) { + const culture = getCultureContext(); + + if (culture) { + contexts.culture = culture; + } + } + + 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 { + 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 { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + if ((process.versions as unknown as any).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(): AppContext { + const app_memory = process.memoryUsage().rss; + const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString(); + + return { app_start_time, app_memory }; +} + +function getDeviceContext(deviceOpt: DeviceContextOptions | true): DeviceContext { + const device: DeviceContext = {}; + + device.boot_time = new Date(Date.now() - os.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.CpuInfo[] | undefined = 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: { [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[]; +} + +/** 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 { + // 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((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(' ')[0].toLowerCase(); +} + +/** Loads the Linux operating system context. */ +async function getLinuxInfo(): Promise { + // 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; +} diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 8017ba458e49..05a25d7cbad3 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -5,3 +5,4 @@ export { OnUnhandledRejection } from './onunhandledrejection'; export { LinkedErrors } from './linkederrors'; export { Modules } from './modules'; export { ContextLines } from './contextlines'; +export { Context } from './context'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index fea6f4473ba8..db1e7e883ced 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -18,7 +18,15 @@ import * as domain from 'domain'; import * as url from 'url'; import { NodeClient } from './client'; -import { Console, ContextLines, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; +import { + Console, + Context, + ContextLines, + Http, + LinkedErrors, + OnUncaughtException, + OnUnhandledRejection, +} from './integrations'; import { getModule } from './module'; import { makeNodeTransport } from './transports'; import { NodeClientOptions, NodeOptions } from './types'; @@ -36,6 +44,7 @@ export const defaultIntegrations = [ new OnUnhandledRejection(), // Misc new LinkedErrors(), + new Context(), ]; /** diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 6fbbeb00b6e6..4b6a08585273 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -1,2 +1,72 @@ export type Context = Record; -export type Contexts = Record; + +export interface Contexts extends Record { + app?: AppContext; + device?: DeviceContext; + os?: OsContext; + culture?: CultureContext; +} + +export interface AppContext extends Record { + app_name?: string; + app_start_time?: string; + app_version?: string; + app_identifier?: string; + build_type?: string; + app_memory?: number; +} + +export interface DeviceContext extends Record { + name?: string; + family?: string; + model?: string; + model_id?: string; + arch?: string; + battery_level?: number; + orientation?: 'portrait' | 'landscape'; + manufacturer?: string; + brand?: string; + screen_resolution?: string; + screen_height_pixels?: number; + screen_width_pixels?: number; + screen_density?: number; + screen_dpi?: number; + online?: boolean; + charging?: boolean; + low_memory?: boolean; + simulator?: boolean; + memory_size?: number; + free_memory?: number; + usable_memory?: number; + storage_size?: number; + free_storage?: number; + external_storage_size?: number; + external_free_storage?: number; + boot_time?: string; + processor_count?: number; + cpu_description?: string; + processor_frequency?: number; + device_type?: string; + battery_status?: string; + device_unique_identifier?: string; + supports_vibration?: boolean; + supports_accelerometer?: boolean; + supports_gyroscope?: boolean; + supports_audio?: boolean; + supports_location_service?: boolean; +} + +export interface OsContext extends Record { + name?: string; + version?: string; + build?: string; + kernel_version?: string; +} + +export interface CultureContext extends Record { + calendar?: string; + display_name?: string; + locale?: string; + is_24_hour_format?: boolean; + timezone?: string; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 80b7fd82a612..c43bb34c8f8e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,7 +3,7 @@ export type { AllowedBaggageKeys, Baggage, BaggageObj } from './baggage'; export type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; export type { Client } from './client'; export type { ClientReport, Outcome, EventDropReason } from './clientreport'; -export type { Context, Contexts } from './context'; +export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext } from './context'; export type { DataCategory } from './datacategory'; export type { DsnComponents, DsnLike, DsnProtocol } from './dsn'; export type { DebugImage, DebugImageType, DebugMeta } from './debugMeta';