Skip to content

Commit 4420844

Browse files
authored
feat(node): Add app.free_memory info to events (#12150)
Adds support for https://nodejs.org/api/process.html#processavailablememory introduced with Node 22 App Context: https://develop.sentry.dev/sdk/event-payloads/contexts/#app-context ref #11455
1 parent f3c8a86 commit 4420844

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

packages/node/src/integrations/context.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import { execFile } from 'node:child_process';
23
import { readFile, readdir } from 'node:fs';
34
import * as os from 'node:os';
@@ -18,6 +19,12 @@ import type {
1819
export const readFileAsync = promisify(readFile);
1920
export const readDirAsync = promisify(readdir);
2021

22+
// Process enhanced with methods from Node 18, 20, 22 as @types/node
23+
// is on `14.18.0` to match minimum version requirements of the SDK
24+
interface ProcessWithCurrentValues extends NodeJS.Process {
25+
availableMemory?(): number;
26+
}
27+
2128
const INTEGRATION_NAME = 'Context';
2229

2330
interface DeviceContextOptions {
@@ -114,10 +121,18 @@ export const nodeContextIntegration = defineIntegration(_nodeContextIntegration)
114121
*/
115122
function _updateContext(contexts: Contexts): Contexts {
116123
// Only update properties if they exist
124+
117125
if (contexts?.app?.app_memory) {
118126
contexts.app.app_memory = process.memoryUsage().rss;
119127
}
120128

129+
if (contexts?.app?.free_memory && typeof (process as ProcessWithCurrentValues).availableMemory === 'function') {
130+
const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.();
131+
if (freeMemory != null) {
132+
contexts.app.free_memory = freeMemory;
133+
}
134+
}
135+
121136
if (contexts?.device?.free_memory) {
122137
contexts.device.free_memory = os.freemem();
123138
}
@@ -183,11 +198,23 @@ function getCultureContext(): CultureContext | undefined {
183198
return;
184199
}
185200

186-
function getAppContext(): AppContext {
201+
/**
202+
* Get app context information from process
203+
*/
204+
export function getAppContext(): AppContext {
187205
const app_memory = process.memoryUsage().rss;
188206
const app_start_time = new Date(Date.now() - process.uptime() * 1000).toISOString();
207+
// https://nodejs.org/api/process.html#processavailablememory
208+
const appContext: AppContext = { app_start_time, app_memory };
209+
210+
if (typeof (process as ProcessWithCurrentValues).availableMemory === 'function') {
211+
const freeMemory = (process as ProcessWithCurrentValues).availableMemory?.();
212+
if (freeMemory != null) {
213+
appContext.free_memory = freeMemory;
214+
}
215+
}
189216

190-
return { app_start_time, app_memory };
217+
return appContext;
191218
}
192219

193220
/**
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { parseSemver } from '@sentry/utils';
2+
3+
const NODE_VERSION = parseSemver(process.versions.node).major;
4+
5+
/**
6+
* Returns`describe` or `describe.skip` depending on allowed major versions of Node.
7+
*
8+
* @param {{ min?: number; max?: number }} allowedVersion
9+
* @return {*} {jest.Describe}
10+
*/
11+
export const conditionalTest = (allowedVersion: { min?: number; max?: number }): jest.It => {
12+
if (!NODE_VERSION) {
13+
return it.skip;
14+
}
15+
16+
return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity)
17+
? test.skip
18+
: test;
19+
};

packages/node/test/integrations/context.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
1-
import * as os from 'os';
1+
import * as os from 'node:os';
22

3-
import { getDeviceContext } from '../../src/integrations/context';
3+
import { getAppContext, getDeviceContext } from '../../src/integrations/context';
4+
import { conditionalTest } from '../helpers/conditional';
45

56
describe('Context', () => {
7+
describe('getAppContext', () => {
8+
afterAll(() => {
9+
jest.clearAllMocks();
10+
});
11+
12+
conditionalTest({ max: 18 })('it does not return free_memory on older node versions', () => {
13+
const appContext = getAppContext();
14+
expect(appContext.free_memory).toBeUndefined();
15+
});
16+
17+
conditionalTest({ min: 22 })(
18+
'returns free_memory if process.availableMemory is defined and returns a valid value',
19+
() => {
20+
const appContext = getAppContext();
21+
expect(appContext.free_memory).toEqual(expect.any(Number));
22+
},
23+
);
24+
25+
conditionalTest({ min: 22 })('returns no free_memory if process.availableMemory ', () => {
26+
jest.spyOn(process as any, 'availableMemory').mockReturnValue(undefined as unknown as number);
27+
const appContext = getAppContext();
28+
expect(appContext.free_memory).toBeUndefined();
29+
});
30+
});
31+
632
describe('getDeviceContext', () => {
733
afterAll(() => {
834
jest.clearAllMocks();

packages/types/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface AppContext extends Record<string, unknown> {
2828
app_identifier?: string;
2929
build_type?: string;
3030
app_memory?: number;
31+
free_memory?: number;
3132
}
3233

3334
export interface DeviceContext extends Record<string, unknown> {

0 commit comments

Comments
 (0)