This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 40f8af23c fix(metrics): keep scalar and histogram samples on separate 
series (#4358)
40f8af23c is described below

commit 40f8af23c90409209a378b9c17b707de5049b160
Author: btlqql <[email protected]>
AuthorDate: Wed Sep 16 16:01:38 2026 +0800

    fix(metrics): keep scalar and histogram samples on separate series (#4358)
---
 web/src/components/MetricsExplorer.tsx             | 31 +++++++++++++-------
 .../components/__tests__/MetricsExplorer.test.tsx  | 10 ++++---
 web/src/utils/metricsExplorerDiagnostics.test.ts   | 34 ++++++++++++++++++++++
 web/src/utils/metricsExplorerDiagnostics.ts        | 26 ++++++++---------
 4 files changed, 73 insertions(+), 28 deletions(-)

diff --git a/web/src/components/MetricsExplorer.tsx 
b/web/src/components/MetricsExplorer.tsx
index fc205f7f9..3e066c538 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -122,14 +122,23 @@ const MetricChart = ({
   hiddenSeriesText,
 }: MetricChartProps) => {
   const allSeries = data.series
-    .map((series, index) => {
-      const { samples, fromHistogram } = toMetricSeriesSamples(series);
-      return {
-        color: SERIES_COLORS[index % SERIES_COLORS.length],
-        label: metricSeriesLabel(series, metric.name),
-        samples,
-        fromHistogram,
-      };
+    .flatMap((series, index) => {
+      const { samples } = toMetricSeriesSamples(series);
+      const baseLabel = metricSeriesLabel(series, metric.name);
+      const isMixed =
+        samples.some((sample) => sample.kind === 'scalar') &&
+        samples.some((sample) => sample.kind === 'histogram');
+      // Keep raw floats and histogram-derived trends on separate lines.
+      return (['scalar', 'histogram'] as const).map((kind, kindIndex) => ({
+        color: SERIES_COLORS[
+          (isMixed ? index * 2 + kindIndex : index) % SERIES_COLORS.length
+        ],
+        label: isMixed
+          ? `${baseLabel} (${kind === 'histogram' ? histogramLabel : 
'scalar'})`
+          : baseLabel,
+        samples: samples.filter((sample) => sample.kind === kind),
+        fromHistogram: kind === 'histogram',
+      }));
     })
     .filter((series) => series.samples.length > 0);
 
@@ -210,7 +219,7 @@ const MetricChart = ({
         })}
         {chartSeries.map((series) => (
           <polyline
-            key={series.label}
+            key={`${series.label}-${series.fromHistogram}`}
             fill="none"
             stroke={series.color}
             strokeWidth="2.5"
@@ -246,7 +255,7 @@ const MetricChart = ({
           const latest = series.samples[series.samples.length - 1];
           return (
             <Flex
-              key={series.label}
+              key={`${series.label}-${series.fromHistogram}`}
               align="center"
               gap={6}
               style={{ flex: '0 1 auto', minWidth: 0, maxWidth: '100%' }}
@@ -968,7 +977,7 @@ const MetricsExplorer = ({ instanceId }: 
MetricsExplorerProps) => {
         width: 130,
         render: (value: MetricSeriesDetailRow['sampleType']) => (
           <Tag color={value === 'histogram' ? 'purple' : 'blue'} style={{ 
marginInlineEnd: 0 }}>
-            {value === 'histogram' ? copy.histogram : 'scalar'}
+            {value === 'histogram' ? copy.histogram : value === 'mixed' ? 
`scalar + ${copy.histogram}` : 'scalar'}
           </Tag>
         ),
       },
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx 
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index 2965b096d..8afd16a8b 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -551,14 +551,14 @@ describe('MetricsExplorer', () => {
     expect(await screen.findByText('20 messages/s')).toBeInTheDocument();
   });
 
-  it('prefers scalar samples when a series has both values and histograms', 
async () => {
+  it('shows separate scalar and histogram trends for a mixed series', async () 
=> {
     vi.mocked(queryMetrics).mockResolvedValue({
       ...metricData,
       series: [
         {
           ...metricData.series[0],
           histograms: [
-            { timestamp: 1_800_000_000, histogram: { count: '99', sum: '999', 
buckets: [] } },
+            { timestamp: 1_800_000_001, histogram: { count: '99', sum: '999', 
buckets: [] } },
           ],
         },
       ],
@@ -567,8 +567,10 @@ describe('MetricsExplorer', () => {
     renderWithProviders(<MetricsExplorer />);
 
     expect(await screen.findByText('42 messages/s')).toBeInTheDocument();
-    expect(screen.queryByText('999 messages/s')).not.toBeInTheDocument();
-    expect(screen.queryByText('直方图')).not.toBeInTheDocument();
+    expect(screen.getByText('999 messages/s')).toBeInTheDocument();
+    expect(screen.getByText('直方图')).toBeInTheDocument();
+    expect(screen.getByText(/broker-a.*\(scalar\)/)).toBeInTheDocument();
+    expect(screen.getByText(/broker-a.*\(直方图\)/)).toBeInTheDocument();
   });
 
   it('queries the selected data source through the datasource endpoint', async 
() => {
diff --git a/web/src/utils/metricsExplorerDiagnostics.test.ts 
b/web/src/utils/metricsExplorerDiagnostics.test.ts
index 717f9d8bf..60fd11e0e 100644
--- a/web/src/utils/metricsExplorerDiagnostics.test.ts
+++ b/web/src/utils/metricsExplorerDiagnostics.test.ts
@@ -88,6 +88,40 @@ const createHistoryEntry = (
   });
 
 describe('metrics explorer diagnostics', () => {
+  it('preserves both sample kinds in a mixed Prometheus matrix series', () => {
+    const data: MetricData = {
+      resultType: 'matrix',
+      warnings: [],
+      series: [{
+        labels: { instance: 'broker-a' },
+        values: [{ timestamp: 2, value: '12' }],
+        histograms: [
+          { timestamp: 3, histogram: { count: '4', sum: '20', buckets: [] } },
+          { timestamp: 1, histogram: { count: '2', sum: '10', buckets: [] } },
+        ],
+      }],
+    };
+
+    expect(toMetricSeriesSamples(data.series[0]).samples.map(({ timestamp, 
kind }) => ({ timestamp, kind })))
+      .toEqual([
+        { timestamp: 1, kind: 'histogram' },
+        { timestamp: 2, kind: 'scalar' },
+        { timestamp: 3, kind: 'histogram' },
+      ]);
+    expect(summarizeMetricData(data)).toMatchObject({
+      seriesCount: 1, visibleSeriesCount: 1, sampleCount: 3,
+      scalarSampleCount: 1, histogramSampleCount: 2,
+      earliestTimestamp: 1, latestTimestamp: 3,
+    });
+    expect(buildMetricSeriesDetailRows(data, metric)[0]).toMatchObject({
+      sampleType: 'mixed', sampleCount: 3, latestTimestamp: 3,
+      latestValue: 20, histogramCount: 4, histogramSum: 20,
+    });
+    const rows = buildMetricCsvRows(data, metric, { profileName: 'test', 
sourceName: 'test' });
+    expect(rows.map((row) => row.sampleType)).toEqual(['histogram', 'scalar', 
'histogram']);
+    expect(rows.map((row) => row.timestamp)).toEqual([1, 2, 3]);
+  });
+
   it('sorts scalar samples and ignores non-numeric values', () => {
     const samples = toMetricSeriesSamples(metricData.series[0]);
 
diff --git a/web/src/utils/metricsExplorerDiagnostics.ts 
b/web/src/utils/metricsExplorerDiagnostics.ts
index 6a37641ed..8ae1980cf 100644
--- a/web/src/utils/metricsExplorerDiagnostics.ts
+++ b/web/src/utils/metricsExplorerDiagnostics.ts
@@ -84,7 +84,7 @@ export interface MetricSeriesDetailRow {
   seriesIndex: number;
   seriesLabel: string;
   labels: string;
-  sampleType: MetricSampleKind;
+  sampleType: MetricSampleKind | 'mixed';
   sampleCount: number;
   latestTimestamp?: number;
   latestValue?: number;
@@ -166,7 +166,7 @@ const toScalarSamples = (series: MetricSeries): 
NumericMetricSample[] =>
     })),
   );
 
-// Native histograms carry no scalar samples. To keep diagnostics usable, 
derive
+// To keep native histogram diagnostics usable, derive
 // a trend value from the observed sum and fall back to observation count.
 const toHistogramSamples = (series: MetricSeries): NumericMetricSample[] =>
   sortMetricSamples(
@@ -186,10 +186,11 @@ const toHistogramSamples = (series: MetricSeries): 
NumericMetricSample[] =>
 
 export const toMetricSeriesSamples = (series: MetricSeries): 
MetricSeriesSamples => {
   const scalar = toScalarSamples(series);
-  if (scalar.length > 0) {
-    return { samples: scalar, fromHistogram: false };
-  }
-  return { samples: toHistogramSamples(series), fromHistogram: true };
+  const histograms = toHistogramSamples(series);
+  return {
+    samples: sortMetricSamples([...scalar, ...histograms]),
+    fromHistogram: histograms.length > 0,
+  };
 };
 
 export const summarizeMetricData = (data: MetricData): MetricResultSummary => {
@@ -200,15 +201,12 @@ export const summarizeMetricData = (data: MetricData): 
MetricResultSummary => {
   let latestTimestamp: number | undefined;
 
   data.series.forEach((series) => {
-    const { samples, fromHistogram } = toMetricSeriesSamples(series);
+    const { samples } = toMetricSeriesSamples(series);
     if (samples.length === 0) return;
     visibleSeriesCount += 1;
-    if (fromHistogram) {
-      histogramSampleCount += samples.length;
-    } else {
-      scalarSampleCount += samples.length;
-    }
     samples.forEach((sample) => {
+      if (sample.kind === 'histogram') histogramSampleCount += 1;
+      else scalarSampleCount += 1;
       earliestTimestamp =
         earliestTimestamp === undefined
           ? sample.timestamp
@@ -322,7 +320,9 @@ export const buildMetricSeriesDetailRows = (
       seriesIndex: seriesIndex + 1,
       seriesLabel: metricSeriesLabel(series, metric.name),
       labels: stableLabelsText(series.labels),
-      sampleType: fromHistogram ? 'histogram' : 'scalar',
+      sampleType: fromHistogram
+        ? samples.some((sample) => sample.kind === 'scalar') ? 'mixed' : 
'histogram'
+        : 'scalar',
       sampleCount: samples.length,
       latestTimestamp: latest?.timestamp,
       latestValue: latest?.value,

Reply via email to