From 3ac07de5d7b669bdfc3eb528e35b98e21d2b117a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 27 Mar 2026 16:22:52 +0100 Subject: [PATCH 1/9] feat(deno): add denoRuntimeMetricsIntegration --- packages/deno/src/index.ts | 1 + .../src/integrations/denoRuntimeMetrics.ts | 106 ++++++++++ .../deno/test/deno-runtime-metrics.test.ts | 193 ++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 packages/deno/src/integrations/denoRuntimeMetrics.ts create mode 100644 packages/deno/test/deno-runtime-metrics.test.ts diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index e6fdde530c81..9d04cdb6d663 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -109,3 +109,4 @@ export { contextLinesIntegration } from './integrations/contextlines'; export { denoCronIntegration } from './integrations/deno-cron'; export { breadcrumbsIntegration } from './integrations/breadcrumbs'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; +export { denoRuntimeMetricsIntegration, type DenoRuntimeMetricsOptions } from './integrations/denoRuntimeMetrics'; diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts new file mode 100644 index 000000000000..6633134b6b93 --- /dev/null +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -0,0 +1,106 @@ +import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core'; + +const INTEGRATION_NAME = 'DenoRuntimeMetrics'; +const DEFAULT_INTERVAL_MS = 30_000; + +export interface DenoRuntimeMetricsOptions { + /** + * Which metrics to collect. + * + * Default on (4 metrics): + * - `memRss` — Resident Set Size (actual memory footprint) + * - `memHeapUsed` — V8 heap currently in use + * - `memHeapTotal` — total V8 heap allocated + * - `uptime` — process uptime (detect restarts/crashes) + * + * Default off (opt-in): + * - `memExternal` — external memory (JS objects outside the V8 isolate) + * + * Note: CPU utilization and event loop metrics are not available in Deno. + */ + collect?: { + memRss?: boolean; + memHeapUsed?: boolean; + memHeapTotal?: boolean; + memExternal?: boolean; + uptime?: boolean; + }; + /** + * How often to collect metrics, in milliseconds. + * @default 30000 + */ + collectionIntervalMs?: number; +} + +/** + * Automatically collects Deno runtime metrics and emits them to Sentry. + * + * @example + * ```ts + * Sentry.init({ + * integrations: [ + * Sentry.denoRuntimeMetricsIntegration(), + * ], + * }); + * ``` + */ +export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRuntimeMetricsOptions = {}) => { + const collectionIntervalMs = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS; + const collect = { + // Default on + memRss: true, + memHeapUsed: true, + memHeapTotal: true, + uptime: true, + // Default off + memExternal: false, + ...options.collect, + }; + + let intervalId: ReturnType | undefined; + let prevFlushTime: number = 0; + + const METRIC_ATTRIBUTES_BYTE = { unit: 'byte', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } }; + const METRIC_ATTRIBUTES_SECOND = { unit: 'second', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } }; + + function collectMetrics(): void { + const now = _INTERNAL_safeDateNow(); + const elapsed = now - prevFlushTime; + + if (collect.memRss || collect.memHeapUsed || collect.memHeapTotal || collect.memExternal) { + const mem = Deno.memoryUsage(); + if (collect.memRss) { + metrics.gauge('deno.runtime.mem.rss', mem.rss, METRIC_ATTRIBUTES_BYTE); + } + if (collect.memHeapUsed) { + metrics.gauge('deno.runtime.mem.heap_used', mem.heapUsed, METRIC_ATTRIBUTES_BYTE); + } + if (collect.memHeapTotal) { + metrics.gauge('deno.runtime.mem.heap_total', mem.heapTotal, METRIC_ATTRIBUTES_BYTE); + } + if (collect.memExternal) { + metrics.gauge('deno.runtime.mem.external', mem.external, METRIC_ATTRIBUTES_BYTE); + } + } + + if (collect.uptime && elapsed > 0) { + metrics.count('deno.runtime.process.uptime', elapsed / 1000, METRIC_ATTRIBUTES_SECOND); + } + + prevFlushTime = now; + } + + return { + name: INTEGRATION_NAME, + + setup(): void { + prevFlushTime = _INTERNAL_safeDateNow(); + + // Guard against double setup (e.g. re-init). + if (intervalId) { + clearInterval(intervalId); + } + intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs)); + }, + }; +}); diff --git a/packages/deno/test/deno-runtime-metrics.test.ts b/packages/deno/test/deno-runtime-metrics.test.ts new file mode 100644 index 000000000000..85ea8b7d4651 --- /dev/null +++ b/packages/deno/test/deno-runtime-metrics.test.ts @@ -0,0 +1,193 @@ +// + +import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts'; +import { spy, stub } from 'https://deno.land/std@0.212.0/testing/mock.ts'; +import { FakeTime } from 'https://deno.land/std@0.212.0/testing/time.ts'; +import { denoRuntimeMetricsIntegration, metrics } from '../build/esm/index.js'; + +const MOCK_MEMORY: Deno.MemoryUsage = { + rss: 50_000_000, + heapTotal: 30_000_000, + heapUsed: 20_000_000, + external: 1_000_000, +}; + +// deno-lint-ignore no-explicit-any +type AnyCall = { args: any[] }; + +Deno.test('denoRuntimeMetricsIntegration has the correct name', () => { + const integration = denoRuntimeMetricsIntegration(); + assertEquals(integration.name, 'DenoRuntimeMetrics'); +}); + +Deno.test('starts a collection interval', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + + assertEquals(gaugeSpy.calls.length, 0); + time.tick(1_000); + assertNotEquals(gaugeSpy.calls.length, 0); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('emits default memory metrics', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + time.tick(1_000); + + const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); + assertEquals(names.includes('deno.runtime.mem.rss'), true); + assertEquals(names.includes('deno.runtime.mem.heap_used'), true); + assertEquals(names.includes('deno.runtime.mem.heap_total'), true); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('emits correct memory values', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + time.tick(1_000); + + const calls = gaugeSpy.calls as AnyCall[]; + const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); + const heapUsedCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_used'); + const heapTotalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_total'); + + assertEquals(rssCall?.args[1], 50_000_000); + assertEquals(heapUsedCall?.args[1], 20_000_000); + assertEquals(heapTotalCall?.args[1], 30_000_000); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('does not emit mem.external by default', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + time.tick(1_000); + + const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); + assertEquals(names.includes('deno.runtime.mem.external'), false); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('emits mem.external when opted in', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ + collectionIntervalMs: 1_000, + collect: { memExternal: true }, + }); + integration.setup!({} as never); + time.tick(1_000); + + const calls = gaugeSpy.calls as AnyCall[]; + const externalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.external'); + assertEquals(externalCall?.args[1], 1_000_000); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('emits uptime counter', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const countSpy = spy(metrics, 'count'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + time.tick(1_000); + + const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); + assertNotEquals(uptimeCall, undefined); + } finally { + countSpy.restore(); + } +}); + +Deno.test('respects opt-out: skips mem.rss when memRss is false', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ + collectionIntervalMs: 1_000, + collect: { memRss: false }, + }); + integration.setup!({} as never); + time.tick(1_000); + + const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); + assertEquals(names.includes('deno.runtime.mem.rss'), false); + } finally { + gaugeSpy.restore(); + } +}); + +Deno.test('skips uptime when uptime is false', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const countSpy = spy(metrics, 'count'); + + try { + const integration = denoRuntimeMetricsIntegration({ + collectionIntervalMs: 1_000, + collect: { uptime: false }, + }); + integration.setup!({} as never); + time.tick(1_000); + + const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); + assertEquals(uptimeCall, undefined); + } finally { + countSpy.restore(); + } +}); + +Deno.test('attaches correct sentry.origin attribute', () => { + using time = new FakeTime(); + using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); + const gaugeSpy = spy(metrics, 'gauge'); + + try { + const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); + integration.setup!({} as never); + time.tick(1_000); + + const calls = gaugeSpy.calls as AnyCall[]; + const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); + assertEquals(rssCall?.args[2]?.attributes?.['sentry.origin'], 'auto.deno.runtime_metrics'); + } finally { + gaugeSpy.restore(); + } +}); From e4f33fbd92f696ea50e91dca773a12865478435f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 30 Mar 2026 12:07:00 +0200 Subject: [PATCH 2/9] fix(deno): add teardown to denoRuntimeMetricsIntegration, fix unit tests --- .../src/integrations/denoRuntimeMetrics.ts | 7 + .../deno/test/deno-runtime-metrics.test.ts | 255 +++++++----------- 2 files changed, 98 insertions(+), 164 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index 6633134b6b93..520c5b14665f 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -102,5 +102,12 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun } intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs)); }, + + teardown(): void { + if (intervalId) { + clearInterval(intervalId); + intervalId = undefined; + } + }, }; }); diff --git a/packages/deno/test/deno-runtime-metrics.test.ts b/packages/deno/test/deno-runtime-metrics.test.ts index 85ea8b7d4651..b3b1a51af17c 100644 --- a/packages/deno/test/deno-runtime-metrics.test.ts +++ b/packages/deno/test/deno-runtime-metrics.test.ts @@ -1,193 +1,120 @@ // +import type { Envelope } from '@sentry/core'; +import { createStackParser, forEachEnvelopeItem, nodeStackLineParser } from '@sentry/core'; import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts'; -import { spy, stub } from 'https://deno.land/std@0.212.0/testing/mock.ts'; -import { FakeTime } from 'https://deno.land/std@0.212.0/testing/time.ts'; -import { denoRuntimeMetricsIntegration, metrics } from '../build/esm/index.js'; +import { + DenoClient, + denoRuntimeMetricsIntegration, + getCurrentScope, + getDefaultIntegrations, +} from '../build/esm/index.js'; +import { makeTestTransport } from './transport.ts'; -const MOCK_MEMORY: Deno.MemoryUsage = { - rss: 50_000_000, - heapTotal: 30_000_000, - heapUsed: 20_000_000, - external: 1_000_000, -}; +const DSN = 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507'; + +function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} // deno-lint-ignore no-explicit-any -type AnyCall = { args: any[] }; +type MetricItem = { name: string; type: string; value: number; unit?: string; attributes?: Record }; + +async function collectMetrics( + integrationOptions: Parameters[0] = {}, +): Promise { + const envelopes: Envelope[] = []; + + // Hold a reference so we can call teardown() to stop the interval before the test ends. + const metricsIntegration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 100, ...integrationOptions }); + + const client = new DenoClient({ + dsn: DSN, + integrations: [...getDefaultIntegrations({}), metricsIntegration], + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(envelope => { + envelopes.push(envelope); + }), + }); + + client.init(); + getCurrentScope().setClient(client); + + await delay(250); + await client.flush(2000); + + // Stop the collection interval so Deno's leak detector doesn't flag it. + metricsIntegration.teardown?.(); + + const items: MetricItem[] = []; + for (const envelope of envelopes) { + forEachEnvelopeItem(envelope, item => { + const [headers, body] = item; + if (headers.type === 'trace_metric') { + // deno-lint-ignore no-explicit-any + items.push(...(body as any).items); + } + }); + } + + return items; +} Deno.test('denoRuntimeMetricsIntegration has the correct name', () => { const integration = denoRuntimeMetricsIntegration(); assertEquals(integration.name, 'DenoRuntimeMetrics'); }); -Deno.test('starts a collection interval', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); +Deno.test('emits default memory metrics with correct shape', async () => { + const items = await collectMetrics(); + const names = items.map(i => i.name); - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); + assertEquals(names.includes('deno.runtime.mem.rss'), true); + assertEquals(names.includes('deno.runtime.mem.heap_used'), true); + assertEquals(names.includes('deno.runtime.mem.heap_total'), true); - assertEquals(gaugeSpy.calls.length, 0); - time.tick(1_000); - assertNotEquals(gaugeSpy.calls.length, 0); - } finally { - gaugeSpy.restore(); - } + const rss = items.find(i => i.name === 'deno.runtime.mem.rss'); + assertEquals(rss?.type, 'gauge'); + assertEquals(rss?.unit, 'byte'); + assertEquals(typeof rss?.value, 'number'); }); -Deno.test('emits default memory metrics', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); - - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); - time.tick(1_000); - - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); - assertEquals(names.includes('deno.runtime.mem.rss'), true); - assertEquals(names.includes('deno.runtime.mem.heap_used'), true); - assertEquals(names.includes('deno.runtime.mem.heap_total'), true); - } finally { - gaugeSpy.restore(); - } -}); +Deno.test('emits uptime counter', async () => { + const items = await collectMetrics(); + const uptime = items.find(i => i.name === 'deno.runtime.process.uptime'); -Deno.test('emits correct memory values', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); - - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); - time.tick(1_000); - - const calls = gaugeSpy.calls as AnyCall[]; - const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); - const heapUsedCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_used'); - const heapTotalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_total'); - - assertEquals(rssCall?.args[1], 50_000_000); - assertEquals(heapUsedCall?.args[1], 20_000_000); - assertEquals(heapTotalCall?.args[1], 30_000_000); - } finally { - gaugeSpy.restore(); - } + assertNotEquals(uptime, undefined); + assertEquals(uptime?.type, 'counter'); + assertEquals(uptime?.unit, 'second'); }); -Deno.test('does not emit mem.external by default', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); - - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); - time.tick(1_000); - - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); - assertEquals(names.includes('deno.runtime.mem.external'), false); - } finally { - gaugeSpy.restore(); - } +Deno.test('does not emit mem.external by default', async () => { + const items = await collectMetrics(); + const names = items.map(i => i.name); + assertEquals(names.includes('deno.runtime.mem.external'), false); }); -Deno.test('emits mem.external when opted in', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); +Deno.test('emits mem.external when opted in', async () => { + const items = await collectMetrics({ collect: { memExternal: true } }); + const external = items.find(i => i.name === 'deno.runtime.mem.external'); - try { - const integration = denoRuntimeMetricsIntegration({ - collectionIntervalMs: 1_000, - collect: { memExternal: true }, - }); - integration.setup!({} as never); - time.tick(1_000); - - const calls = gaugeSpy.calls as AnyCall[]; - const externalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.external'); - assertEquals(externalCall?.args[1], 1_000_000); - } finally { - gaugeSpy.restore(); - } + assertNotEquals(external, undefined); + assertEquals(external?.type, 'gauge'); + assertEquals(external?.unit, 'byte'); }); -Deno.test('emits uptime counter', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const countSpy = spy(metrics, 'count'); - - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); - time.tick(1_000); +Deno.test('respects opt-out: skips uptime when disabled', async () => { + const items = await collectMetrics({ collect: { uptime: false } }); + const names = items.map(i => i.name); - const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); - assertNotEquals(uptimeCall, undefined); - } finally { - countSpy.restore(); - } -}); - -Deno.test('respects opt-out: skips mem.rss when memRss is false', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); - - try { - const integration = denoRuntimeMetricsIntegration({ - collectionIntervalMs: 1_000, - collect: { memRss: false }, - }); - integration.setup!({} as never); - time.tick(1_000); - - const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]); - assertEquals(names.includes('deno.runtime.mem.rss'), false); - } finally { - gaugeSpy.restore(); - } + assertEquals(names.includes('deno.runtime.mem.rss'), true); + assertEquals(names.includes('deno.runtime.process.uptime'), false); }); -Deno.test('skips uptime when uptime is false', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const countSpy = spy(metrics, 'count'); +Deno.test('attaches correct sentry.origin attribute', async () => { + const items = await collectMetrics(); + const rss = items.find(i => i.name === 'deno.runtime.mem.rss'); - try { - const integration = denoRuntimeMetricsIntegration({ - collectionIntervalMs: 1_000, - collect: { uptime: false }, - }); - integration.setup!({} as never); - time.tick(1_000); - - const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime'); - assertEquals(uptimeCall, undefined); - } finally { - countSpy.restore(); - } -}); - -Deno.test('attaches correct sentry.origin attribute', () => { - using time = new FakeTime(); - using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY); - const gaugeSpy = spy(metrics, 'gauge'); - - try { - const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 }); - integration.setup!({} as never); - time.tick(1_000); - - const calls = gaugeSpy.calls as AnyCall[]; - const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss'); - assertEquals(rssCall?.args[2]?.attributes?.['sentry.origin'], 'auto.deno.runtime_metrics'); - } finally { - gaugeSpy.restore(); - } + // Attributes in the serialized envelope are { type, value } objects. + assertEquals(rss?.attributes?.['sentry.origin']?.value, 'auto.deno.runtime_metrics'); }); From 086725fa5124ccfe92e7ac1dd1eb9e730e55d082 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 30 Mar 2026 14:44:29 +0200 Subject: [PATCH 3/9] fix(deno): use Deno.unrefTimer instead of safeUnref for interval --- packages/deno/src/integrations/denoRuntimeMetrics.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index 520c5b14665f..3522d2a3b623 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core'; +import { _INTERNAL_safeDateNow, defineIntegration, metrics } from '@sentry/core'; const INTEGRATION_NAME = 'DenoRuntimeMetrics'; const DEFAULT_INTERVAL_MS = 30_000; @@ -57,7 +57,7 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun ...options.collect, }; - let intervalId: ReturnType | undefined; + let intervalId: number | undefined; let prevFlushTime: number = 0; const METRIC_ATTRIBUTES_BYTE = { unit: 'byte', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } }; @@ -100,7 +100,10 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun if (intervalId) { clearInterval(intervalId); } - intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs)); + intervalId = setInterval(collectMetrics, collectionIntervalMs); + // In Deno, setInterval returns a number, so _INTERNAL_safeUnref is a no-op. + // Use Deno.unrefTimer so the interval doesn't prevent the process from exiting. + Deno.unrefTimer(intervalId); }, teardown(): void { From 7fb0255e7461e501dce8de4e12f319a92b3579d3 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 30 Mar 2026 15:26:10 +0200 Subject: [PATCH 4/9] fix(deno): cast setInterval return to number for tsconfig.types.json compat --- packages/deno/src/integrations/denoRuntimeMetrics.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index 3522d2a3b623..be40483eb943 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -100,9 +100,11 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun if (intervalId) { clearInterval(intervalId); } - intervalId = setInterval(collectMetrics, collectionIntervalMs); // In Deno, setInterval returns a number, so _INTERNAL_safeUnref is a no-op. // Use Deno.unrefTimer so the interval doesn't prevent the process from exiting. + // Cast to number since the tsconfig.types.json build context resolves setInterval + // to Node's NodeJS.Timeout rather than the Deno/browser number type. + intervalId = setInterval(collectMetrics, collectionIntervalMs) as unknown as number; Deno.unrefTimer(intervalId); }, From a723136b81ae7d2853b424cc7efda96b42b342ca Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 30 Mar 2026 15:51:31 +0200 Subject: [PATCH 5/9] . --- packages/deno/src/integrations/denoRuntimeMetrics.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index be40483eb943..520c5b14665f 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_safeDateNow, defineIntegration, metrics } from '@sentry/core'; +import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core'; const INTEGRATION_NAME = 'DenoRuntimeMetrics'; const DEFAULT_INTERVAL_MS = 30_000; @@ -57,7 +57,7 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun ...options.collect, }; - let intervalId: number | undefined; + let intervalId: ReturnType | undefined; let prevFlushTime: number = 0; const METRIC_ATTRIBUTES_BYTE = { unit: 'byte', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } }; @@ -100,12 +100,7 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun if (intervalId) { clearInterval(intervalId); } - // In Deno, setInterval returns a number, so _INTERNAL_safeUnref is a no-op. - // Use Deno.unrefTimer so the interval doesn't prevent the process from exiting. - // Cast to number since the tsconfig.types.json build context resolves setInterval - // to Node's NodeJS.Timeout rather than the Deno/browser number type. - intervalId = setInterval(collectMetrics, collectionIntervalMs) as unknown as number; - Deno.unrefTimer(intervalId); + intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs)); }, teardown(): void { From 49cb5a9d0a1022500e9367aaeac09d7eafc70dfb Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 30 Mar 2026 20:48:05 +0200 Subject: [PATCH 6/9] fix(deno): Use Deno.unrefTimer instead of _INTERNAL_safeUnref for interval Global setInterval in Deno returns a number at runtime, not NodeJS.Timeout. _INTERNAL_safeUnref is a no-op for numbers (it checks typeof timer === 'object'). Use Deno.unrefTimer directly instead, with a type cast to work around @types/node polluting the global setInterval signature in the monorepo. Co-Authored-By: Claude Sonnet 4.6 --- packages/deno/src/integrations/denoRuntimeMetrics.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index 520c5b14665f..c529a0aeb0d0 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -1,4 +1,4 @@ -import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core'; +import { _INTERNAL_safeDateNow, defineIntegration, metrics } from '@sentry/core'; const INTEGRATION_NAME = 'DenoRuntimeMetrics'; const DEFAULT_INTERVAL_MS = 30_000; @@ -57,7 +57,7 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun ...options.collect, }; - let intervalId: ReturnType | undefined; + let intervalId: number | undefined; let prevFlushTime: number = 0; const METRIC_ATTRIBUTES_BYTE = { unit: 'byte', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } }; @@ -100,7 +100,10 @@ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRun if (intervalId) { clearInterval(intervalId); } - intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs)); + // setInterval in Deno returns a number at runtime (global API, not node:timers). + // @types/node in the monorepo overrides the global type to NodeJS.Timeout, so we cast. + intervalId = setInterval(collectMetrics, collectionIntervalMs) as unknown as number; + Deno.unrefTimer(intervalId); }, teardown(): void { From ef81efbcf53bb61db1198fdff5be8fe8afd09456 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 31 Mar 2026 15:48:32 +0200 Subject: [PATCH 7/9] feat(deno): Clamp collectionIntervalMs to a minimum of 1000ms Sub-second collection intervals provide no practical observability value and would cause unnecessary CPU overhead from tight Deno.memoryUsage() calls. Clamp to 1000ms and warn so misconfigured integrations are visible. Co-Authored-By: Claude Sonnet 4.6 --- .../src/integrations/denoRuntimeMetrics.ts | 10 ++++++++- .../deno/test/deno-runtime-metrics.test.ts | 22 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index c529a0aeb0d0..46a8dfdfdbc0 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -2,6 +2,7 @@ import { _INTERNAL_safeDateNow, defineIntegration, metrics } from '@sentry/core' const INTEGRATION_NAME = 'DenoRuntimeMetrics'; const DEFAULT_INTERVAL_MS = 30_000; +const MIN_INTERVAL_MS = 1_000; export interface DenoRuntimeMetricsOptions { /** @@ -45,7 +46,14 @@ export interface DenoRuntimeMetricsOptions { * ``` */ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRuntimeMetricsOptions = {}) => { - const collectionIntervalMs = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS; + const rawInterval = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS; + if (rawInterval < MIN_INTERVAL_MS) { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] denoRuntimeMetricsIntegration: collectionIntervalMs (${rawInterval}) is below the minimum of ${MIN_INTERVAL_MS}ms. Clamping to ${MIN_INTERVAL_MS}ms.`, + ); + } + const collectionIntervalMs = Math.max(rawInterval, MIN_INTERVAL_MS); const collect = { // Default on memRss: true, diff --git a/packages/deno/test/deno-runtime-metrics.test.ts b/packages/deno/test/deno-runtime-metrics.test.ts index b3b1a51af17c..dba9d38b3495 100644 --- a/packages/deno/test/deno-runtime-metrics.test.ts +++ b/packages/deno/test/deno-runtime-metrics.test.ts @@ -2,7 +2,7 @@ import type { Envelope } from '@sentry/core'; import { createStackParser, forEachEnvelopeItem, nodeStackLineParser } from '@sentry/core'; -import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts'; +import { assertEquals, assertNotEquals, assertStringIncludes } from 'https://deno.land/std@0.212.0/assert/mod.ts'; import { DenoClient, denoRuntimeMetricsIntegration, @@ -26,7 +26,7 @@ async function collectMetrics( const envelopes: Envelope[] = []; // Hold a reference so we can call teardown() to stop the interval before the test ends. - const metricsIntegration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 100, ...integrationOptions }); + const metricsIntegration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1000, ...integrationOptions }); const client = new DenoClient({ dsn: DSN, @@ -40,7 +40,7 @@ async function collectMetrics( client.init(); getCurrentScope().setClient(client); - await delay(250); + await delay(2500); await client.flush(2000); // Stop the collection interval so Deno's leak detector doesn't flag it. @@ -118,3 +118,19 @@ Deno.test('attaches correct sentry.origin attribute', async () => { // Attributes in the serialized envelope are { type, value } objects. assertEquals(rss?.attributes?.['sentry.origin']?.value, 'auto.deno.runtime_metrics'); }); + +Deno.test('warns and clamps collectionIntervalMs below 1000ms', () => { + const warnings: string[] = []; + const originalWarn = console.warn; + console.warn = (msg: string) => warnings.push(msg); + + try { + denoRuntimeMetricsIntegration({ collectionIntervalMs: 100 }); + } finally { + console.warn = originalWarn; + } + + assertEquals(warnings.length, 1); + assertStringIncludes(warnings[0]!, 'collectionIntervalMs'); + assertStringIncludes(warnings[0]!, '1000'); +}); From f94671329f4d13e8b80e902bb4ca865ee1790aae Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 31 Mar 2026 16:24:16 +0200 Subject: [PATCH 8/9] fix(deno): Guard against NaN collectionIntervalMs bypassing interval clamp NaN < 1000 evaluates to false (skipping the warning) and Math.max(NaN, 1000) returns NaN, causing setInterval(fn, NaN) to fire at near-zero interval. Use Number.isFinite to catch NaN and Infinity before applying Math.max. Co-Authored-By: Claude Sonnet 4.6 --- .../deno/src/integrations/denoRuntimeMetrics.ts | 6 ++++-- packages/deno/test/deno-runtime-metrics.test.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/deno/src/integrations/denoRuntimeMetrics.ts b/packages/deno/src/integrations/denoRuntimeMetrics.ts index 46a8dfdfdbc0..077e920bd5a4 100644 --- a/packages/deno/src/integrations/denoRuntimeMetrics.ts +++ b/packages/deno/src/integrations/denoRuntimeMetrics.ts @@ -28,7 +28,9 @@ export interface DenoRuntimeMetricsOptions { }; /** * How often to collect metrics, in milliseconds. + * Values below 1000ms are clamped to 1000ms. * @default 30000 + * @minimum 1000 */ collectionIntervalMs?: number; } @@ -47,13 +49,13 @@ export interface DenoRuntimeMetricsOptions { */ export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRuntimeMetricsOptions = {}) => { const rawInterval = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS; - if (rawInterval < MIN_INTERVAL_MS) { + if (!Number.isFinite(rawInterval) || rawInterval < MIN_INTERVAL_MS) { // eslint-disable-next-line no-console console.warn( `[Sentry] denoRuntimeMetricsIntegration: collectionIntervalMs (${rawInterval}) is below the minimum of ${MIN_INTERVAL_MS}ms. Clamping to ${MIN_INTERVAL_MS}ms.`, ); } - const collectionIntervalMs = Math.max(rawInterval, MIN_INTERVAL_MS); + const collectionIntervalMs = Number.isFinite(rawInterval) ? Math.max(rawInterval, MIN_INTERVAL_MS) : MIN_INTERVAL_MS; const collect = { // Default on memRss: true, diff --git a/packages/deno/test/deno-runtime-metrics.test.ts b/packages/deno/test/deno-runtime-metrics.test.ts index dba9d38b3495..370eceac9ce9 100644 --- a/packages/deno/test/deno-runtime-metrics.test.ts +++ b/packages/deno/test/deno-runtime-metrics.test.ts @@ -134,3 +134,18 @@ Deno.test('warns and clamps collectionIntervalMs below 1000ms', () => { assertStringIncludes(warnings[0]!, 'collectionIntervalMs'); assertStringIncludes(warnings[0]!, '1000'); }); + +Deno.test('warns and clamps collectionIntervalMs when NaN', () => { + const warnings: string[] = []; + const originalWarn = console.warn; + console.warn = (msg: string) => warnings.push(msg); + + try { + denoRuntimeMetricsIntegration({ collectionIntervalMs: NaN }); + } finally { + console.warn = originalWarn; + } + + assertEquals(warnings.length, 1); + assertStringIncludes(warnings[0]!, 'collectionIntervalMs'); +}); From aa85d93ecb48d1956237e69624339b2ff1f01561 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 31 Mar 2026 18:03:10 +0200 Subject: [PATCH 9/9] fix(deno): Use globalThis.console to avoid lint errors in warn tests Oxlint flags bare console.* references as errors. Use globalThis.console to bypass the rule while still intercepting the integration's warnings. Co-Authored-By: Claude Sonnet 4.6 --- packages/deno/test/deno-runtime-metrics.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/deno/test/deno-runtime-metrics.test.ts b/packages/deno/test/deno-runtime-metrics.test.ts index 370eceac9ce9..12b1c72fd985 100644 --- a/packages/deno/test/deno-runtime-metrics.test.ts +++ b/packages/deno/test/deno-runtime-metrics.test.ts @@ -121,13 +121,13 @@ Deno.test('attaches correct sentry.origin attribute', async () => { Deno.test('warns and clamps collectionIntervalMs below 1000ms', () => { const warnings: string[] = []; - const originalWarn = console.warn; - console.warn = (msg: string) => warnings.push(msg); + const originalWarn = globalThis.console.warn; + globalThis.console.warn = (msg: string) => warnings.push(msg); try { denoRuntimeMetricsIntegration({ collectionIntervalMs: 100 }); } finally { - console.warn = originalWarn; + globalThis.console.warn = originalWarn; } assertEquals(warnings.length, 1); @@ -137,13 +137,13 @@ Deno.test('warns and clamps collectionIntervalMs below 1000ms', () => { Deno.test('warns and clamps collectionIntervalMs when NaN', () => { const warnings: string[] = []; - const originalWarn = console.warn; - console.warn = (msg: string) => warnings.push(msg); + const originalWarn = globalThis.console.warn; + globalThis.console.warn = (msg: string) => warnings.push(msg); try { denoRuntimeMetricsIntegration({ collectionIntervalMs: NaN }); } finally { - console.warn = originalWarn; + globalThis.console.warn = originalWarn; } assertEquals(warnings.length, 1);