From 64b4c3f5c8d92be84a4b3eb8b7cd28ac2cdaf6b9 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 21 May 2024 15:08:28 -0400 Subject: [PATCH 1/2] feat(node): Add app.free_memory info to events --- packages/node/src/integrations/context.ts | 31 +++++++++++++++++-- packages/node/test/helpers/conditional.ts | 19 ++++++++++++ .../node/test/integrations/context.test.ts | 30 ++++++++++++++++-- packages/types/src/context.ts | 1 + 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 packages/node/test/helpers/conditional.ts diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index 35410effa528..b2ef83f20b4d 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { execFile } from 'node:child_process'; import { readFile, readdir } from 'node:fs'; import * as os from 'node:os'; @@ -18,6 +19,12 @@ import type { export const readFileAsync = promisify(readFile); export const readDirAsync = 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 { @@ -114,10 +121,18 @@ export const nodeContextIntegration = defineIntegration(_nodeContextIntegration) */ 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(); } @@ -183,11 +198,23 @@ function getCultureContext(): CultureContext | undefined { return; } -function getAppContext(): AppContext { +/** + * 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 { app_start_time, app_memory }; + return appContext; } /** diff --git a/packages/node/test/helpers/conditional.ts b/packages/node/test/helpers/conditional.ts new file mode 100644 index 000000000000..7c6ecab77cd7 --- /dev/null +++ b/packages/node/test/helpers/conditional.ts @@ -0,0 +1,19 @@ +import { parseSemver } from '@sentry/utils'; + +const NODE_VERSION = parseSemver(process.versions.node).major; + +/** + * Returns`describe` or `describe.skip` depending on allowed major versions of Node. + * + * @param {{ min?: number; max?: number }} allowedVersion + * @return {*} {jest.Describe} + */ +export const conditionalTest = (allowedVersion: { min?: number; max?: number }): jest.It => { + if (!NODE_VERSION) { + return it.skip; + } + + return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity) + ? test.skip + : test; +}; diff --git a/packages/node/test/integrations/context.test.ts b/packages/node/test/integrations/context.test.ts index 519e101187ff..dde182c552ba 100644 --- a/packages/node/test/integrations/context.test.ts +++ b/packages/node/test/integrations/context.test.ts @@ -1,8 +1,34 @@ -import * as os from 'os'; +import * as os from 'node:os'; -import { getDeviceContext } from '../../src/integrations/context'; +import { getAppContext, getDeviceContext } from '../../src/integrations/context'; +import { conditionalTest } from '../helpers/conditional'; describe('Context', () => { + describe('getAppContext', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + conditionalTest({ max: 18 })('it does not return free_memory on older node versions', () => { + const appContext = getAppContext(); + expect(appContext.free_memory).toBeUndefined(); + }); + + conditionalTest({ min: 22 })( + 'returns free_memory if process.availableMemory is defined and returns a valid value', + () => { + const appContext = getAppContext(); + expect(appContext.free_memory).toEqual(expect.any(Number)); + }, + ); + + conditionalTest({ min: 22 })('returns no free_memory if process.availableMemory ', () => { + jest.spyOn(process as any, 'availableMemory').mockReturnValue(undefined as unknown as number); + const appContext = getAppContext(); + expect(appContext.free_memory).toBeUndefined(); + }); + }); + describe('getDeviceContext', () => { afterAll(() => { jest.clearAllMocks(); diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 4f92ff4c1c6a..40d3822d6b3c 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -28,6 +28,7 @@ export interface AppContext extends Record { app_identifier?: string; build_type?: string; app_memory?: number; + free_memory?: number; } export interface DeviceContext extends Record { From 6ba2f00c7403765237cfa437c1309f038330fc58 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 21 May 2024 18:38:03 -0400 Subject: [PATCH 2/2] make optionality more clear --- packages/node/src/integrations/context.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index b2ef83f20b4d..379af4710794 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -22,7 +22,7 @@ export const readDirAsync = 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; + availableMemory?(): number; } const INTEGRATION_NAME = 'Context'; @@ -127,7 +127,7 @@ function _updateContext(contexts: Contexts): Contexts { } if (contexts?.app?.free_memory && typeof (process as ProcessWithCurrentValues).availableMemory === 'function') { - const freeMemory = (process as ProcessWithCurrentValues).availableMemory(); + const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.(); if (freeMemory != null) { contexts.app.free_memory = freeMemory; } @@ -208,7 +208,7 @@ export function getAppContext(): AppContext { const appContext: AppContext = { app_start_time, app_memory }; if (typeof (process as ProcessWithCurrentValues).availableMemory === 'function') { - const freeMemory = (process as ProcessWithCurrentValues).availableMemory(); + const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.(); if (freeMemory != null) { appContext.free_memory = freeMemory; }