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 62d0ad92e fix(alerts): count 24h triggers from the UTC stamp (#5723)
62d0ad92e is described below

commit 62d0ad92e931de46a2fc944ae5d970e6fc3d7311
Author: 0 <[email protected]>
AuthorDate: Mon Oct 19 20:52:10 2026 +0800

    fix(alerts): count 24h triggers from the UTC stamp (#5723)
    
    Verified against `rocketmq-studio` at e1577bd31 before merging: the defect 
is still present on the
    current trunk and the change merges cleanly.
    
    Merged as-is (squash); no maintainer edits were needed.
    
    Locally verified: targeted suites green on the integration branch; a full 
backend and frontend
    build plus test pass runs on the trunk once this batch lands.
---
 web/src/pages/ops/__tests__/AlertsPage.test.tsx | 23 ++++++++++++++++++++
 web/src/pages/ops/alerts.tsx                    | 12 +++++++----
 web/src/utils/format.test.ts                    | 28 +++++++++++++++++++++++++
 web/src/utils/format.ts                         | 18 ++++++++++++++++
 4 files changed, 77 insertions(+), 4 deletions(-)

diff --git a/web/src/pages/ops/__tests__/AlertsPage.test.tsx 
b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
index 3f25253cf..9fa63dad7 100644
--- a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
@@ -1205,6 +1205,29 @@ describe('AlertsPage', () => {
     expect(screen.queryByText('Page one rule 1')).not.toBeInTheDocument();
   });
 
+  it('counts the 24h-triggered stat against the UTC timestamp, not the browser 
zone', async () => {
+    // Pin the viewer zone off UTC so a browser-zone parse is observably 
wrong, and pin the
+    // clock so the fixture's age is exact. The stat must read the same value 
the adjacent
+    // Last-Triggered column renders — UTC, per the formatUtcDateTime 
convention.
+    vi.stubEnv('TZ', 'Asia/Shanghai');
+    const nowSpy = vi.spyOn(Date, 
'now').mockReturnValue(Date.parse('2026-08-24T12:00:00Z'));
+    try {
+      // 2026-08-23T13:00:00 UTC is 23h before the pinned now. Parsed as 
Shanghai wall time
+      // it becomes 05:00Z — 31h old — which is what the browser-zone parse 
wrongly excludes.
+      vi.mocked(listAlertRulesPage).mockResolvedValue(
+        pageResult([{ ...cloneRule(alertRules[0]), lastTriggered: 
'2026-08-23T13:00:00' }]),
+      );
+
+      renderPage();
+
+      const statLabel = await screen.findByText('本页 24h 触发');
+      expect(statLabel.nextElementSibling).toHaveTextContent('1');
+    } finally {
+      nowSpy.mockRestore();
+      vi.unstubAllEnvs();
+    }
+  });
+
   it('clamps to the last valid page when a bulk delete empties the current 
one', async () => {
     // 21 rules: page 2 holds only rule 21. Deleting it leaves 20 rules across 
1 page, so the
     // refresh of page 2 comes back empty and the view must clamp to the last 
valid page (1).
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index f81e20487..044527b97 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -66,7 +66,7 @@ import {
 } from '../../services/opsService';
 import { attachThresholdUnit, normalizeDuration, normalizeMetric } from 
'./alertRulePayload';
 import { tableScrollX } from '../../utils/table';
-import { formatUtcDateTime, ratioToPercent } from '../../utils/format';
+import { formatUtcDateTime, parseAlertTimestamp, ratioToPercent } from 
'../../utils/format';
 import { listInstances } from '../../services/instanceService';
 import type { Instance } from '../../api/instance';
 import { downloadBlob } from '../../utils/download';
@@ -415,9 +415,13 @@ const AlertsPageContent = ({ domain = 'CLUSTER' }: 
AlertsPageProps) => {
 
   // eslint-disable-next-line react-hooks/purity
   const dayAgo = Date.now() - 24 * 60 * 60 * 1000;
-  const triggered24h = rules.filter(
-    (r) => r.lastTriggered && new Date(r.lastTriggered).getTime() > dayAgo,
-  ).length;
+  // lastTriggered is a UTC LocalDateTime without an offset suffix (see the 
column below), so the
+  // comparison must parse it as UTC — the same convention formatUtcDateTime 
renders it with.
+  const triggered24h = rules.filter((r) => {
+    if (!r.lastTriggered) return false;
+    const triggeredAt = parseAlertTimestamp(r.lastTriggered);
+    return Number.isFinite(triggeredAt) && triggeredAt > dayAgo;
+  }).length;
 
   const openCreateModal = () => {
     setEditingRule(null);
diff --git a/web/src/utils/format.test.ts b/web/src/utils/format.test.ts
index de99180dd..8e7b992b0 100644
--- a/web/src/utils/format.test.ts
+++ b/web/src/utils/format.test.ts
@@ -11,6 +11,7 @@ import {
   formatRelativeTime,
   formatTimeOfDay,
   formatTimeMs,
+  parseAlertTimestamp,
   ratioToPercent,
   roundPercent,
 } from './format';
@@ -120,6 +121,33 @@ describe('formatBytes', () => {
   });
 });
 
+describe('parseAlertTimestamp', () => {
+  it('parses an offset-less alert timestamp as UTC regardless of the browser 
zone', () => {
+    // Expressed against Date.parse with an explicit Z so the expectation 
cannot drift with TZ.
+    
expect(parseAlertTimestamp('2026-08-23T10:35:38')).toBe(Date.parse('2026-08-23T10:35:38Z'));
+    expect(parseAlertTimestamp('2026-08-23 
10:35:38')).toBe(Date.parse('2026-08-23T10:35:38Z'));
+  });
+
+  it('honors an explicit offset instead of assuming UTC', () => {
+    
expect(parseAlertTimestamp('2026-08-23T10:35:38Z')).toBe(Date.parse('2026-08-23T10:35:38Z'));
+    expect(parseAlertTimestamp('2026-08-23T10:35:38+08:00')).toBe(
+      Date.parse('2026-08-23T10:35:38+08:00'),
+    );
+    expect(parseAlertTimestamp('2026-08-23T10:35:38-05:00')).toBe(
+      Date.parse('2026-08-23T10:35:38-05:00'),
+    );
+  });
+
+  it('passes Date objects through and reports unusable values as NaN', () => {
+    const date = new Date(Date.parse('2026-08-23T10:35:38Z'));
+    expect(parseAlertTimestamp(date)).toBe(date.getTime());
+    expect(parseAlertTimestamp('not a timestamp')).toBeNaN();
+    expect(parseAlertTimestamp('')).toBeNaN();
+    expect(parseAlertTimestamp(null)).toBeNaN();
+    expect(parseAlertTimestamp(undefined)).toBeNaN();
+  });
+});
+
 describe('formatTimeMs', () => {
   it.each([8_640_000_000_000_001, -8_640_000_000_000_001, Number.MAX_VALUE, 
-Number.MAX_VALUE])(
     'uses the placeholder for finite timestamps outside the Date range: %s',
diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts
index 90084a997..b9e96e7c1 100644
--- a/web/src/utils/format.ts
+++ b/web/src/utils/format.ts
@@ -49,6 +49,24 @@ export function formatTimeMs(value: number | string | null | 
undefined): string
   return `${formatDateTime(date)}.${pad(date.getMilliseconds(), 3)}`;
 }
 
+/**
+ * Epoch milliseconds of a backend alert timestamp, parsed the way the backend 
stamps it.
+ *
+ * Alert APIs serialize UTC `LocalDateTime` values without an offset, so a 
plain
+ * `new Date(value)` would read them in the browser's zone. An explicit offset 
(`Z` or
+ * `±hh:mm`) is honored; unusable input yields `NaN` so callers can treat it 
as absent.
+ */
+export function parseAlertTimestamp(date: string | Date | null | undefined): 
number {
+  if (date === null || date === undefined || (typeof date === 'string' && 
!date.trim())) {
+    return Number.NaN;
+  }
+  const utcDate =
+    typeof date === 'string' && !/(?:Z|[+-]\d{2}:?\d{2})$/i.test(date.trim())
+      ? new Date(`${date}Z`)
+      : new Date(date);
+  return utcDate.getTime();
+}
+
 /**
  * Format a UTC timestamp for alert events in the viewer's timezone. Alert APIs
  * serialize UTC LocalDateTime values without an offset, so normal Date parsing

Reply via email to