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')} + + + + + + + + + + ); +}