diff --git a/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx b/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx
index a0bdd087816e1e..93d188e8dd2d67 100644
--- a/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx
+++ b/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx
@@ -10,6 +10,7 @@ import {useHasFirstSpan} from 'sentry/views/insights/common/queries/useHasFirstS
import {useOnboardingProject} from 'sentry/views/insights/common/queries/useOnboardingProject';
import {Onboarding as AgentOnboarding} from 'sentry/views/insights/pages/agents/onboarding';
import {Onboarding as MCPOnboarding} from 'sentry/views/insights/pages/mcp/onboarding';
+import {NodeRuntimeMetricsOnboarding} from 'sentry/views/insights/pages/nodeRuntime/onboarding';
import {ModuleName} from 'sentry/views/insights/types';
import {LegacyOnboarding} from 'sentry/views/performance/onboarding';
@@ -84,6 +85,10 @@ export function PrebuiltDashboardOnboardingGate({
return ;
}
+ if (onboarding.componentId === 'node-runtime-metrics') {
+ return ;
+ }
+
return ;
}
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs.tsx b/static/app/views/dashboards/utils/prebuiltConfigs.tsx
index f71882337a0c97..31bfbbe2047240 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs.tsx
+++ b/static/app/views/dashboards/utils/prebuiltConfigs.tsx
@@ -21,6 +21,7 @@ import {MOBILE_VITALS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebu
import {MOBILE_VITALS_SCREEN_LOADS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenLoads';
import {MOBILE_VITALS_SCREEN_RENDERING_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/mobileVitals/screenRendering';
import {NEXTJS_FRONTEND_OVERVIEW_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/nextJsOverview/nextJsOverview';
+import {NODE_RUNTIME_METRICS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics';
import {QUERIES_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/queries';
import {QUERIES_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/queryDetails';
import {QUEUE_DETAILS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/queues/queueDetails';
@@ -59,6 +60,7 @@ export enum PrebuiltDashboardId {
BACKEND_QUEUES = 26,
BACKEND_QUEUE_SUMMARY = 27,
BACKEND_CACHES = 28,
+ NODE_RUNTIME_METRICS = 29,
}
/** Boolean flags on Project that indicate whether telemetry data has been received. */
@@ -75,9 +77,9 @@ type OnboardingConfig =
requiredProjectFlags?: ProjectTelemetryFlag[];
}
| {
- componentId: 'agent-monitoring' | 'mcp';
+ componentId: 'agent-monitoring' | 'mcp' | 'node-runtime-metrics';
requiredProjectFlags: ProjectTelemetryFlag[];
- // Custom onboarding component (AI Agents, MCP)
+ // Custom onboarding component (AI Agents, MCP, Node.js Runtime Metrics)
type: 'custom';
}
| {
@@ -127,4 +129,5 @@ export const PREBUILT_DASHBOARDS: Record
[PrebuiltDashboardId.BACKEND_QUEUES]: QUEUES_PREBUILT_CONFIG,
[PrebuiltDashboardId.BACKEND_QUEUE_SUMMARY]: QUEUE_DETAILS_PREBUILT_CONFIG,
[PrebuiltDashboardId.BACKEND_CACHES]: CACHES_PREBUILT_CONFIG,
+ [PrebuiltDashboardId.NODE_RUNTIME_METRICS]: NODE_RUNTIME_METRICS_PREBUILT_CONFIG,
};
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts
new file mode 100644
index 00000000000000..b8e8d18bd842c7
--- /dev/null
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts
@@ -0,0 +1,214 @@
+import {t} from 'sentry/locale';
+import {DurationUnit, SizeUnit} from 'sentry/utils/discover/fields';
+import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
+import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings';
+import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
+import {traceMetricField} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField';
+import {SpanFields} from 'sentry/views/insights/types';
+
+const INTERVAL = '5m';
+
+const KPI_WIDGETS = spaceWidgetsEquallyOnRow(
+ [
+ {
+ id: 'node-runtime-event-loop-utilization-kpi',
+ title: t('Event Loop Utilization'),
+ description: t(
+ 'Average fraction of time the Node.js event loop is active (0-100%) across the selected time range. High utilization means less capacity to handle new work and may indicate CPU-bound processing or blocking operations.'
+ ),
+ displayType: DisplayType.BIG_NUMBER,
+ widgetType: WidgetType.TRACEMETRICS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ traceMetricField('avg', 'node.runtime.event_loop.utilization', 'gauge', null),
+ ],
+ aggregates: [
+ traceMetricField('avg', 'node.runtime.event_loop.utilization', 'gauge', null),
+ ],
+ columns: [],
+ conditions: '',
+ orderby: '',
+ },
+ ],
+ },
+ {
+ id: 'node-runtime-cpu-utilization-kpi',
+ title: t('CPU Utilization'),
+ description: t(
+ 'Average CPU usage across all cores over the selected time range. Values above 1.0 (100%) are possible on multi-core systems. Sustained high utilization may indicate compute-bound workloads or insufficient scaling.'
+ ),
+ displayType: DisplayType.BIG_NUMBER,
+ widgetType: WidgetType.TRACEMETRICS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null),
+ ],
+ aggregates: [
+ traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null),
+ ],
+ columns: [],
+ conditions: '',
+ orderby: '',
+ },
+ ],
+ },
+ {
+ id: 'node-runtime-process-uptime-kpi',
+ title: t('Process Uptime'),
+ description: t(
+ 'Total process uptime summed across instances. Sudden resets indicate process crashes or restarts. Useful for detecting instability and correlating with deployment events.'
+ ),
+ displayType: DisplayType.BIG_NUMBER,
+ widgetType: WidgetType.TRACEMETRICS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ traceMetricField(
+ 'sum',
+ 'node.runtime.process.uptime',
+ 'counter',
+ DurationUnit.SECOND
+ ),
+ ],
+ aggregates: [
+ traceMetricField(
+ 'sum',
+ 'node.runtime.process.uptime',
+ 'counter',
+ DurationUnit.SECOND
+ ),
+ ],
+ columns: [],
+ conditions: '',
+ orderby: '',
+ },
+ ],
+ },
+ ],
+ 0,
+ {h: 1, minH: 1}
+);
+
+const MEMORY_WIDGETS = spaceWidgetsEquallyOnRow(
+ [
+ {
+ id: 'node-runtime-memory-usage',
+ title: t('Memory Usage'),
+ description: t(
+ 'Resident Set Size (RSS, total memory footprint), V8 heap total (allocated), and heap used (in-use). Growing RSS without matching heap growth may indicate native memory leaks. Heap used approaching heap total triggers more frequent garbage collection.'
+ ),
+ displayType: DisplayType.AREA,
+ widgetType: WidgetType.TRACEMETRICS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ traceMetricField('avg', 'node.runtime.mem.rss', 'gauge', SizeUnit.BYTE),
+ traceMetricField(
+ 'avg',
+ 'node.runtime.mem.heap_total',
+ 'gauge',
+ SizeUnit.BYTE
+ ),
+ traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', SizeUnit.BYTE),
+ ],
+ aggregates: [
+ traceMetricField('avg', 'node.runtime.mem.rss', 'gauge', SizeUnit.BYTE),
+ traceMetricField(
+ 'avg',
+ 'node.runtime.mem.heap_total',
+ 'gauge',
+ SizeUnit.BYTE
+ ),
+ traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', SizeUnit.BYTE),
+ ],
+ columns: [],
+ conditions: '',
+ orderby: '',
+ },
+ ],
+ },
+ ],
+ 1
+);
+
+const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow(
+ [
+ {
+ id: 'node-runtime-cpu-utilization-over-time',
+ title: t('CPU Utilization Over Time'),
+ description: t(
+ 'CPU utilization trend over time. Correlate spikes with deployments, traffic changes, or event loop delay increases to identify compute-bound bottlenecks.'
+ ),
+ displayType: DisplayType.LINE,
+ widgetType: WidgetType.TRACEMETRICS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null),
+ ],
+ aggregates: [
+ traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null),
+ ],
+ columns: [],
+ conditions: '',
+ orderby: '',
+ },
+ ],
+ },
+ {
+ id: 'node-runtime-http-request-duration',
+ title: t('HTTP Request Duration'),
+ description: t(
+ 'Server-side HTTP request latency (p50 and p95). Compare with event loop delay and CPU utilization to determine if slow responses are caused by runtime bottlenecks or application logic.'
+ ),
+ displayType: DisplayType.LINE,
+ widgetType: WidgetType.SPANS,
+ interval: INTERVAL,
+ queries: [
+ {
+ name: '',
+ fields: [
+ `p50(${SpanFields.SPAN_DURATION})`,
+ `p95(${SpanFields.SPAN_DURATION})`,
+ ],
+ aggregates: [
+ `p50(${SpanFields.SPAN_DURATION})`,
+ `p95(${SpanFields.SPAN_DURATION})`,
+ ],
+ columns: [],
+ conditions: `${SpanFields.SPAN_OP}:http.server`,
+ orderby: '',
+ },
+ ],
+ },
+ ],
+ 3
+);
+
+const WIDGETS: Widget[] = [...KPI_WIDGETS, ...MEMORY_WIDGETS, ...CORRELATION_WIDGETS];
+
+export const NODE_RUNTIME_METRICS_PREBUILT_CONFIG: PrebuiltDashboard = {
+ dateCreated: '',
+ filters: {},
+ projects: [],
+ title: DASHBOARD_TITLE,
+ widgets: WIDGETS,
+ onboarding: {
+ type: 'custom',
+ componentId: 'node-runtime-metrics',
+ requiredProjectFlags: ['firstTransactionEvent'],
+ },
+};
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings.ts
new file mode 100644
index 00000000000000..562699fbde8359
--- /dev/null
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings.ts
@@ -0,0 +1,3 @@
+import {t} from 'sentry/locale';
+
+export const DASHBOARD_TITLE = t('Node.js Runtime Metrics');
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts
new file mode 100644
index 00000000000000..2eb5ca40220c81
--- /dev/null
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts
@@ -0,0 +1,14 @@
+import type {DataUnit} from 'sentry/utils/discover/fields';
+import type {TraceMetricTypeValue} from 'sentry/views/explore/metrics/types';
+
+type TraceMetricAggregation = 'avg' | 'sum' | 'max';
+
+// Trace metric field format: `aggregation(value,metric_name,metric_type,unit)`
+export function traceMetricField(
+ aggregation: TraceMetricAggregation,
+ name: string,
+ metricType: TraceMetricTypeValue,
+ unit: DataUnit
+) {
+ return `${aggregation}(value,${name},${metricType},${unit ?? '-'})`;
+}
diff --git a/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx
new file mode 100644
index 00000000000000..f84660b6da1d9d
--- /dev/null
+++ b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx
@@ -0,0 +1,36 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {NodeRuntimeMetricsOnboarding} from 'sentry/views/insights/pages/nodeRuntime/onboarding';
+
+describe('NodeRuntimeMetricsOnboarding', () => {
+ it('renders setup instructions with the SDK version requirement', () => {
+ render();
+
+ expect(
+ screen.getByRole('heading', {name: 'Monitor Node.js Runtime Metrics'})
+ ).toBeInTheDocument();
+ expect(screen.getByText('@sentry/node 10.47.0')).toBeInTheDocument();
+ expect(screen.getByText(/or later/)).toBeInTheDocument();
+ expect(
+ screen.getByText(/Data appears after the first collection interval/)
+ ).toBeInTheDocument();
+ });
+
+ it('includes the integration code snippet', () => {
+ render();
+
+ expect(
+ screen.getByText(/Sentry\.nodeRuntimeMetricsIntegration\(\)/)
+ ).toBeInTheDocument();
+ });
+
+ it('links to the integration docs', () => {
+ render();
+
+ const docsLink = screen.getByRole('button', {name: 'Read the Docs'});
+ expect(docsLink).toHaveAttribute(
+ 'href',
+ expect.stringContaining('noderuntimemetrics')
+ );
+ });
+});
diff --git a/static/app/views/insights/pages/nodeRuntime/onboarding.tsx b/static/app/views/insights/pages/nodeRuntime/onboarding.tsx
new file mode 100644
index 00000000000000..2ab58f05ee3d3b
--- /dev/null
+++ b/static/app/views/insights/pages/nodeRuntime/onboarding.tsx
@@ -0,0 +1,64 @@
+import emptyStateImg from 'sentry-images/spot/performance-waiting-for-span.svg';
+
+import {LinkButton} from '@sentry/scraps/button';
+import {CodeBlock, InlineCode} from '@sentry/scraps/code';
+import {Image} from '@sentry/scraps/image';
+import {Flex} from '@sentry/scraps/layout';
+import {Heading, Text} from '@sentry/scraps/text';
+
+import {Panel} from 'sentry/components/panels/panel';
+import {t, tct} from 'sentry/locale';
+
+const DOCS_URL =
+ 'https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/noderuntimemetrics/';
+
+const CODE_SNIPPET = `import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: '__YOUR_DSN__',
+ integrations: [Sentry.nodeRuntimeMetricsIntegration()],
+});`;
+
+export function NodeRuntimeMetricsOnboarding() {
+ return (
+
+
+
+
+
+ {t('Monitor Node.js Runtime Metrics')}
+
+
+
+ {t(
+ 'Track CPU utilization, memory usage, and event loop health for your Node.js processes. Enable the runtime metrics integration to start collecting data.'
+ )}
+
+
+
+ {tct('Requires [pkg] or later.', {
+ pkg: @sentry/node 10.47.0,
+ })}
+
+
+ {CODE_SNIPPET}
+
+
+ {t(
+ 'Data appears after the first collection interval (default 30 seconds).'
+ )}
+
+
+
+ {t('Read the Docs')}
+
+
+
+
+
+
+
+
+
+ );
+}