From e4812b2a635d5ebc91c7719d75892d857a4e615a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Apr 2026 10:30:22 +0200 Subject: [PATCH 1/8] add dashboard --- .../prebuiltDashboardOnboardingGate.tsx | 5 + .../dashboards/utils/prebuiltConfigs.tsx | 7 +- .../nodeRuntimeMetrics.spec.ts | 52 +++++ .../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 215 ++++++++++++++++++ .../nodeRuntimeMetrics/settings.ts | 3 + .../pages/nodeRuntime/onboarding.spec.tsx | 35 +++ .../insights/pages/nodeRuntime/onboarding.tsx | 64 ++++++ 7 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/settings.ts create mode 100644 static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx create mode 100644 static/app/views/insights/pages/nodeRuntime/onboarding.tsx diff --git a/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx b/static/app/views/dashboards/components/prebuiltDashboardOnboardingGate.tsx index a0bdd087816e..93d188e8dd2d 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 03453b799f9d..9995dd3ea911 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 @@ export 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.spec.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts new file mode 100644 index 000000000000..3269d2f4fe45 --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts @@ -0,0 +1,52 @@ +import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; +import {NODE_RUNTIME_METRICS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics'; + +describe('NODE_RUNTIME_METRICS_PREBUILT_CONFIG', () => { + it('has the expected dashboard title', () => { + expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.title).toBe('Node.js Runtime Metrics'); + }); + + it('has seven widgets matching the handover spec', () => { + expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets).toHaveLength(7); + }); + + it('uses BIG_NUMBER display type for all KPI widgets', () => { + const kpiWidgets = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.slice(0, 3); + kpiWidgets.forEach(widget => { + expect(widget.displayType).toBe(DisplayType.BIG_NUMBER); + expect(widget.widgetType).toBe(WidgetType.TRACEMETRICS); + }); + }); + + it('uses max() aggregation for event loop delay percentiles', () => { + const eventLoopDelayWidget = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.find( + w => w.id === 'node-runtime-event-loop-delay' + ); + expect(eventLoopDelayWidget).toBeDefined(); + expect(eventLoopDelayWidget?.queries[0]!.fields).toEqual([ + 'max(value,node.runtime.event_loop.delay.p50,gauge,second)', + 'max(value,node.runtime.event_loop.delay.p99,gauge,second)', + ]); + }); + + it('queries HTTP request duration from spans dataset', () => { + const httpWidget = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.find( + w => w.id === 'node-runtime-http-request-duration' + ); + expect(httpWidget).toBeDefined(); + expect(httpWidget?.widgetType).toBe(WidgetType.SPANS); + expect(httpWidget?.queries[0]!.conditions).toBe('span.op:http.server'); + expect(httpWidget?.queries[0]!.fields).toEqual([ + 'p50(span.duration)', + 'p95(span.duration)', + ]); + }); + + it('configures custom onboarding for node-runtime-metrics', () => { + expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.onboarding).toEqual({ + type: 'custom', + componentId: 'node-runtime-metrics', + requiredProjectFlags: ['firstTransactionEvent'], + }); + }); +}); 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 000000000000..509dcbe9ca6f --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts @@ -0,0 +1,215 @@ +import {t} from 'sentry/locale'; +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 {SpanFields} from 'sentry/views/insights/types'; + +const INTERVAL = '5m'; + +// Trace metric tuple format: `aggregation(value,metric_name,metric_type,unit)` +const metric = ( + aggregation: 'avg' | 'sum' | 'max', + name: string, + metricType: 'gauge' | 'counter', + unit: 'none' | 'byte' | 'second' +) => `${aggregation}(value,${name},${metricType},${unit})`; + +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: [metric('avg', 'node.runtime.event_loop.utilization', 'gauge', 'none')], + aggregates: [ + metric('avg', 'node.runtime.event_loop.utilization', 'gauge', 'none'), + ], + 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: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + aggregates: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + 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: [metric('sum', 'node.runtime.process.uptime', 'counter', 'second')], + aggregates: [metric('sum', 'node.runtime.process.uptime', 'counter', 'second')], + columns: [], + conditions: '', + orderby: '', + }, + ], + }, + ], + 0, + {h: 1, minH: 1} +); + +const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( + [ + { + id: 'node-runtime-memory-usage', + title: t('Memory Usage'), + description: t( + '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: [ + metric('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), + metric('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), + metric('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + ], + aggregates: [ + metric('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), + metric('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), + metric('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + ], + columns: [], + conditions: '', + orderby: '', + }, + ], + }, + { + id: 'node-runtime-event-loop-delay', + title: t('Event Loop Delay'), + description: t( + 'Maximum observed p50 (median) and p99 (tail) event loop delay across instances. High p50 suggests consistent blocking; high p99 reveals intermittent stalls. Values are shown as worst-case per time bucket to avoid masking spikes.' + ), + displayType: DisplayType.LINE, + widgetType: WidgetType.TRACEMETRICS, + interval: INTERVAL, + queries: [ + { + name: '', + // max() is used because p50/p99 are precomputed percentiles from the SDK's + // event loop delay histogram. Averaging precomputed percentiles is statistically + // incorrect; max() surfaces the worst-observed value per bucket. + fields: [ + metric('max', 'node.runtime.event_loop.delay.p50', 'gauge', 'second'), + metric('max', 'node.runtime.event_loop.delay.p99', 'gauge', 'second'), + ], + aggregates: [ + metric('max', 'node.runtime.event_loop.delay.p50', 'gauge', 'second'), + metric('max', 'node.runtime.event_loop.delay.p99', 'gauge', 'second'), + ], + 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: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + aggregates: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + 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, ...RUNTIME_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 000000000000..562699fbde83 --- /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/insights/pages/nodeRuntime/onboarding.spec.tsx b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx new file mode 100644 index 000000000000..f5ce91615e81 --- /dev/null +++ b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx @@ -0,0 +1,35 @@ +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 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 000000000000..2ab58f05ee3d --- /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')} + + + + + + + + + + ); +} From fcfb0a93f8052caa9b392eeded885df4c619216e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Apr 2026 11:30:40 +0200 Subject: [PATCH 2/8] fix test --- .../app/views/insights/pages/nodeRuntime/onboarding.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx index f5ce91615e81..f84660b6da1d 100644 --- a/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx +++ b/static/app/views/insights/pages/nodeRuntime/onboarding.spec.tsx @@ -9,7 +9,8 @@ describe('NodeRuntimeMetricsOnboarding', () => { expect( screen.getByRole('heading', {name: 'Monitor Node.js Runtime Metrics'}) ).toBeInTheDocument(); - expect(screen.getByText(/@sentry\/node 10\.47\.0 or later/)).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(); From e6e6985399e23f3886eeb04a427750751c6ce09b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 24 Apr 2026 12:03:54 +0200 Subject: [PATCH 3/8] rm test file --- .../nodeRuntimeMetrics.spec.ts | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts deleted file mode 100644 index 3269d2f4fe45..000000000000 --- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {DisplayType, WidgetType} from 'sentry/views/dashboards/types'; -import {NODE_RUNTIME_METRICS_PREBUILT_CONFIG} from 'sentry/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics'; - -describe('NODE_RUNTIME_METRICS_PREBUILT_CONFIG', () => { - it('has the expected dashboard title', () => { - expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.title).toBe('Node.js Runtime Metrics'); - }); - - it('has seven widgets matching the handover spec', () => { - expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets).toHaveLength(7); - }); - - it('uses BIG_NUMBER display type for all KPI widgets', () => { - const kpiWidgets = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.slice(0, 3); - kpiWidgets.forEach(widget => { - expect(widget.displayType).toBe(DisplayType.BIG_NUMBER); - expect(widget.widgetType).toBe(WidgetType.TRACEMETRICS); - }); - }); - - it('uses max() aggregation for event loop delay percentiles', () => { - const eventLoopDelayWidget = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.find( - w => w.id === 'node-runtime-event-loop-delay' - ); - expect(eventLoopDelayWidget).toBeDefined(); - expect(eventLoopDelayWidget?.queries[0]!.fields).toEqual([ - 'max(value,node.runtime.event_loop.delay.p50,gauge,second)', - 'max(value,node.runtime.event_loop.delay.p99,gauge,second)', - ]); - }); - - it('queries HTTP request duration from spans dataset', () => { - const httpWidget = NODE_RUNTIME_METRICS_PREBUILT_CONFIG.widgets.find( - w => w.id === 'node-runtime-http-request-duration' - ); - expect(httpWidget).toBeDefined(); - expect(httpWidget?.widgetType).toBe(WidgetType.SPANS); - expect(httpWidget?.queries[0]!.conditions).toBe('span.op:http.server'); - expect(httpWidget?.queries[0]!.fields).toEqual([ - 'p50(span.duration)', - 'p95(span.duration)', - ]); - }); - - it('configures custom onboarding for node-runtime-metrics', () => { - expect(NODE_RUNTIME_METRICS_PREBUILT_CONFIG.onboarding).toEqual({ - type: 'custom', - componentId: 'node-runtime-metrics', - requiredProjectFlags: ['firstTransactionEvent'], - }); - }); -}); From bb790f5462bab941f2df8e59c496a98c49a0cacf Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 24 Apr 2026 12:04:38 +0200 Subject: [PATCH 4/8] extract trace metric field --- .../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 91 +++++++++++++------ .../prebuiltConfigs/utils/traceMetricField.ts | 13 +++ 2 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts index 509dcbe9ca6f..dfea9bb31cca 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts @@ -3,18 +3,11 @@ import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/type 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'; -// Trace metric tuple format: `aggregation(value,metric_name,metric_type,unit)` -const metric = ( - aggregation: 'avg' | 'sum' | 'max', - name: string, - metricType: 'gauge' | 'counter', - unit: 'none' | 'byte' | 'second' -) => `${aggregation}(value,${name},${metricType},${unit})`; - const KPI_WIDGETS = spaceWidgetsEquallyOnRow( [ { @@ -29,9 +22,21 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( queries: [ { name: '', - fields: [metric('avg', 'node.runtime.event_loop.utilization', 'gauge', 'none')], + fields: [ + traceMetricField( + 'avg', + 'node.runtime.event_loop.utilization', + 'gauge', + 'none' + ), + ], aggregates: [ - metric('avg', 'node.runtime.event_loop.utilization', 'gauge', 'none'), + traceMetricField( + 'avg', + 'node.runtime.event_loop.utilization', + 'gauge', + 'none' + ), ], columns: [], conditions: '', @@ -51,8 +56,12 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( queries: [ { name: '', - fields: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], - aggregates: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + fields: [ + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + ], + aggregates: [ + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + ], columns: [], conditions: '', orderby: '', @@ -71,8 +80,12 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( queries: [ { name: '', - fields: [metric('sum', 'node.runtime.process.uptime', 'counter', 'second')], - aggregates: [metric('sum', 'node.runtime.process.uptime', 'counter', 'second')], + fields: [ + traceMetricField('sum', 'node.runtime.process.uptime', 'counter', 'second'), + ], + aggregates: [ + traceMetricField('sum', 'node.runtime.process.uptime', 'counter', 'second'), + ], columns: [], conditions: '', orderby: '', @@ -90,7 +103,7 @@ const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( id: 'node-runtime-memory-usage', title: t('Memory Usage'), description: t( - '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.' + '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, @@ -99,14 +112,14 @@ const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - metric('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), - metric('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), - metric('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), ], aggregates: [ - metric('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), - metric('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), - metric('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), + traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), ], columns: [], conditions: '', @@ -130,12 +143,32 @@ const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( // event loop delay histogram. Averaging precomputed percentiles is statistically // incorrect; max() surfaces the worst-observed value per bucket. fields: [ - metric('max', 'node.runtime.event_loop.delay.p50', 'gauge', 'second'), - metric('max', 'node.runtime.event_loop.delay.p99', 'gauge', 'second'), + traceMetricField( + 'max', + 'node.runtime.event_loop.delay.p50', + 'gauge', + 'second' + ), + traceMetricField( + 'max', + 'node.runtime.event_loop.delay.p99', + 'gauge', + 'second' + ), ], aggregates: [ - metric('max', 'node.runtime.event_loop.delay.p50', 'gauge', 'second'), - metric('max', 'node.runtime.event_loop.delay.p99', 'gauge', 'second'), + traceMetricField( + 'max', + 'node.runtime.event_loop.delay.p50', + 'gauge', + 'second' + ), + traceMetricField( + 'max', + 'node.runtime.event_loop.delay.p99', + 'gauge', + 'second' + ), ], columns: [], conditions: '', @@ -161,8 +194,12 @@ const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( queries: [ { name: '', - fields: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], - aggregates: [metric('avg', 'node.runtime.cpu.utilization', 'gauge', 'none')], + fields: [ + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + ], + aggregates: [ + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + ], columns: [], conditions: '', orderby: '', 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 000000000000..3634ada9ab38 --- /dev/null +++ b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts @@ -0,0 +1,13 @@ +type TraceMetricAggregation = 'avg' | 'sum' | 'max'; +type TraceMetricType = 'gauge' | 'counter'; +type TraceMetricUnit = 'none' | 'byte' | 'second'; + +// Trace metric field format: `aggregation(value,metric_name,metric_type,unit)` +export function traceMetricField( + aggregation: TraceMetricAggregation, + name: string, + metricType: TraceMetricType, + unit: TraceMetricUnit +) { + return `${aggregation}(value,${name},${metricType},${unit})`; +} From 22209fef492c08d88c3172d363616f397ea3dd98 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 27 Apr 2026 10:00:18 +0200 Subject: [PATCH 5/8] event loop delay table --- .../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 114 ++++++++++-------- 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts index dfea9bb31cca..f49fd14e34d2 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts @@ -97,7 +97,7 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( {h: 1, minH: 1} ); -const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( +const MEMORY_WIDGETS = spaceWidgetsEquallyOnRow( [ { id: 'node-runtime-memory-usage', @@ -127,59 +127,64 @@ const RUNTIME_WIDGETS = spaceWidgetsEquallyOnRow( }, ], }, - { - id: 'node-runtime-event-loop-delay', - title: t('Event Loop Delay'), - description: t( - 'Maximum observed p50 (median) and p99 (tail) event loop delay across instances. High p50 suggests consistent blocking; high p99 reveals intermittent stalls. Values are shown as worst-case per time bucket to avoid masking spikes.' - ), - displayType: DisplayType.LINE, - widgetType: WidgetType.TRACEMETRICS, - interval: INTERVAL, - queries: [ - { - name: '', - // max() is used because p50/p99 are precomputed percentiles from the SDK's - // event loop delay histogram. Averaging precomputed percentiles is statistically - // incorrect; max() surfaces the worst-observed value per bucket. - fields: [ - traceMetricField( - 'max', - 'node.runtime.event_loop.delay.p50', - 'gauge', - 'second' - ), - traceMetricField( - 'max', - 'node.runtime.event_loop.delay.p99', - 'gauge', - 'second' - ), - ], - aggregates: [ - traceMetricField( - 'max', - 'node.runtime.event_loop.delay.p50', - 'gauge', - 'second' - ), - traceMetricField( - 'max', - 'node.runtime.event_loop.delay.p99', - 'gauge', - 'second' - ), - ], - columns: [], - conditions: '', - orderby: '', - }, - ], - }, ], 1 ); +// Event loop delay tables list the worst observed p99/p50 samples across instances. +// Each row = one emitted sample identified by (server.address, timestamp). max() is +// a no-op over a single-row group, used only to satisfy the query DSL. This avoids +// cross-instance aggregation of pre-computed percentiles, which is statistically +// unsound. +const worstEventLoopDelayTable = ( + percentile: 'p50' | 'p99', + title: string, + description: string +): Widget => { + const metricName = `node.runtime.event_loop.delay.${percentile}`; + const field = traceMetricField('max', metricName, 'gauge', 'second'); + return { + id: `node-runtime-event-loop-delay-${percentile}-samples`, + title, + description, + displayType: DisplayType.TABLE, + widgetType: WidgetType.TRACEMETRICS, + interval: INTERVAL, + limit: 10, + queries: [ + { + name: '', + fields: ['server.address', 'timestamp', field], + aggregates: [field], + columns: ['server.address', 'timestamp'], + conditions: '', + orderby: `-${field}`, + fieldAliases: [t('Server'), t('Timestamp'), t('%s Delay', percentile)], + }, + ], + }; +}; + +const EVENT_LOOP_DELAY_TABLES = spaceWidgetsEquallyOnRow( + [ + worstEventLoopDelayTable( + 'p99', + t('Top 10 Worst p99 Samples'), + t( + "Ten highest observed p99 event loop delay samples. Each row is one instance's 30s-interval percentile from Node's `perf_hooks` histogram — a true per-instance measurement, not a cross-instance aggregate." + ) + ), + worstEventLoopDelayTable( + 'p50', + t('Top 10 Worst p50 Samples'), + t( + "Ten highest observed p50 (median) event loop delay samples. High p50 suggests consistent blocking. Each row is one instance's 30s-interval percentile — a true per-instance measurement, not a cross-instance aggregate." + ) + ), + ], + 3 +); + const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( [ { @@ -233,10 +238,15 @@ const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( ], }, ], - 3 + 5 ); -const WIDGETS: Widget[] = [...KPI_WIDGETS, ...RUNTIME_WIDGETS, ...CORRELATION_WIDGETS]; +const WIDGETS: Widget[] = [ + ...KPI_WIDGETS, + ...MEMORY_WIDGETS, + ...EVENT_LOOP_DELAY_TABLES, + ...CORRELATION_WIDGETS, +]; export const NODE_RUNTIME_METRICS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', From 5ed23c0b9672bc79878215c2d1dd6357a06ce1c9 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Apr 2026 10:31:49 +0200 Subject: [PATCH 6/8] rm event loop delay tables --- .../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 63 +------------------ 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts index f49fd14e34d2..e3a4b91d0ca5 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts @@ -131,60 +131,6 @@ const MEMORY_WIDGETS = spaceWidgetsEquallyOnRow( 1 ); -// Event loop delay tables list the worst observed p99/p50 samples across instances. -// Each row = one emitted sample identified by (server.address, timestamp). max() is -// a no-op over a single-row group, used only to satisfy the query DSL. This avoids -// cross-instance aggregation of pre-computed percentiles, which is statistically -// unsound. -const worstEventLoopDelayTable = ( - percentile: 'p50' | 'p99', - title: string, - description: string -): Widget => { - const metricName = `node.runtime.event_loop.delay.${percentile}`; - const field = traceMetricField('max', metricName, 'gauge', 'second'); - return { - id: `node-runtime-event-loop-delay-${percentile}-samples`, - title, - description, - displayType: DisplayType.TABLE, - widgetType: WidgetType.TRACEMETRICS, - interval: INTERVAL, - limit: 10, - queries: [ - { - name: '', - fields: ['server.address', 'timestamp', field], - aggregates: [field], - columns: ['server.address', 'timestamp'], - conditions: '', - orderby: `-${field}`, - fieldAliases: [t('Server'), t('Timestamp'), t('%s Delay', percentile)], - }, - ], - }; -}; - -const EVENT_LOOP_DELAY_TABLES = spaceWidgetsEquallyOnRow( - [ - worstEventLoopDelayTable( - 'p99', - t('Top 10 Worst p99 Samples'), - t( - "Ten highest observed p99 event loop delay samples. Each row is one instance's 30s-interval percentile from Node's `perf_hooks` histogram — a true per-instance measurement, not a cross-instance aggregate." - ) - ), - worstEventLoopDelayTable( - 'p50', - t('Top 10 Worst p50 Samples'), - t( - "Ten highest observed p50 (median) event loop delay samples. High p50 suggests consistent blocking. Each row is one instance's 30s-interval percentile — a true per-instance measurement, not a cross-instance aggregate." - ) - ), - ], - 3 -); - const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( [ { @@ -238,15 +184,10 @@ const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( ], }, ], - 5 + 3 ); -const WIDGETS: Widget[] = [ - ...KPI_WIDGETS, - ...MEMORY_WIDGETS, - ...EVENT_LOOP_DELAY_TABLES, - ...CORRELATION_WIDGETS, -]; +const WIDGETS: Widget[] = [...KPI_WIDGETS, ...MEMORY_WIDGETS, ...CORRELATION_WIDGETS]; export const NODE_RUNTIME_METRICS_PREBUILT_CONFIG: PrebuiltDashboard = { dateCreated: '', From 1d159e0005a2f67a729205d0a227a668ec4e3279 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Apr 2026 17:28:03 +0200 Subject: [PATCH 7/8] use types --- .../nodeRuntimeMetrics/nodeRuntimeMetrics.ts | 59 +++++++++++-------- .../prebuiltConfigs/utils/traceMetricField.ts | 11 ++-- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts index e3a4b91d0ca5..b8e8d18bd842 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/nodeRuntimeMetrics/nodeRuntimeMetrics.ts @@ -1,4 +1,5 @@ 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'; @@ -23,20 +24,10 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - traceMetricField( - 'avg', - 'node.runtime.event_loop.utilization', - 'gauge', - 'none' - ), + traceMetricField('avg', 'node.runtime.event_loop.utilization', 'gauge', null), ], aggregates: [ - traceMetricField( - 'avg', - 'node.runtime.event_loop.utilization', - 'gauge', - 'none' - ), + traceMetricField('avg', 'node.runtime.event_loop.utilization', 'gauge', null), ], columns: [], conditions: '', @@ -57,10 +48,10 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null), ], aggregates: [ - traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null), ], columns: [], conditions: '', @@ -81,10 +72,20 @@ const KPI_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - traceMetricField('sum', 'node.runtime.process.uptime', 'counter', 'second'), + traceMetricField( + 'sum', + 'node.runtime.process.uptime', + 'counter', + DurationUnit.SECOND + ), ], aggregates: [ - traceMetricField('sum', 'node.runtime.process.uptime', 'counter', 'second'), + traceMetricField( + 'sum', + 'node.runtime.process.uptime', + 'counter', + DurationUnit.SECOND + ), ], columns: [], conditions: '', @@ -112,14 +113,24 @@ const MEMORY_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - traceMetricField('avg', 'node.runtime.mem.rss', 'gauge', 'byte'), - traceMetricField('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), - traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + 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', 'byte'), - traceMetricField('avg', 'node.runtime.mem.heap_total', 'gauge', 'byte'), - traceMetricField('avg', 'node.runtime.mem.heap_used', 'gauge', 'byte'), + 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: '', @@ -146,10 +157,10 @@ const CORRELATION_WIDGETS = spaceWidgetsEquallyOnRow( { name: '', fields: [ - traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null), ], aggregates: [ - traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', 'none'), + traceMetricField('avg', 'node.runtime.cpu.utilization', 'gauge', null), ], columns: [], conditions: '', diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts index 3634ada9ab38..796d3fd10bf1 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts @@ -1,13 +1,14 @@ +import type {DataUnit} from 'sentry/utils/discover/fields'; +import type {TraceMetricTypeValue} from 'sentry/views/explore/metrics/types'; + type TraceMetricAggregation = 'avg' | 'sum' | 'max'; -type TraceMetricType = 'gauge' | 'counter'; -type TraceMetricUnit = 'none' | 'byte' | 'second'; // Trace metric field format: `aggregation(value,metric_name,metric_type,unit)` export function traceMetricField( aggregation: TraceMetricAggregation, name: string, - metricType: TraceMetricType, - unit: TraceMetricUnit + metricType: TraceMetricTypeValue, + unit: DataUnit ) { - return `${aggregation}(value,${name},${metricType},${unit})`; + return `${aggregation}(value,${name},${metricType},${unit ?? 'none'})`; } From 184996d3bd3370e802e3c4d2490d3d35a1f5ce93 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 30 Apr 2026 10:56:51 +0200 Subject: [PATCH 8/8] replace placeholder --- .../dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts index 796d3fd10bf1..2eb5ca40220c 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/utils/traceMetricField.ts @@ -10,5 +10,5 @@ export function traceMetricField( metricType: TraceMetricTypeValue, unit: DataUnit ) { - return `${aggregation}(value,${name},${metricType},${unit ?? 'none'})`; + return `${aggregation}(value,${name},${metricType},${unit ?? '-'})`; }