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 253a85a7e fix: runtime observability and result accuracy (#2199)
253a85a7e is described below

commit 253a85a7e0f8c76ad324ff91a27b61200f67873b
Author: shown <[email protected]>
AuthorDate: Wed Aug 19 11:18:48 2026 +0800

    fix: runtime observability and result accuracy (#2199)
    
    * fix(metrics): honor configured authentication mode
    
    * fix(web): order metric samples by timestamp
    
    * fix(dlq): report resend hard-cap truncation
    
    * fix(dashboard): flag incomplete broker topology
    
    Signed-off-by: yuluo-yx <[email protected]>
    
    ---------
    
    Signed-off-by: yuluo-yx <[email protected]>
---
 .../AbstractPrometheusCompatibleMetricsSource.java | 36 ++++++++----
 .../cluster/metrics/MetricsSourceFactory.java      |  1 +
 .../cluster/metrics/MetricsSourceSettings.java     |  2 +
 .../cluster/metrics/PrometheusMetricsSource.java   | 12 ++++
 .../provider/apache/RocketMQDLQProvider.java       | 16 ++++--
 .../provider/apache/RocketMQDashboardProvider.java | 20 ++++++-
 .../metrics/MultiBackendMetricsSourceTest.java     | 66 ++++++++++++++++++++++
 .../provider/apache/RocketMQDLQProviderTest.java   | 47 +++++++++++++++
 .../apache/RocketMQDashboardProviderTest.java      |  3 +
 web/src/components/MetricsExplorer.tsx             |  6 +-
 .../components/__tests__/MetricsExplorer.test.tsx  | 23 ++++++++
 11 files changed, 211 insertions(+), 21 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
index b7d044b95..f8de3e9c2 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
@@ -42,6 +42,7 @@ import java.net.URI;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.stream.StreamSupport;
 
@@ -198,18 +199,29 @@ public abstract class 
AbstractPrometheusCompatibleMetricsSource implements Metri
     }
 
     private void applyAuthentication(HttpHeaders headers) {
-        if (StringUtils.hasText(settings.getBearerToken())) {
-            headers.setBearerAuth(settings.getBearerToken());
-            return;
-        }
-        boolean hasUsername = StringUtils.hasText(settings.getUsername());
-        boolean hasPassword = StringUtils.hasText(settings.getPassword());
-        if (hasUsername != hasPassword) {
-            throw new 
PrometheusException(HttpStatus.SERVICE_UNAVAILABLE.value(),
-                    backendLabel() + " basic authentication is incomplete");
-        }
-        if (hasUsername) {
-            headers.setBasicAuth(settings.getUsername(), 
settings.getPassword());
+        String authType = StringUtils.hasText(settings.getAuthType())
+                ? settings.getAuthType().strip().toLowerCase(Locale.ROOT)
+                : "none";
+        switch (authType) {
+            case "none" -> {
+                // Credentials may remain after changing modes; none must 
never send them.
+            }
+            case "basic" -> {
+                if (!StringUtils.hasText(settings.getUsername()) || 
!StringUtils.hasText(settings.getPassword())) {
+                    throw new 
PrometheusException(HttpStatus.SERVICE_UNAVAILABLE.value(),
+                            backendLabel() + " basic authentication is 
incomplete");
+                }
+                headers.setBasicAuth(settings.getUsername(), 
settings.getPassword());
+            }
+            case "bearer" -> {
+                if (!StringUtils.hasText(settings.getBearerToken())) {
+                    throw new 
PrometheusException(HttpStatus.SERVICE_UNAVAILABLE.value(),
+                            backendLabel() + " bearer authentication is 
incomplete");
+                }
+                headers.setBearerAuth(settings.getBearerToken());
+            }
+            default -> throw new 
PrometheusException(HttpStatus.SERVICE_UNAVAILABLE.value(),
+                    "Unsupported " + backendLabel() + " authentication mode: " 
+ settings.getAuthType());
         }
     }
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceFactory.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceFactory.java
index 33fd391f5..3d3014eba 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceFactory.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceFactory.java
@@ -45,6 +45,7 @@ public class MetricsSourceFactory {
                 .baseUrl(config.getUrl())
                 .connectTimeout(Duration.ofSeconds(3))
                 .readTimeout(Duration.ofSeconds(10))
+                .authType(config.getAuthType())
                 .username(config.getUsername())
                 .password(config.getPassword())
                 .bearerToken(config.getBearerToken())
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceSettings.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceSettings.java
index a38aa2805..a5dbd2f9d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceSettings.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/MetricsSourceSettings.java
@@ -40,6 +40,8 @@ public class MetricsSourceSettings {
     private final Duration connectTimeout = Duration.ofSeconds(3);
     @Builder.Default
     private final Duration readTimeout = Duration.ofSeconds(10);
+    @Builder.Default
+    private final String authType = "none";
     private final String username;
     private final String password;
     private final String bearerToken;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
index 42effc126..4a822c39d 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/PrometheusMetricsSource.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.cluster.metrics;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestClient;
 
 /**
@@ -49,9 +50,20 @@ public class PrometheusMetricsSource extends 
AbstractPrometheusCompatibleMetrics
                 .baseUrl(properties.getBaseUrl())
                 .connectTimeout(properties.getConnectTimeout())
                 .readTimeout(properties.getReadTimeout())
+                .authType(legacyAuthType(properties))
                 .username(properties.getUsername())
                 .password(properties.getPassword())
                 .bearerToken(properties.getBearerToken())
                 .build();
     }
+
+    private static String legacyAuthType(PrometheusProperties properties) {
+        if (StringUtils.hasText(properties.getBearerToken())) {
+            return "bearer";
+        }
+        if (StringUtils.hasText(properties.getUsername()) || 
StringUtils.hasText(properties.getPassword())) {
+            return "basic";
+        }
+        return "none";
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
index 217c7633c..df86a1e58 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProvider.java
@@ -209,9 +209,10 @@ public class RocketMQDLQProvider implements DLQProvider {
 
         String outcome = classifyOutcome(deadLetters.size(), resent, failed, 
scanResult.scanIncomplete());
         String detail = String.format("instanceId=%s, group=%s, dlqTopic=%s, 
targetTopic=%s, matched=%d, resent=%d, "
-                        + "failed=%d, scanIncomplete=%s, scanFailedQueues=%d",
+                        + "failed=%d, scanIncomplete=%s, scanTruncated=%s, 
scanFailedQueues=%d",
                 instanceId, groupName, dlqTopic, 
StringUtils.hasText(targetTopic) ? targetTopic : "<original>",
-                deadLetters.size(), resent, failed, 
scanResult.scanIncomplete(), scanResult.failedQueueCount());
+                deadLetters.size(), resent, failed, 
scanResult.scanIncomplete(), scanResult.truncated(),
+                scanResult.failedQueueCount());
         recordAudit(groupName, detail, outcome);
         log.info("DLQ resend completed: {}", detail);
         return DLQResendResultVO.builder()
@@ -267,11 +268,12 @@ public class RocketMQDLQProvider implements DLQProvider {
         DefaultMQPullConsumer consumer = newPullConsumer(endpoint, 
credentialHook);
         List<MessageExt> result = new ArrayList<>();
         int failedQueueCount = 0;
+        boolean truncated = false;
         try {
             consumer.start();
             Set<MessageQueue> queues = 
consumer.fetchSubscribeMessageQueues(dlqTopic);
             if (queues == null || queues.isEmpty()) {
-                return new DeadLetterScanResult(result, 0);
+                return new DeadLetterScanResult(result, 0, false);
             }
             outer:
             for (MessageQueue queue : queues) {
@@ -283,6 +285,7 @@ public class RocketMQDLQProvider implements DLQProvider {
                     int consecutiveIllegalOffsets = 0;
                     for (long offset = minOffset; offset <= maxOffset; ) {
                         if (result.size() >= cap) {
+                            truncated = true;
                             break outer;
                         }
                         PullResult pullResult = consumer.pull(queue, "*", 
offset, 32);
@@ -325,6 +328,7 @@ public class RocketMQDLQProvider implements DLQProvider {
                                     && messageExt.getStoreTimestamp() <= end) {
                                 result.add(messageExt);
                                 if (result.size() >= cap) {
+                                    truncated = true;
                                     break outer;
                                 }
                             }
@@ -347,7 +351,7 @@ public class RocketMQDLQProvider implements DLQProvider {
         } finally {
             consumer.shutdown();
         }
-        return new DeadLetterScanResult(result, failedQueueCount);
+        return new DeadLetterScanResult(result, failedQueueCount, truncated);
     }
 
     private boolean resendOne(DefaultMQProducer producer, MessageExt 
deadLetter, String targetTopic) {
@@ -458,9 +462,9 @@ public class RocketMQDLQProvider implements DLQProvider {
         }
     }
 
-    private record DeadLetterScanResult(List<MessageExt> messages, int 
failedQueueCount) {
+    private record DeadLetterScanResult(List<MessageExt> messages, int 
failedQueueCount, boolean truncated) {
         boolean scanIncomplete() {
-            return failedQueueCount > 0;
+            return failedQueueCount > 0 || truncated;
         }
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
index dc94c2f75..dc3699d5f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProvider.java
@@ -111,6 +111,23 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
             totalClusters = clusterAddrTable.size();
             totalBrokers = brokerAddrTable.size();
 
+            Set<String> topologyUnavailableClusters = new HashSet<>();
+            for (Map.Entry<String, Set<String>> clusterEntry : 
clusterAddrTable.entrySet()) {
+                Set<String> brokerNames = clusterEntry.getValue();
+                if (brokerNames == null || brokerNames.isEmpty()) {
+                    topologyUnavailableClusters.add(clusterEntry.getKey());
+                    continue;
+                }
+                boolean incomplete = brokerNames.stream().anyMatch(brokerName 
-> {
+                    BrokerData brokerData = brokerAddrTable.get(brokerName);
+                    return brokerData == null || brokerData.getBrokerAddrs() 
== null
+                            || brokerData.getBrokerAddrs().get(0L) == null;
+                });
+                if (incomplete) {
+                    topologyUnavailableClusters.add(clusterEntry.getKey());
+                }
+            }
+
             // Collect all unique broker addresses (master only, brokerId=0)
             Set<String> masterAddrs = new HashSet<>();
 
@@ -241,7 +258,8 @@ public class RocketMQDashboardProvider implements 
DashboardProvider {
                 long clusterTpsIn = 0;
                 long clusterTpsOut = 0;
                 String version = "unknown";
-                boolean runtimeMetricsUnavailable = 
topicCountsUnavailableClusters.contains(clusterName)
+                boolean runtimeMetricsUnavailable = 
topologyUnavailableClusters.contains(clusterName)
+                        || topicCountsUnavailableClusters.contains(clusterName)
                         || 
groupCountsUnavailableClusters.contains(clusterName);
 
                 for (String brokerName : brokerNames) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
index e20662ec0..a6bdb5f7d 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/MultiBackendMetricsSourceTest.java
@@ -44,6 +44,7 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 class MultiBackendMetricsSourceTest {
 
@@ -139,6 +140,71 @@ class MultiBackendMetricsSourceTest {
         
assertThat(factory.create(config)).isInstanceOf(PrometheusMetricsSource.class);
     }
 
+    @Test
+    void noneAuthenticationShouldIgnoreConfiguredCredentials() {
+        assertAuthorization("none", "user", "password", "token", null);
+    }
+
+    @Test
+    void basicAuthenticationShouldTakePrecedenceOverAnUnrelatedBearerToken() {
+        assertAuthorization("basic", "user", "password", "token", "Basic 
dXNlcjpwYXNzd29yZA==");
+    }
+
+    @Test
+    void bearerAuthenticationShouldIgnoreConfiguredBasicCredentials() {
+        assertAuthorization("bearer", "user", "password", "token", "Bearer 
token");
+    }
+
+    @Test
+    void authenticationModeShouldRejectMissingRequiredCredentials() {
+        assertAuthenticationFailure("basic", "user", null, null,
+                "Prometheus basic authentication is incomplete");
+        assertAuthenticationFailure("bearer", null, null, null,
+                "Prometheus bearer authentication is incomplete");
+    }
+
+    @Test
+    void unsupportedAuthenticationModeShouldBeRejected() {
+        assertAuthenticationFailure("digest", "user", "password", "token",
+                "Unsupported Prometheus authentication mode: digest");
+    }
+
+    private void assertAuthorization(String authType, String username, String 
password,
+                                     String bearerToken, String 
expectedAuthorization) {
+        AtomicReference<String> authorization = new AtomicReference<>();
+        server.createContext(MetricsBackendType.PROMETHEUS.getQueryPath(), 
exchange -> {
+            
authorization.set(exchange.getRequestHeaders().getFirst("Authorization"));
+            respond(exchange, 200, """
+                    
{"status":"success","data":{"resultType":"matrix","result":[]}}
+                    """);
+        });
+
+        factory.create(configWithAuth(authType, username, password, 
bearerToken)).query(query());
+
+        assertThat(authorization.get()).isEqualTo(expectedAuthorization);
+    }
+
+    private void assertAuthenticationFailure(String authType, String username, 
String password,
+                                             String bearerToken, String 
message) {
+        assertThatExceptionOfType(PrometheusException.class)
+                .isThrownBy(() -> factory.create(configWithAuth(authType, 
username, password, bearerToken))
+                        .query(query()))
+                .satisfies(exception -> {
+                    assertThat(exception.getStatusCode()).isEqualTo(503);
+                    assertThat(exception.getMessage()).isEqualTo(message);
+                });
+    }
+
+    private MetricsDataSourceConfig configWithAuth(String authType, String 
username,
+                                                   String password, String 
bearerToken) {
+        MetricsDataSourceConfig config = 
configFor(MetricsBackendType.PROMETHEUS);
+        config.setAuthType(authType);
+        config.setUsername(username);
+        config.setPassword(password);
+        config.setBearerToken(bearerToken);
+        return config;
+    }
+
     private MetricsDataSourceConfig configFor(MetricsBackendType backendType) {
         MetricsDataSourceConfig config = new MetricsDataSourceConfig();
         config.setName(backendType.name().toLowerCase());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index 0035e97c7..7fd6c8a80 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -53,6 +53,7 @@ import java.util.Base64;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -525,6 +526,52 @@ class RocketMQDLQProviderTest {
         }
     }
 
+    @Test
+    void resendMessagesMarksAResultPartialWhenScanReachesHardCap() throws 
Exception {
+        String dlqTopic = MixAll.DLQ_GROUP_TOPIC_PREFIX + "group-a";
+        MessageQueue queue = new MessageQueue(dlqTopic, "broker-a", 0);
+        List<MessageExt> deadLetters = IntStream.range(0, 5001)
+                .mapToObj(index -> {
+                    MessageExt deadLetter = new MessageExt();
+                    deadLetter.setMsgId("msg-" + index);
+                    deadLetter.setTopic(dlqTopic);
+                    deadLetter.setBody(new byte[] {1});
+                    deadLetter.setStoreTimestamp(150L);
+                    return deadLetter;
+                })
+                .toList();
+        PullResult pullResult = new PullResult(PullStatus.FOUND, 5001L, 0L, 
5000L, deadLetters);
+        SendResult sendResult = new SendResult();
+        sendResult.setSendStatus(SendStatus.SEND_OK);
+
+        try (MockedConstruction<DefaultMQPullConsumer> mockedConsumers =
+                     mockConstruction(DefaultMQPullConsumer.class, (consumer, 
context) -> {
+                         doNothing().when(consumer).start();
+                         
when(consumer.fetchSubscribeMessageQueues(dlqTopic)).thenReturn(Set.of(queue));
+                         when(consumer.searchOffset(queue, 
100L)).thenReturn(0L);
+                         when(consumer.searchOffset(queue, 
200L)).thenReturn(5001L);
+                         when(consumer.pull(queue, "*", 0L, 
32)).thenReturn(pullResult);
+                         doNothing().when(consumer).shutdown();
+                     });
+             MockedConstruction<DefaultMQProducer> mockedProducers =
+                     mockConstruction(DefaultMQProducer.class, (producer, 
context) -> {
+                         doNothing().when(producer).start();
+                         
when(producer.send(any(Message.class))).thenReturn(sendResult);
+                         doNothing().when(producer).shutdown();
+                     })) {
+            assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "target-topic"))
+                    .extracting("matched", "resent", "failed", "outcome", 
"scanIncomplete", "failedQueueCount")
+                    .containsExactly(5000, 5000, 0, "PARTIAL", true, 0);
+
+            verify(mockedProducers.constructed().get(0), 
times(5000)).send(any(Message.class));
+        }
+        verify(auditService).record(
+                eq("RESEND_DLQ"),
+                eq("group-a"),
+                contains("scanTruncated=true"),
+                eq("PARTIAL"));
+    }
+
     @Test
     void createsUniqueProducerGroupsForConcurrentDlqResends() {
         String first = RocketMQDLQProvider.nextResendProducerGroup();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
index b26499d69..e12515fcd 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDashboardProviderTest.java
@@ -331,6 +331,7 @@ class RocketMQDashboardProviderTest {
         assertThat(dashboard.getStats()).isNotNull();
         assertThat(dashboard.getClusters()).hasSize(1);
         assertThat(dashboard.getClusters().get(0).getBrokers()).isZero();
+        
assertThat(dashboard.getClusters().get(0).getStatus()).isEqualTo(ClusterStatus.warning);
     }
 
     @Test
@@ -353,6 +354,7 @@ class RocketMQDashboardProviderTest {
         assertThat(dashboard.getClusters().get(0).getBrokers()).isZero();
         assertThat(dashboard.getStats().getTotalClusters()).isEqualTo(1);
         assertThat(dashboard.getStats().getTotalBrokers()).isZero();
+        assertThat(dashboard.getStats().getHealthyClusters()).isZero();
     }
 
     @Test
@@ -451,6 +453,7 @@ class RocketMQDashboardProviderTest {
         assertThat(dashboard.getClusters()).singleElement().satisfies(cluster 
-> {
             assertThat(cluster.getName()).isEqualTo("cluster-without-members");
             assertThat(cluster.getBrokers()).isZero();
+            assertThat(cluster.getStatus()).isEqualTo(ClusterStatus.warning);
         });
     }
 
diff --git a/web/src/components/MetricsExplorer.tsx 
b/web/src/components/MetricsExplorer.tsx
index b9207c2cf..0c38fdfd1 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -59,8 +59,10 @@ interface NumericSample {
 
 const toNumericSamples = (series: MetricSeries): NumericSample[] =>
   series.values
-    .map((sample) => ({ timestamp: sample.timestamp, value: 
Number(sample.value) }))
-    .filter((sample) => Number.isFinite(sample.timestamp) && 
Number.isFinite(sample.value));
+    .map((sample, index) => ({ timestamp: sample.timestamp, value: 
Number(sample.value), index }))
+    .filter((sample) => Number.isFinite(sample.timestamp) && 
Number.isFinite(sample.value))
+    .sort((left, right) => left.timestamp - right.timestamp || left.index - 
right.index)
+    .map(({ timestamp, value }) => ({ timestamp, value }));
 
 const seriesLabel = (series: MetricSeries, fallback: string) => {
   const labels = Object.entries(series.labels)
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx 
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index f36f757b6..aee5234fc 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -147,6 +147,29 @@ describe('MetricsExplorer', () => {
     expect(screen.getByText('42 messages/s')).toBeInTheDocument();
   });
 
+  it('sorts provider samples before drawing and selecting the latest value', 
async () => {
+    vi.mocked(queryMetrics).mockResolvedValue({
+      ...metricData,
+      series: [
+        {
+          ...metricData.series[0],
+          values: [
+            { timestamp: 1_800_000_000, value: '42' },
+            { timestamp: 1_799_996_400, value: '40' },
+          ],
+        },
+      ],
+    });
+
+    renderWithProviders(<MetricsExplorer />);
+
+    expect(await screen.findByText('42 messages/s')).toBeInTheDocument();
+    const chart = screen.getByRole('img', { name: 'Message In TPS time series' 
});
+    const points = 
chart.querySelector('polyline')?.getAttribute('points')?.split(' ') ?? [];
+    const xCoordinates = points.map((point) => Number(point.split(',')[0]));
+    expect(xCoordinates).toEqual([...xCoordinates].sort((left, right) => left 
- right));
+  });
+
   it('updates the query window when the range changes', async () => {
     const user = userEvent.setup();
     renderWithProviders(<MetricsExplorer />);

Reply via email to