Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
106 changes: 106 additions & 0 deletions packages/deno/src/integrations/denoRuntimeMetrics.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setInterval> | 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));
},
};
});
193 changes: 193 additions & 0 deletions packages/deno/test/deno-runtime-metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// <reference lib="deno.ns" />

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();
}
});
Loading