Skip to content

Commit 4be2e67

Browse files
chargomeclaude
andauthored
feat(node, bun): Enforce minimum collection interval in runtime metrics integrations (#20068)
- Values below 1000ms are normalized to 1000ms with a `console.warn` - Non-finite values (e.g. `NaN`) are also caught and fall back to 1000ms - Shared `_INTERNAL_normalizeCollectionInterval` helper extracted to `@sentry/node-core` and re-exported through `@sentry/node` for use in `@sentry/bun` - Each runtime retains ownership of its own `DEFAULT_INTERVAL_MS` - Also adapted deno logging to be in sync with the other runtimes --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ae36796 commit 4be2e67

File tree

15 files changed

+152
-22
lines changed

15 files changed

+152
-22
lines changed

dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const NODE_EXPORTS_IGNORE = [
2121
'SentryContextManager',
2222
'validateOpenTelemetrySetup',
2323
'preloadOpenTelemetry',
24+
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
25+
'_INTERNAL_normalizeCollectionInterval',
2426
];
2527

2628
const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e));

dev-packages/node-integration-tests/suites/bun-runtime-metrics/scenario-all.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Sentry.init({
99
transport: loggingTransport,
1010
integrations: [
1111
bunRuntimeMetricsIntegration({
12-
collectionIntervalMs: 100,
12+
collectionIntervalMs: 1000,
1313
collect: {
1414
cpuTime: true,
1515
memExternal: true,
@@ -19,7 +19,7 @@ Sentry.init({
1919
});
2020

2121
async function run(): Promise<void> {
22-
await new Promise<void>(resolve => setTimeout(resolve, 250));
22+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
2323
await Sentry.flush();
2424
}
2525

dev-packages/node-integration-tests/suites/bun-runtime-metrics/scenario-opt-out.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Sentry.init({
99
transport: loggingTransport,
1010
integrations: [
1111
bunRuntimeMetricsIntegration({
12-
collectionIntervalMs: 100,
12+
collectionIntervalMs: 1000,
1313
collect: {
1414
cpuUtilization: false,
1515
cpuTime: false,
@@ -21,7 +21,7 @@ Sentry.init({
2121
});
2222

2323
async function run(): Promise<void> {
24-
await new Promise<void>(resolve => setTimeout(resolve, 250));
24+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
2525
await Sentry.flush();
2626
}
2727

dev-packages/node-integration-tests/suites/bun-runtime-metrics/scenario.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ Sentry.init({
99
transport: loggingTransport,
1010
integrations: [
1111
bunRuntimeMetricsIntegration({
12-
collectionIntervalMs: 100,
12+
collectionIntervalMs: 1000,
1313
}),
1414
],
1515
});
1616

1717
async function run(): Promise<void> {
18-
await new Promise<void>(resolve => setTimeout(resolve, 250));
18+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
1919
await Sentry.flush();
2020
}
2121

dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-all.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Sentry.init({
88
transport: loggingTransport,
99
integrations: [
1010
Sentry.nodeRuntimeMetricsIntegration({
11-
collectionIntervalMs: 100,
11+
collectionIntervalMs: 1000,
1212
collect: {
1313
cpuTime: true,
1414
memExternal: true,
@@ -22,7 +22,7 @@ Sentry.init({
2222
});
2323

2424
async function run(): Promise<void> {
25-
await new Promise<void>(resolve => setTimeout(resolve, 250));
25+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
2626
await Sentry.flush();
2727
}
2828

dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario-opt-out.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Sentry.init({
88
transport: loggingTransport,
99
integrations: [
1010
Sentry.nodeRuntimeMetricsIntegration({
11-
collectionIntervalMs: 100,
11+
collectionIntervalMs: 1000,
1212
collect: {
1313
cpuUtilization: false,
1414
cpuTime: false,
@@ -22,7 +22,7 @@ Sentry.init({
2222
});
2323

2424
async function run(): Promise<void> {
25-
await new Promise<void>(resolve => setTimeout(resolve, 250));
25+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
2626
await Sentry.flush();
2727
}
2828

dev-packages/node-integration-tests/suites/node-runtime-metrics/scenario.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ Sentry.init({
88
transport: loggingTransport,
99
integrations: [
1010
Sentry.nodeRuntimeMetricsIntegration({
11-
collectionIntervalMs: 100,
11+
collectionIntervalMs: 1000,
1212
}),
1313
],
1414
});
1515

1616
async function run(): Promise<void> {
1717
// Wait long enough for the collection interval to fire at least once.
18-
await new Promise<void>(resolve => setTimeout(resolve, 250));
18+
await new Promise<void>(resolve => setTimeout(resolve, 1100));
1919
await Sentry.flush();
2020
}
2121

packages/bun/src/integrations/bunRuntimeMetrics.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { performance } from 'perf_hooks';
22
import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core';
3-
import type { NodeRuntimeMetricsOptions } from '@sentry/node';
3+
import { _INTERNAL_normalizeCollectionInterval, type NodeRuntimeMetricsOptions } from '@sentry/node';
44

55
const INTEGRATION_NAME = 'BunRuntimeMetrics';
66
const DEFAULT_INTERVAL_MS = 30_000;
@@ -44,7 +44,9 @@ export interface BunRuntimeMetricsOptions {
4444
collect?: BunCollectOptions;
4545
/**
4646
* How often to collect metrics, in milliseconds.
47+
* Minimum allowed value is 1000ms.
4748
* @default 30000
49+
* @minimum 1000
4850
*/
4951
collectionIntervalMs?: number;
5052
}
@@ -62,7 +64,11 @@ export interface BunRuntimeMetricsOptions {
6264
* ```
6365
*/
6466
export const bunRuntimeMetricsIntegration = defineIntegration((options: BunRuntimeMetricsOptions = {}) => {
65-
const collectionIntervalMs = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS;
67+
const collectionIntervalMs = _INTERNAL_normalizeCollectionInterval(
68+
options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS,
69+
INTEGRATION_NAME,
70+
DEFAULT_INTERVAL_MS,
71+
);
6672
const collect = {
6773
// Default on
6874
cpuUtilization: true,

packages/bun/test/integrations/bunRuntimeMetrics.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,39 @@ describe('bunRuntimeMetricsIntegration', () => {
212212
expect(countSpy).not.toHaveBeenCalledWith('bun.runtime.process.uptime', expect.anything(), expect.anything());
213213
});
214214
});
215+
216+
describe('collectionIntervalMs minimum', () => {
217+
it('enforces minimum of 1000ms and warns', () => {
218+
const warnSpy = spyOn(globalThis.console, 'warn').mockImplementation(() => {});
219+
220+
const integration = bunRuntimeMetricsIntegration({ collectionIntervalMs: 100 });
221+
integration.setup();
222+
223+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('collectionIntervalMs'));
224+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('1000'));
225+
226+
// Should fire at minimum 1000ms, not at 100ms
227+
jest.advanceTimersByTime(100);
228+
expect(gaugeSpy).not.toHaveBeenCalled();
229+
230+
jest.advanceTimersByTime(900);
231+
expect(gaugeSpy).toHaveBeenCalled();
232+
});
233+
234+
it('falls back to default when NaN', () => {
235+
const warnSpy = spyOn(globalThis.console, 'warn').mockImplementation(() => {});
236+
237+
const integration = bunRuntimeMetricsIntegration({ collectionIntervalMs: NaN });
238+
integration.setup();
239+
240+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('collectionIntervalMs'));
241+
242+
// Should fire at the default 30000ms, not at 1000ms
243+
jest.advanceTimersByTime(1000);
244+
expect(gaugeSpy).not.toHaveBeenCalled();
245+
246+
jest.advanceTimersByTime(29_000);
247+
expect(gaugeSpy).toHaveBeenCalled();
248+
});
249+
});
215250
});

packages/deno/src/integrations/denoRuntimeMetrics.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface DenoRuntimeMetricsOptions {
2828
};
2929
/**
3030
* How often to collect metrics, in milliseconds.
31-
* Values below 1000ms are clamped to 1000ms.
31+
* Minimum allowed value is 1000ms.
3232
* @default 30000
3333
* @minimum 1000
3434
*/
@@ -49,13 +49,22 @@ export interface DenoRuntimeMetricsOptions {
4949
*/
5050
export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRuntimeMetricsOptions = {}) => {
5151
const rawInterval = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS;
52-
if (!Number.isFinite(rawInterval) || rawInterval < MIN_INTERVAL_MS) {
52+
let collectionIntervalMs: number;
53+
if (!Number.isFinite(rawInterval)) {
5354
// eslint-disable-next-line no-console
5455
console.warn(
55-
`[Sentry] denoRuntimeMetricsIntegration: collectionIntervalMs (${rawInterval}) is below the minimum of ${MIN_INTERVAL_MS}ms. Clamping to ${MIN_INTERVAL_MS}ms.`,
56+
`[Sentry] denoRuntimeMetricsIntegration: collectionIntervalMs (${rawInterval}) is invalid. Using default of ${DEFAULT_INTERVAL_MS}ms.`,
5657
);
58+
collectionIntervalMs = DEFAULT_INTERVAL_MS;
59+
} else if (rawInterval < MIN_INTERVAL_MS) {
60+
// eslint-disable-next-line no-console
61+
console.warn(
62+
`[Sentry] denoRuntimeMetricsIntegration: collectionIntervalMs (${rawInterval}) is below the minimum of ${MIN_INTERVAL_MS}ms. Using minimum of ${MIN_INTERVAL_MS}ms.`,
63+
);
64+
collectionIntervalMs = MIN_INTERVAL_MS;
65+
} else {
66+
collectionIntervalMs = rawInterval;
5767
}
58-
const collectionIntervalMs = Number.isFinite(rawInterval) ? Math.max(rawInterval, MIN_INTERVAL_MS) : MIN_INTERVAL_MS;
5968
const collect = {
6069
// Default on
6170
memRss: true,

0 commit comments

Comments
 (0)