From d3fa9c08d612dbdb89027e1135abd0a3faea3dca Mon Sep 17 00:00:00 2001 From: Sunny Date: Fri, 23 Oct 2026 12:11:00 +0530 Subject: [PATCH 1/2] fix(dashboard): distinguish disabled observability from no traffic Signed-off-by: Sunny --- openapi.json | 10 +++++++ src/api/metrics.ts | 3 +- .../dashboard/SystemSparklinesCard.test.tsx | 29 +++++++++++++++---- .../dashboard/SystemSparklinesCard.tsx | 10 +++++-- .../SystemSparklinesCardConnected.test.tsx | 7 +++-- src/hooks/useMetrics.test.ts | 17 +++++++++-- src/hooks/useMetrics.ts | 3 +- src/i18n/locales/en-US/dashboard.json | 3 +- src/i18n/locales/es-ES/dashboard.json | 3 +- src/i18n/locales/pt-BR/dashboard.json | 3 +- src/types/metrics.ts | 27 ----------------- 11 files changed, 70 insertions(+), 45 deletions(-) delete mode 100644 src/types/metrics.ts diff --git a/openapi.json b/openapi.json index 1f2e32f4..aa9b91ae 100644 --- a/openapi.json +++ b/openapi.json @@ -49761,6 +49761,11 @@ }, "type": "array", "title": "P99" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true } }, "type": "object", @@ -55433,6 +55438,11 @@ }, "type": "array", "title": "Error Count" + }, + "enabled": { + "type": "boolean", + "title": "Enabled", + "default": true } }, "type": "object", diff --git a/src/api/metrics.ts b/src/api/metrics.ts index 82c278b9..9466ae55 100644 --- a/src/api/metrics.ts +++ b/src/api/metrics.ts @@ -6,7 +6,8 @@ * and `platform_viewer` hold it by default, so a 403 is a routine outcome. */ -import type { PercentilesResponse, TimeseriesResponse } from "@/types/metrics"; +import type { PercentilesResponse } from "@/generated/types/percentilesResponse"; +import type { TimeseriesResponse } from "@/generated/types/timeseriesResponse"; import { api } from "./client"; diff --git a/src/components/dashboard/SystemSparklinesCard.test.tsx b/src/components/dashboard/SystemSparklinesCard.test.tsx index c2dcfd91..5bb4e3d6 100644 --- a/src/components/dashboard/SystemSparklinesCard.test.tsx +++ b/src/components/dashboard/SystemSparklinesCard.test.tsx @@ -33,7 +33,12 @@ describe("SystemSparklinesCard", () => { it("totals executions across the window and takes the latest latency bucket", () => { renderWithProviders( { expect(screen.getByText("0.523ms")).toBeInTheDocument(); }); - it("explains an empty window rather than showing it as a failure", () => { + it("explains when observability is disabled", () => { + renderWithProviders( + , + ); + + expect(screen.getByText(/Tracing is off/)).toBeInTheDocument(); + expect(screen.queryByText(/No MCP or A2A requests/)).not.toBeInTheDocument(); + }); + + it("explains an enabled window with no traffic", () => { renderWithProviders( , ); - expect(screen.getByText(/No traced requests/)).toBeInTheDocument(); - expect(screen.queryByText("Couldn't load metrics.")).not.toBeInTheDocument(); + expect(screen.getByText(/No MCP or A2A requests/)).toBeInTheDocument(); + expect(screen.queryByText(/Tracing is off/)).not.toBeInTheDocument(); }); it("surfaces a load failure", () => { diff --git a/src/components/dashboard/SystemSparklinesCard.tsx b/src/components/dashboard/SystemSparklinesCard.tsx index 97144f15..3c0242d5 100644 --- a/src/components/dashboard/SystemSparklinesCard.tsx +++ b/src/components/dashboard/SystemSparklinesCard.tsx @@ -9,7 +9,8 @@ import { useMemo } from "react"; import { useIntl } from "react-intl"; -import type { PercentilesResponse, TimeseriesResponse } from "@/types/metrics"; +import type { PercentilesResponse } from "@/generated/types/percentilesResponse"; +import type { TimeseriesResponse } from "@/generated/types/timeseriesResponse"; import { WINDOW_HOURS, WINDOW_INTERVAL_MINUTES } from "@/hooks/useMetrics"; import { SparklineRow } from "./SparklineRow"; @@ -87,7 +88,12 @@ export function SystemSparklinesCard({ const statusMessage = error ? intl.formatMessage({ id: "dashboard.home.sparklines.error" }) : isEmpty - ? intl.formatMessage({ id: "dashboard.home.sparklines.empty" }) + ? intl.formatMessage({ + id: + timeseries?.enabled === false || percentiles?.enabled === false + ? "dashboard.home.sparklines.empty.disabled" + : "dashboard.home.sparklines.empty.noTraffic", + }) : null; return ( diff --git a/src/components/dashboard/SystemSparklinesCardConnected.test.tsx b/src/components/dashboard/SystemSparklinesCardConnected.test.tsx index bce7ecfd..3023dcdb 100644 --- a/src/components/dashboard/SystemSparklinesCardConnected.test.tsx +++ b/src/components/dashboard/SystemSparklinesCardConnected.test.tsx @@ -45,7 +45,7 @@ describe("SystemSparklinesCardConnected", () => { it("renders the card otherwise", () => { mockUseMetrics.mockReturnValue( metricsState({ - timeseries: { buckets: [], values: [] }, + timeseries: { buckets: [], values: [], success_count: [], error_count: [] }, percentiles: { buckets: [], p50: [], p95: [], p99: [] }, }), ); @@ -57,7 +57,10 @@ describe("SystemSparklinesCardConnected", () => { it("only shows the loading state on the first load, not on a refresh", () => { mockUseMetrics.mockReturnValue( - metricsState({ isLoading: true, timeseries: { buckets: [], values: [] } }), + metricsState({ + isLoading: true, + timeseries: { buckets: [], values: [], success_count: [], error_count: [] }, + }), ); const { container } = renderWithProviders(); diff --git a/src/hooks/useMetrics.test.ts b/src/hooks/useMetrics.test.ts index c64e1893..e87a8154 100644 --- a/src/hooks/useMetrics.test.ts +++ b/src/hooks/useMetrics.test.ts @@ -13,7 +13,7 @@ const mockTimeseries = vi.mocked(metricsApi.getTimeseries); const mockPercentiles = vi.mocked(metricsApi.getPercentiles); function resolveBoth() { - mockTimeseries.mockResolvedValue({ buckets: [], values: [] }); + mockTimeseries.mockResolvedValue({ buckets: [], values: [], success_count: [], error_count: [] }); mockPercentiles.mockResolvedValue({ buckets: [], p50: [], p95: [], p99: [] }); } @@ -104,7 +104,13 @@ describe("useMetrics", () => { mockTimeseries.mockImplementationOnce( () => new Promise((resolve) => { - release = () => resolve({ buckets: ["2026-09-01T12:00:00Z"], values: [9] }); + release = () => + resolve({ + buckets: ["2026-09-01T12:00:00Z"], + values: [9], + success_count: [0], + error_count: [0], + }); }), ); @@ -118,7 +124,12 @@ describe("useMetrics", () => { }); await waitFor(() => expect(result.current.isLoading).toBe(false)); - expect(result.current.timeseries).toEqual({ buckets: [], values: [] }); + expect(result.current.timeseries).toEqual({ + buckets: [], + values: [], + success_count: [], + error_count: [], + }); }); it("refetches on becoming visible when nothing has loaded yet", async () => { diff --git a/src/hooks/useMetrics.ts b/src/hooks/useMetrics.ts index 9422b02e..e7aad1a6 100644 --- a/src/hooks/useMetrics.ts +++ b/src/hooks/useMetrics.ts @@ -10,7 +10,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { metricsApi } from "@/api/metrics"; -import type { PercentilesResponse, TimeseriesResponse } from "@/types/metrics"; +import type { PercentilesResponse } from "@/generated/types/percentilesResponse"; +import type { TimeseriesResponse } from "@/generated/types/timeseriesResponse"; /** 24h at 60m granularity = 24 buckets. Fixed: the card has no range control. */ export const WINDOW_HOURS = 24; diff --git a/src/i18n/locales/en-US/dashboard.json b/src/i18n/locales/en-US/dashboard.json index cd593385..a781a2a5 100644 --- a/src/i18n/locales/en-US/dashboard.json +++ b/src/i18n/locales/en-US/dashboard.json @@ -93,7 +93,8 @@ "dashboard.home.sparklines.p95": "p95 latency", "dashboard.home.sparklines.p99": "p99 latency", "dashboard.home.sparklines.error": "Couldn't load metrics.", - "dashboard.home.sparklines.empty": "No traced requests. If you haven't already, set OBSERVABILITY_ENABLED=true and restart ContextForge.", + "dashboard.home.sparklines.empty.disabled": "Tracing is off. Set OBSERVABILITY_ENABLED=true on the ContextForge server and restart.", + "dashboard.home.sparklines.empty.noTraffic": "No MCP or A2A requests in this window.", "dashboard.home.sparklines.tooltip.noRequests": "No requests", "dashboard.home.sparklines.tooltip.requests": "{count, plural, one {# request} other {# requests}}", "dashboard.home.sparklines.tooltip.executions": "{count, plural, one {# execution} other {# executions}}", diff --git a/src/i18n/locales/es-ES/dashboard.json b/src/i18n/locales/es-ES/dashboard.json index 451c266e..39baafb6 100644 --- a/src/i18n/locales/es-ES/dashboard.json +++ b/src/i18n/locales/es-ES/dashboard.json @@ -93,7 +93,8 @@ "dashboard.home.sparklines.p95": "Latencia p95", "dashboard.home.sparklines.p99": "Latencia p99", "dashboard.home.sparklines.error": "No se pudieron cargar las métricas.", - "dashboard.home.sparklines.empty": "Sin solicitudes rastreadas. Si aún no lo ha hecho, establezca OBSERVABILITY_ENABLED=true y reinicie ContextForge.", + "dashboard.home.sparklines.empty.disabled": "El seguimiento está desactivado. Establezca OBSERVABILITY_ENABLED=true en el servidor ContextForge y reinícielo.", + "dashboard.home.sparklines.empty.noTraffic": "No hay solicitudes MCP ni A2A en este período.", "dashboard.home.sparklines.tooltip.noRequests": "Sin solicitudes", "dashboard.home.sparklines.tooltip.requests": "{count, plural, one {# solicitud} other {# solicitudes}}", "dashboard.home.sparklines.tooltip.executions": "{count, plural, one {# ejecución} other {# ejecuciones}}", diff --git a/src/i18n/locales/pt-BR/dashboard.json b/src/i18n/locales/pt-BR/dashboard.json index 4effdfef..6834e965 100644 --- a/src/i18n/locales/pt-BR/dashboard.json +++ b/src/i18n/locales/pt-BR/dashboard.json @@ -93,7 +93,8 @@ "dashboard.home.sparklines.p95": "Latência p95", "dashboard.home.sparklines.p99": "Latência p99", "dashboard.home.sparklines.error": "Não foi possível carregar as métricas.", - "dashboard.home.sparklines.empty": "Nenhuma requisição rastreada. Se ainda não tiver feito isso, defina OBSERVABILITY_ENABLED=true e reinicie o ContextForge.", + "dashboard.home.sparklines.empty.disabled": "O rastreamento está desativado. Defina OBSERVABILITY_ENABLED=true no servidor ContextForge e reinicie-o.", + "dashboard.home.sparklines.empty.noTraffic": "Não há requisições MCP ou A2A neste período.", "dashboard.home.sparklines.tooltip.noRequests": "Sem requisições", "dashboard.home.sparklines.tooltip.requests": "{count, plural, one {# requisição} other {# requisições}}", "dashboard.home.sparklines.tooltip.executions": "{count, plural, one {# execução} other {# execuções}}", diff --git a/src/types/metrics.ts b/src/types/metrics.ts deleted file mode 100644 index 067752f7..00000000 --- a/src/types/metrics.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Observability metrics types. `mcpgateway/routers/observability.py` is the - * source of truth. - * - * `buckets` is sparse — only intervals containing traces are returned, so a 24h - * window with three busy hours yields three entries. Align onto a full grid - * before plotting; see `components/dashboard/sparklineSeries.ts`. - * - * Both endpoints return empty arrays, not an error, when observability is off. - */ - -/** Execution counts bucketed over time. */ -export interface TimeseriesResponse { - /** ISO 8601 bucket start times, ascending. Sparse. */ - buckets: string[]; - /** Execution count per bucket, index-aligned with `buckets`. */ - values: number[]; -} - -/** Latency percentiles (milliseconds) bucketed over time. */ -export interface PercentilesResponse { - /** ISO 8601 bucket start times, ascending. Sparse. */ - buckets: string[]; - p50: number[]; - p95: number[]; - p99: number[]; -} From b46e077a1dc5181f81cb9e74203ca980f4aa3cf8 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 24 Oct 2026 09:35:45 +0530 Subject: [PATCH 2/2] fix(dashboard): preserve unknown observability state Signed-off-by: Sunny --- openapi.json | 10 ---------- .../dashboard/SystemSparklinesCard.test.tsx | 13 +++++++++++++ .../dashboard/SystemSparklinesCard.tsx | 17 ++++++++++++----- src/i18n/locales/en-US/dashboard.json | 1 + src/i18n/locales/es-ES/dashboard.json | 1 + src/i18n/locales/pt-BR/dashboard.json | 1 + 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openapi.json b/openapi.json index aa9b91ae..1f2e32f4 100644 --- a/openapi.json +++ b/openapi.json @@ -49761,11 +49761,6 @@ }, "type": "array", "title": "P99" - }, - "enabled": { - "type": "boolean", - "title": "Enabled", - "default": true } }, "type": "object", @@ -55438,11 +55433,6 @@ }, "type": "array", "title": "Error Count" - }, - "enabled": { - "type": "boolean", - "title": "Enabled", - "default": true } }, "type": "object", diff --git a/src/components/dashboard/SystemSparklinesCard.test.tsx b/src/components/dashboard/SystemSparklinesCard.test.tsx index 5bb4e3d6..5219bb13 100644 --- a/src/components/dashboard/SystemSparklinesCard.test.tsx +++ b/src/components/dashboard/SystemSparklinesCard.test.tsx @@ -78,6 +78,19 @@ describe("SystemSparklinesCard", () => { expect(screen.queryByText(/Tracing is off/)).not.toBeInTheDocument(); }); + it("uses the original empty-state message when observability status is unknown", () => { + renderWithProviders( + , + ); + + expect(screen.getByText(/set OBSERVABILITY_ENABLED=true/)).toBeInTheDocument(); + expect(screen.queryByText(/Tracing is off/)).not.toBeInTheDocument(); + expect(screen.queryByText(/No MCP or A2A requests/)).not.toBeInTheDocument(); + }); + it("surfaces a load failure", () => { renderWithProviders( , diff --git a/src/components/dashboard/SystemSparklinesCard.tsx b/src/components/dashboard/SystemSparklinesCard.tsx index 3c0242d5..a96ea075 100644 --- a/src/components/dashboard/SystemSparklinesCard.tsx +++ b/src/components/dashboard/SystemSparklinesCard.tsx @@ -24,9 +24,11 @@ import { } from "./sparklineSeries"; import { formatCount, formatResponseTime } from "./systemMetrics"; +type ObservabilityStatus = { enabled?: boolean }; + interface SystemSparklinesCardProps { - timeseries: TimeseriesResponse | null; - percentiles: PercentilesResponse | null; + timeseries: (TimeseriesResponse & ObservabilityStatus) | null; + percentiles: (PercentilesResponse & ObservabilityStatus) | null; loading?: boolean; error?: Error | null; } @@ -84,15 +86,20 @@ export function SystemSparklinesCard({ const isEmpty = !loading && !error && rows.every((row) => isEmptySeries(row.points)); + // APIs older than IBM/mcp-context-forge#7326 report neither state. + const tracingEnabled = timeseries?.enabled ?? percentiles?.enabled; + // Sits under the title const statusMessage = error ? intl.formatMessage({ id: "dashboard.home.sparklines.error" }) : isEmpty ? intl.formatMessage({ id: - timeseries?.enabled === false || percentiles?.enabled === false - ? "dashboard.home.sparklines.empty.disabled" - : "dashboard.home.sparklines.empty.noTraffic", + tracingEnabled === undefined + ? "dashboard.home.sparklines.empty" + : tracingEnabled + ? "dashboard.home.sparklines.empty.noTraffic" + : "dashboard.home.sparklines.empty.disabled", }) : null; diff --git a/src/i18n/locales/en-US/dashboard.json b/src/i18n/locales/en-US/dashboard.json index a781a2a5..1e8984a1 100644 --- a/src/i18n/locales/en-US/dashboard.json +++ b/src/i18n/locales/en-US/dashboard.json @@ -93,6 +93,7 @@ "dashboard.home.sparklines.p95": "p95 latency", "dashboard.home.sparklines.p99": "p99 latency", "dashboard.home.sparklines.error": "Couldn't load metrics.", + "dashboard.home.sparklines.empty": "No traced requests. If you haven't already, set OBSERVABILITY_ENABLED=true and restart ContextForge.", "dashboard.home.sparklines.empty.disabled": "Tracing is off. Set OBSERVABILITY_ENABLED=true on the ContextForge server and restart.", "dashboard.home.sparklines.empty.noTraffic": "No MCP or A2A requests in this window.", "dashboard.home.sparklines.tooltip.noRequests": "No requests", diff --git a/src/i18n/locales/es-ES/dashboard.json b/src/i18n/locales/es-ES/dashboard.json index 39baafb6..5aca6c20 100644 --- a/src/i18n/locales/es-ES/dashboard.json +++ b/src/i18n/locales/es-ES/dashboard.json @@ -93,6 +93,7 @@ "dashboard.home.sparklines.p95": "Latencia p95", "dashboard.home.sparklines.p99": "Latencia p99", "dashboard.home.sparklines.error": "No se pudieron cargar las métricas.", + "dashboard.home.sparklines.empty": "Sin solicitudes rastreadas. Si aún no lo ha hecho, establezca OBSERVABILITY_ENABLED=true y reinicie ContextForge.", "dashboard.home.sparklines.empty.disabled": "El seguimiento está desactivado. Establezca OBSERVABILITY_ENABLED=true en el servidor ContextForge y reinícielo.", "dashboard.home.sparklines.empty.noTraffic": "No hay solicitudes MCP ni A2A en este período.", "dashboard.home.sparklines.tooltip.noRequests": "Sin solicitudes", diff --git a/src/i18n/locales/pt-BR/dashboard.json b/src/i18n/locales/pt-BR/dashboard.json index 6834e965..77976eb4 100644 --- a/src/i18n/locales/pt-BR/dashboard.json +++ b/src/i18n/locales/pt-BR/dashboard.json @@ -93,6 +93,7 @@ "dashboard.home.sparklines.p95": "Latência p95", "dashboard.home.sparklines.p99": "Latência p99", "dashboard.home.sparklines.error": "Não foi possível carregar as métricas.", + "dashboard.home.sparklines.empty": "Nenhuma requisição rastreada. Se ainda não tiver feito isso, defina OBSERVABILITY_ENABLED=true e reinicie o ContextForge.", "dashboard.home.sparklines.empty.disabled": "O rastreamento está desativado. Defina OBSERVABILITY_ENABLED=true no servidor ContextForge e reinicie-o.", "dashboard.home.sparklines.empty.noTraffic": "Não há requisições MCP ou A2A neste período.", "dashboard.home.sparklines.tooltip.noRequests": "Sem requisições",