Dashboard Query Backend Architecture#
Dashboard widgets in Langfuse run queries against one of two ClickHouse query backends — v1 (legacy traces/observations tables) or v2 (events-based events_core/events_full tables). Which backend a widget uses is determined at render time by combining a per-widget minVersion integer stored in the database with a per-user v4BetaEnabled session flag.
v1 vs. v2 Table Mapping#
| Version | Traces | Observations |
|---|---|---|
| v1 | traces table | observations table |
| v2 | events_traces | events_observations (events-based) |
ViewVersion is a Zod enum "v1" | "v2" . The v2 views back onto events_core for most queries, automatically upgrading to events_full when full I/O or metadata expansion is requested . The traces view is only available in v1 — viewsV2 explicitly omits it .
The EventsQueryBuilder in packages/shared provides the fluent query builder for the events tables, with primary-key–aligned ORDER BY and xxHash32 optimization for trace-ID filters .
Runtime Version Selection#
DashboardWidget.tsx computes metricsVersion with a single expression:
const metricsVersion: ViewVersion =
(widget.data?.minVersion ?? 1) >= 2 || isBetaEnabled ? "v2" : "v1";
A widget with minVersion >= 2 always uses v2, regardless of the user's beta status. A widget with minVersion = 1 uses v2 only when isBetaEnabled is true; otherwise it falls back to v1 .
Widget render
├── widget.minVersion >= 2? ──yes──► use "v2"
└── no
└── user.v4BetaEnabled? ──yes──► use "v2" ──► SSE progress enabled
└── no ──────► use "v1" ──► polling
When metricsVersion is "v2" and isBetaEnabled is true, loading switches from polling to Server-Sent Events (SSE) via shouldUseWidgetSSE() . This flag flows into useScheduledDashboardExecuteQuery and getChartLoadingProgress .
minVersion and v4BetaEnabled#
minVersion — Per-Widget Version Gating#
minVersion is an integer persisted on every widget row. It is set at save time by WidgetForm.tsx, which calls requiresV2() to diff v1 vs. v2 view declarations for the selected view. If any chosen dimension or measure only exists in v2, minVersion is saved as 2; otherwise 1 .
On the server, dashboardWidgets router helpers derive version = (minVersion ?? 1) >= 2 ? "v2" : "v1" before looking up the view declaration, so widgets are validated against the correct schema at write time .
v4BetaEnabled — Per-User Session Flag#
useV4Beta manages the v4BetaEnabled boolean stored in session.user.v4BetaEnabled. Key API:
isBetaEnabled— read from the Next-Auth sessionsetBetaEnabled(enabled)— callsapi.userAccount.setV4BetaEnabled, refreshes the session, and updates a PostHog person propertyenableWithIntro()— shows a one-time intro dialog (localStorage key"v4-beta-intro-dialog-seen") before enabling
Because the flag lives in the database and is loaded into the session, it persists across tabs and devices .
Bug: Incorrect minVersion from the Public API (PR #15565 / #15597)#
Root cause. POST /api/public/unstable/dashboard-widgets (and MCP tools behind it) hard-coded minVersion = 2 for every widget it created. Widgets with minVersion = 2 route all queries through the v2 engine, which reads the events_* tables. Those tables are only populated by the worker when LANGFUSE_MIGRATION_V4_WRITE_MODE is dual or events_only. On a legacy deployment — or any v3 installation (which has no events_* migration at all) — the v2 query hits a missing table and returns an opaque Internal Server Error. Widgets built in the UI were unaffected because WidgetForm stamps requiresV2(...) ? 2 : 1 .
Fix (PR #15565, merged 2026-07-29). normalizePublicDashboardWidgetInput was updated to accept an optional requestedMinVersion and cap it against deploymentMinWidgetVersion(), which returns:
1whenLANGFUSE_MIGRATION_V4_WRITE_MODE === "legacy"2otherwise
The cap only demotes — a stored v1 widget is never promoted. A PATCH on an already-broken v2 widget on a legacy deployment heals it back to minVersion = 1. Widgets requiring v2-only fields on a legacy deployment are rejected with a clear 400 error instead of being persisted unrenderable .
Note: LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN is not consulted — it gates user opt-in to the v4 read path, not whether the underlying events_* tables exist .
v3 backport (PR #15597). An open backport to the v3 branch carries the same fix. On v3 the events_* tables are absent in every write mode (migrations 0039_create_events_full and later only exist on main), so the default write mode on v3 is legacy and the fix stamps all public-API widgets as minVersion = 1 .
Widgets already persisted as broken are not auto-healed. A PATCH or a UI Save restamps them to the correct version. They also start rendering automatically once an operator enables dual/events_only write mode .
Key Files#
| File | Role |
|---|---|
DashboardWidget.tsx | Runtime metricsVersion selection (minVersion >= 2 || isBetaEnabled) |
useV4Beta.ts | Per-user beta flag — read, set, intro dialog |
WidgetForm.tsx | Computes minVersion on save via requiresV2() |
dataModel.ts | requiresV2() diff; v1/v2 view declarations |
dashboardWidgets.ts (router) | Server-side version-aware validation |
public-dashboard-widget-service.ts | deploymentMinWidgetVersion(), normalizePublicDashboardWidgetInput |
event-query-builder.ts | EventsQueryBuilder / EventsAggregationQueryBuilder for events_core/events_full |
types.ts | ViewVersion enum, viewsV2 list |
utils.ts | shouldUseWidgetSSE() |