Dashboards Reliability Smoke Test — Fase 1
Manual verification that the Fase 1 reliability floor (1.A–1.E) holds end to end. Run in a dev environment with a dashboard that has at least one widget bound to a working dataset. Keep DevTools open (Network, Console) and, if available, the Prometheus/Sentry views.
All six cases must pass before shipping Fase 1.
1 — Widget crash is isolated (WidgetErrorBoundary)
Setup: open a dashboard with 3+ widgets. Temporarily add throw new Error('boom')
at the top of WidgetChart (behind if (widget.id === '<one-id>')) to force one
widget to crash. Reload.
Expected:
- The crashing widget shows the red-accented fallback: icon, "Widget error",
RetryandCopy IDbuttons, monospace Error ID underneath. - The widget header (title + Remove button) stays visible so the user can still delete the broken widget.
- Other widgets in the canvas keep rendering normally.
- Clicking
Copy IDshows "Copied" for ~1.8s and puts the UUID in the clipboard. - Clicking
Retryre-mounts the children; if you undo thethrow, the widget recovers without a full reload. - Prometheus:
dashboard_client_errors_total{scope="widget",widget_type="<type>",source_kind="<kind>"}increments. - Sentry: a
DashboardError[widget]event exists with the same Error ID as the copied UUID.
2 — Backend 500 triggers one retry, then surfaces the error
Setup: in DevTools Network, override /api/dashboards/query with a 500
response (right-click → "Block request URL" doesn't work here; use a proxy or
temporarily return new Response(null, { status: 500 }) in the route handler).
Expected:
- The widget shows the loading skeleton, then the "Query error" placeholder with the server message as hint.
- Network tab: two POSTs to
/api/dashboards/querywithin ~600ms of each other (the second is the automatic retry with jittered backoff). - Prometheus:
dashboard_query_errors_total{error_type="server"}increments. - Removing the 500 override and clicking around to re-trigger the fetch recovers normally (no stuck state).
3 — 15 s timeout fires cleanly
Setup: in a proxy / MSW mock, make /api/dashboards/query hang indefinitely.
Expected:
- Widget shows loading skeleton for 15 s.
- After 15 s, it flips to the error placeholder with message
"Query timed out after 15s"— no infinite spinner. - Network tab: request is marked as cancelled/aborted at ~15 s; no third or fourth request (we do not retry timeouts).
- Prometheus:
dashboard_query_errors_total{error_type="timeout"}increments. - Switching to a different dashboard and back shows normal behaviour (no leaked pending request, no memory leak in DevTools Memory).
4 — Save race condition is prevented
Setup: in the Builder, make a small edit (rename a widget). The Save button lights up in unsaved state. Open Network tab.
Expected (double-click):
- Double-click Save rapidly → exactly one PUT to
/api/projectsfires. The second click is swallowed by thesaveStatus === 'saving'lock. - While saving, the Save button shows the spinner and is disabled.
- On success: button transitions to green "Saved" for 2 s, then back to neutral "Saved".
Expected (Ctrl+S spam):
- Hold Ctrl+S for a second (fires the keydown ~20× depending on repeat rate) → only one PUT fires.
Expected (create modal doubled-click):
- In the "New dashboard" modal, spam-click Save → exactly one POST to
/api/projects. - Retrying after a failure reuses the same
Idempotency-Keyheader (check the request headers — both attempts carry the same UUID).
5 — Network failure surfaces human message, doesn't lose work
Setup: in the Builder, make an edit. Before clicking Save, disable the network (DevTools → Network tab → "Offline").
Expected:
- Click Save → banner appears next to the Save button with
"Network issue — check your connection and try again." - Save button flips to red "Retry" state.
- The unsaved changes are still in the local state (widget positions, names, bindings intact).
- Re-enable network → click Retry → save succeeds → banner dismisses.
- Prometheus:
dashboard_save_errors_total{failure_mode="network"}incremented.
Same flow in the create modal:
- The modal stays open on failure with the red banner.
- Input values are preserved.
- Clicking "Retry" (button text changes from Save to Retry after an error) re-attempts with the same idempotency key.
6 — Error ID round-trip: UI → Sentry / logs
Setup: force a crash in any widget (case 1) OR trigger a malformed response
from the backend (case 2 but with {"success": true, "rows": "not-an-array"}).
Expected:
- Error ID displayed in the fallback UI is a valid UUID.
-
Copy IDputs it in the clipboard. - Server logs (pino, search for
component: 'api-dashboards-telemetry') contain a record with matchingerrorId, includingwidgetType,sourceKind,dashboardId,scope. - Sentry: event with matching
errorIdtag, grouped underDashboardError[widget]orDashboardError[builder]. - The
source_kindtag on the Sentry event matches the widget's actual data path (ontology_backedif the widget is bound to anobject_type-backed dataset,raw_datasetotherwise).
Coverage note — the ontology differentiator
Every metric emitted across these cases carries source_kind. Before shipping,
eyeball one canonical dashboard with both an object_type widget and a dataset
widget, trigger a success on each, and confirm Prometheus has two distinct rows:
dashboard_widget_renders_total{status="success", source_kind="ontology_backed", widget_type="…"} 1
dashboard_widget_renders_total{status="success", source_kind="raw_dataset", widget_type="…"} 1
That split is the foundation for measuring adoption of the Node differentiator in Fase 5 onwards. If the split breaks, stop and fix before moving on.