fix: recover expired dashboard metrics sessions (#1355)

This commit is contained in:
ZacharyZcR
2026-08-28 10:36:47 +08:00
committed by GitHub
parent c129666d7f
commit bf67f56c51
3 changed files with 42 additions and 6 deletions
+10 -2
View File
@@ -256,10 +256,18 @@ export async function stopMetricsPolling(
export async function sendMetricsHeartbeat(
viewerSessionId: string,
): Promise<void> {
): Promise<boolean> {
try {
await statsApi.post("/metrics/heartbeat", { viewerSessionId });
const response = await statsApi.post(
"/metrics/heartbeat",
{ viewerSessionId },
{ validateStatus: (status) => status === 200 || status === 404 },
);
return response.status !== 404;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return false;
}
handleApiError(error, "send metrics heartbeat");
throw error;
}
+12 -3
View File
@@ -1366,7 +1366,10 @@ export function DashboardTab({
try {
const existing = newSessions.get(hostId);
if (!existing) {
if (existing && !(await sendMetricsHeartbeat(existing))) {
newSessions.delete(hostId);
}
if (!newSessions.has(hostId)) {
const reg = await registerMetricsViewer(hostId);
if (reg.skipped) return null;
if (reg.success && reg.viewerSessionId) {
@@ -1489,8 +1492,14 @@ export function DashboardTab({
if (!isVisible || viewerSessionsRef.current.size === 0) return;
const heartbeat = setInterval(async () => {
if (document.visibilityState === "hidden") return;
for (const [, sessionId] of viewerSessionsRef.current) {
sendMetricsHeartbeat(sessionId).catch(() => {});
for (const [hostId, sessionId] of viewerSessionsRef.current) {
sendMetricsHeartbeat(sessionId)
.then((alive) => {
if (!alive && viewerSessionsRef.current.get(hostId) === sessionId) {
viewerSessionsRef.current.delete(hostId);
}
})
.catch(() => {});
}
}, 30000);
return () => clearInterval(heartbeat);
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const statsApiMock = vi.hoisted(() => ({ get: vi.fn() }));
const statsApiMock = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() }));
const remoteStatsApiMock = vi.hoisted(() => ({ get: vi.fn() }));
const sshHostApiMock = vi.hoisted(() => ({ get: vi.fn() }));
const resolveConnectionOriginMock = vi.hoisted(() => vi.fn());
@@ -24,6 +24,7 @@ vi.mock("@/lib/connection-origin", () => ({
import {
getAllServerStatuses,
getServerMetricsById,
sendMetricsHeartbeat,
} from "../../api/host-metrics-status-api";
beforeEach(() => {
@@ -98,3 +99,21 @@ describe("metrics request coalescing", () => {
expect(statsApiMock.get).toHaveBeenCalledOnce();
});
});
describe("metrics viewer heartbeat", () => {
it("reports a swept viewer session without throwing or logging", async () => {
statsApiMock.post.mockResolvedValueOnce({ status: 404 });
await expect(sendMetricsHeartbeat("expired-viewer")).resolves.toBe(false);
expect(statsApiMock.post).toHaveBeenCalledWith(
"/metrics/heartbeat",
{ viewerSessionId: "expired-viewer" },
{ validateStatus: expect.any(Function) },
);
});
it("keeps a live viewer session", async () => {
statsApiMock.post.mockResolvedValueOnce({ status: 200 });
await expect(sendMetricsHeartbeat("live-viewer")).resolves.toBe(true);
});
});