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 6817f30f5 fix(metrics): survive blocked browser storage (#5728)
6817f30f5 is described below
commit 6817f30f5c113e56d58b70be1ca10901a2252d01
Author: 0 <[email protected]>
AuthorDate: Mon Oct 19 18:14:10 2026 +0800
fix(metrics): survive blocked browser storage (#5728)
Verified against `rocketmq-studio` at 86aa49c34 before merging: 缺陷仍在。主干
`web/src/components/MetricsExplorer.tsx` 恰好 6 处裸
`localStorage`(`:729`、`:755`、`:901`、`:903`、`:1179`、`:1207`),数目与基线一致,主干这 100
个提交**没有新增第 7 处**;
merge-tree clean against the current trunk and no outstanding must-fix
items; merged as-is (squash) per the maintainer review of 2026-10-19.
---
web/src/components/MetricsExplorer.tsx | 13 ++++++------
.../components/__tests__/MetricsExplorer.test.tsx | 23 ++++++++++++++++++++++
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/web/src/components/MetricsExplorer.tsx
b/web/src/components/MetricsExplorer.tsx
index a33d2ec86..37001e872 100644
--- a/web/src/components/MetricsExplorer.tsx
+++ b/web/src/components/MetricsExplorer.tsx
@@ -55,6 +55,7 @@ import type { MetricData, MetricMapping, MetricProfile } from
'../api/metrics';
import { useLang } from '../i18n/LangContext';
import { downloadCsv } from '../utils/download';
import { tableScrollX } from '../utils/table';
+import { readLocalStorage, removeLocalStorage, writeLocalStorage } from
'../utils/browserStorage';
import {
buildMetricCsvFilename,
buildMetricCsvFromRows,
@@ -726,7 +727,7 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
// A re-run after an earlier failure must clear the error banner, or
the recovered
// panels below would stay hidden behind it until the component is
remounted.
setProfileError(false);
- const storedProfileId = localStorage.getItem(PROFILE_STORAGE_KEY);
+ const storedProfileId = readLocalStorage(PROFILE_STORAGE_KEY);
const initialProfile =
nextProfiles.find((profile) => profile.id === storedProfileId) ??
nextProfiles[0];
setProfileId(initialProfile?.id ?? '');
@@ -752,7 +753,7 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
const handleProfileChange = (nextProfileId: string) => {
const nextProfile = profiles.find((profile) => profile.id ===
nextProfileId);
- localStorage.setItem(PROFILE_STORAGE_KEY, nextProfileId);
+ writeLocalStorage(PROFILE_STORAGE_KEY, nextProfileId);
setProfileId(nextProfileId);
void loadAll(nextProfile, selectedRange);
};
@@ -898,9 +899,9 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
rangeIdRef.current = replay.checkpoint.rangeId;
setCustomPromql(replay.checkpoint.customPromql);
if (replay.checkpoint.storedProfileId === null) {
- localStorage.removeItem(PROFILE_STORAGE_KEY);
+ removeLocalStorage(PROFILE_STORAGE_KEY);
} else {
- localStorage.setItem(PROFILE_STORAGE_KEY,
replay.checkpoint.storedProfileId);
+ writeLocalStorage(PROFILE_STORAGE_KEY,
replay.checkpoint.storedProfileId);
}
}
setPendingDataSource(null);
@@ -1176,7 +1177,7 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
profileId,
rangeId,
customPromql,
- storedProfileId: localStorage.getItem(PROFILE_STORAGE_KEY),
+ storedProfileId: readLocalStorage(PROFILE_STORAGE_KEY),
};
if (entry.profileId === CUSTOM_HISTORY_PROFILE_ID) {
setRangeId(nextRange.value);
@@ -1204,7 +1205,7 @@ const MetricsExplorer = ({ instanceId }:
MetricsExplorerProps) => {
setRangeId(nextRange.value);
setHistoryOpen(false);
- localStorage.setItem(PROFILE_STORAGE_KEY, nextProfile.id);
+ writeLocalStorage(PROFILE_STORAGE_KEY, nextProfile.id);
setProfileId(nextProfile.id);
if (nextDataSource && getDataSourceAuthMode(nextDataSource.auth) !==
'none') {
restoreProtectedDataSource(
diff --git a/web/src/components/__tests__/MetricsExplorer.test.tsx
b/web/src/components/__tests__/MetricsExplorer.test.tsx
index f2fa1c033..a3d2cbcd3 100644
--- a/web/src/components/__tests__/MetricsExplorer.test.tsx
+++ b/web/src/components/__tests__/MetricsExplorer.test.tsx
@@ -284,6 +284,29 @@ describe('MetricsExplorer', () => {
expect(screen.getByText('42 messages/s')).toBeInTheDocument();
});
+ it('still loads the profiles when storage access is blocked', async () => {
+ // Safari private mode and hardened cookie policies make localStorage
access throw;
+ // utils/browserStorage exists so every other storage consumer survives
that. The
+ // explorer must not turn a blocked read into a "profiles failed to load"
page.
+ const blocked = () => {
+ throw new DOMException('Access is denied for this document',
'SecurityError');
+ };
+ const getItemSpy = vi.spyOn(Storage.prototype,
'getItem').mockImplementation(blocked);
+ const setItemSpy = vi.spyOn(Storage.prototype,
'setItem').mockImplementation(blocked);
+ const removeItemSpy = vi.spyOn(Storage.prototype,
'removeItem').mockImplementation(blocked);
+
+ renderWithProviders(<MetricsExplorer />);
+
+ expect(await screen.findByText('RocketMQ 5.x Native')).toBeInTheDocument();
+ await waitFor(() =>
+ expect(screen.getByRole('img', { name: 'Message In TPS time series'
})).toBeInTheDocument(),
+ );
+
+ getItemSpy.mockRestore();
+ setItemSpy.mockRestore();
+ removeItemSpy.mockRestore();
+ });
+
it('sorts provider samples before drawing and selecting the latest value',
async () => {
vi.mocked(queryMetrics).mockResolvedValue({
...metricData,