Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a3fa333
wip
chargome Mar 20, 2026
1198de9
fix units
chargome Mar 23, 2026
ede983c
.
chargome Mar 23, 2026
36c65ad
trim down defaults
chargome Mar 23, 2026
8eb8b58
fix: re-export nodeRuntimeMetricsIntegration in astro/aws-serverless/…
chargome Mar 24, 2026
22d0b42
fix(test): disable eventLoopDelayP50 in opt-out scenario
chargome Mar 24, 2026
347917f
fix(node): use process.on instead of process.once for beforeExit list…
chargome Mar 25, 2026
d67aa51
fix(node): remove beforeExit flush from nodeRuntimeMetricsIntegration
chargome Mar 25, 2026
eb3c16d
feat(node): add sentry.origin attribute to runtime metrics, full asse…
chargome Mar 25, 2026
ff6e6a0
feat(core): export safeUnref as _INTERNAL_safeUnref, use in nodeRunti…
chargome Mar 25, 2026
83d32be
style(node): use bracketed if blocks in nodeRuntimeMetricsIntegration
chargome Mar 25, 2026
7abc910
fmt
chargome Mar 25, 2026
37b2e12
prevent leaks
chargome Mar 25, 2026
cdd501f
fix(node): use expect.objectContaining for metric attribute assertion…
chargome Mar 25, 2026
a97b5b2
feat(bun): add bunRuntimeMetricsIntegration
chargome Mar 25, 2026
3fca862
test(bun): add integration tests for bunRuntimeMetricsIntegration
chargome Mar 25, 2026
33f816b
fmt
chargome Mar 25, 2026
c2b78bd
meta(changelog): add bunRuntimeMetricsIntegration entry
chargome Mar 26, 2026
5c26482
meta(changelog): consolidate node/bun runtime metrics entries
chargome Mar 26, 2026
c96481e
refactor(bun): use Pick instead of Omit for BunCollectOptions
chargome Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

## Unreleased

### Important Changes

- **feat(node, bun): Add runtime metrics integrations for Node.js and Bun ([#19923](https://github.com/getsentry/sentry-javascript/pull/19923), [#19979](https://github.com/getsentry/sentry-javascript/pull/19979))**

New `nodeRuntimeMetricsIntegration` and `bunRuntimeMetricsIntegration` automatically collect runtime health metrics and send them to Sentry on a configurable interval (default: 30s). Collected metrics include memory (RSS, heap used/total), CPU utilization, event loop utilization, and process uptime. Node additionally collects event loop delay percentiles (p50, p99). Extra metrics like CPU time and external memory are available as opt-in.

```ts
// Node.js
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: '...',
integrations: [Sentry.nodeRuntimeMetricsIntegration()],
});
Comment on lines +5 to +18
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the note for node again bc this was somehow lost in the gitflow merge (?)


// Bun
import * as Sentry from '@sentry/bun';

Sentry.init({
dsn: '...',
integrations: [Sentry.bunRuntimeMetricsIntegration()],
});
```

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

## 10.46.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Sentry from '@sentry/node';
import { bunRuntimeMetricsIntegration } from '@sentry/bun';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0.0',
environment: 'test',
transport: loggingTransport,
integrations: [
bunRuntimeMetricsIntegration({
collectionIntervalMs: 100,
collect: {
cpuTime: true,
memExternal: true,
},
}),
],
});

async function run(): Promise<void> {
await new Promise<void>(resolve => setTimeout(resolve, 250));
await Sentry.flush();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Sentry from '@sentry/node';
import { bunRuntimeMetricsIntegration } from '@sentry/bun';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0.0',
environment: 'test',
transport: loggingTransport,
integrations: [
bunRuntimeMetricsIntegration({
collectionIntervalMs: 100,
collect: {
cpuUtilization: false,
cpuTime: false,
eventLoopUtilization: false,
uptime: false,
},
}),
],
});

async function run(): Promise<void> {
await new Promise<void>(resolve => setTimeout(resolve, 250));
await Sentry.flush();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Sentry from '@sentry/node';
import { bunRuntimeMetricsIntegration } from '@sentry/bun';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0.0',
environment: 'test',
transport: loggingTransport,
integrations: [
bunRuntimeMetricsIntegration({
collectionIntervalMs: 100,
}),
],
});

async function run(): Promise<void> {
await new Promise<void>(resolve => setTimeout(resolve, 250));
await Sentry.flush();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();
116 changes: 116 additions & 0 deletions dev-packages/node-integration-tests/suites/bun-runtime-metrics/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../utils/runner';

const SENTRY_ATTRIBUTES = {
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'sentry.origin': { value: 'auto.bun.runtime_metrics', type: 'string' },
};

const gauge = (name: string, unit?: string) => ({
timestamp: expect.any(Number),
trace_id: expect.any(String),
name,
type: 'gauge',
...(unit ? { unit } : {}),
value: expect.any(Number),
attributes: expect.objectContaining(SENTRY_ATTRIBUTES),
});

const counter = (name: string, unit?: string) => ({
timestamp: expect.any(Number),
trace_id: expect.any(String),
name,
type: 'counter',
...(unit ? { unit } : {}),
value: expect.any(Number),
attributes: expect.objectContaining(SENTRY_ATTRIBUTES),
});

describe('bunRuntimeMetricsIntegration', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('emits default runtime metrics with correct shape', async () => {
const runner = createRunner(__dirname, 'scenario.ts')
.expect({
trace_metric: {
items: expect.arrayContaining([
gauge('bun.runtime.mem.rss', 'byte'),
gauge('bun.runtime.mem.heap_used', 'byte'),
gauge('bun.runtime.mem.heap_total', 'byte'),
gauge('bun.runtime.cpu.utilization'),
gauge('bun.runtime.event_loop.utilization'),
counter('bun.runtime.process.uptime', 'second'),
]),
},
})
.start();

await runner.completed();
});

test('does not emit opt-in metrics by default', async () => {
const runner = createRunner(__dirname, 'scenario.ts')
.expect({
trace_metric: (container: { items: Array<{ name: string }> }) => {
const names = container.items.map(item => item.name);
expect(names).not.toContain('bun.runtime.cpu.user');
expect(names).not.toContain('bun.runtime.cpu.system');
expect(names).not.toContain('bun.runtime.mem.external');
expect(names).not.toContain('bun.runtime.mem.array_buffers');
},
})
.start();

await runner.completed();
});

test('emits all metrics when fully opted in', async () => {
const runner = createRunner(__dirname, 'scenario-all.ts')
.expect({
trace_metric: {
items: expect.arrayContaining([
gauge('bun.runtime.mem.rss', 'byte'),
gauge('bun.runtime.mem.heap_used', 'byte'),
gauge('bun.runtime.mem.heap_total', 'byte'),
gauge('bun.runtime.mem.external', 'byte'),
gauge('bun.runtime.mem.array_buffers', 'byte'),
gauge('bun.runtime.cpu.user', 'second'),
gauge('bun.runtime.cpu.system', 'second'),
gauge('bun.runtime.cpu.utilization'),
gauge('bun.runtime.event_loop.utilization'),
counter('bun.runtime.process.uptime', 'second'),
]),
},
})
.start();

await runner.completed();
});

test('respects opt-out: only memory metrics remain when cpu/event loop/uptime are disabled', async () => {
const runner = createRunner(__dirname, 'scenario-opt-out.ts')
.expect({
trace_metric: (container: { items: Array<{ name: string }> }) => {
const names = container.items.map(item => item.name);

// Memory metrics should still be present
expect(names).toContain('bun.runtime.mem.rss');
expect(names).toContain('bun.runtime.mem.heap_used');
expect(names).toContain('bun.runtime.mem.heap_total');

// Everything else should be absent
expect(names).not.toContain('bun.runtime.cpu.utilization');
expect(names).not.toContain('bun.runtime.event_loop.utilization');
expect(names).not.toContain('bun.runtime.process.uptime');
},
})
.start();

await runner.completed();
});
});
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,5 @@ export type { BunOptions } from './types';
export { BunClient } from './client';
export { getDefaultIntegrations, init } from './sdk';
export { bunServerIntegration } from './integrations/bunserver';
export { bunRuntimeMetricsIntegration, type BunRuntimeMetricsOptions } from './integrations/bunRuntimeMetrics';
export { makeFetchTransport } from './transports';
Loading
Loading