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 />);