Skip to content

Serve the dashboard

The dashboard is a Next.js app that reads from the collector’s query API and renders heatmaps, mesh rankings, performance panels, and session replay.

Terminal window
pnpm dev:dashboard

The dashboard authenticates to the collector with a project API key. Configure it via the environment (e.g. NEXT_PUBLIC_API_KEY and the collector URL) so the browser can call the read API with x-api-key. The key only ever reads its own project’s data.

  • View-direction & pointer heatmaps — 2D panels and full 3D against a scene proxy backdrop.
  • View coverage — a 360° coverage gauge that buckets sessions by how much of the object they actually looked at (0–25 / 25–50 / 50–75 / 75–100%), derived from the same camera view-direction grid as the dome — the “did they rotate it to see the back?” number.
  • Flow Sankey — gaze→click flow links rendered in 3D, with position-aware standpoint gating, a two-stage (standpoint → gaze → mesh) view, and a camera-mode toggle (see below).
  • Mesh rankings — most-interacted meshes, a part-popularity leaderboard (trend sparkline + per-mesh input-source split, scoped to active interactions so passive gaze doesn’t inflate the ranking), interaction-kind breakdown, object dwell, hover hesitation, and a blind-spots report (meshes rendered but never noticed — high visibility, little engagement; #143).
  • Configurator variants — a variant → conversion leaderboard ranking custom variant events by views, with dwell and an in-panel picker for per-variant conversion rate (see below).
  • Input — an input-modality split (mouse/touch/keyboard/gamepad/XR share) with the most-used app-level shortcuts.
  • Performance — FPS/frame-time summaries with percentile aggregation, a performance distribution histogram (p05/p50/p95 bands), compile stalls, resource footprint.
  • Navigation & XR — camera gestures, coverage and a dead-zone report (proxy meshes visitors never approach), XR rotation and source usage, a VR comfort & locomotion panel (teleport vs. smooth-locomotion vs. navigate mix for XR sessions, correlated with early exits — a motion-discomfort proxy), a guardian boundary-contacts panel (per-session near-boundary approach counts + near-zone time — a room-scale comfort signal; #157), and a tracking quality panel (share of XR session time spent with degraded or lost spatial tracking, split by hand vs. controller — from capability_change tracking transitions).
  • Scene retention funnel — a canned level-retention preset: session counts flowing scene → scene in observed order (from scene_change markers), with drop-off visible per source scene and no funnel steps to author (#147).
  • Error heatmap (3D) — voxel-binned world position of positioned runtime_error + graphics_diagnostic events against the scene proxy backdrop, so you can see where in the scene things break. Empty is the healthy case; only errors carrying a best-effort camera position appear.
  • Boundary-touch heatmap (3D) — voxel-binned world position of xr_boundary_proximity events against the scene proxy backdrop, so you can see where room-scale VR visitors reach their play-space boundary (#157). The boundary polygon / room geometry is never transmitted — only the on-device outcome (coarse position + duration) per approach.
  • Session replay — re-drive a recorded session inside a scene (requires raw retention; see below).
  • Analytics assistant — an in-browser LLM assistant (an Ask the assistant card on the overview) that answers natural-language questions of the connected project, grounded in read-only tool calls against the same query API. It runs locally on your GPU (WebLLM) or via your own hosted key, and is fully code-split — the panel and model runtime load only when you open it, so the main bundle is unchanged otherwise. See the assistant guide.

The Flow Sankey panel visualizes how viewers move from where they look to what they click, in 3D. It reads GET /api/v1/heatmaps/flow and exposes three controls:

  • Standpoints — when the scene is walkable, clicks are binned by the camera standpoint they were made from (position-aware mode, groupByOrigin). Pick a standpoint to gate the flow to clicks made from that spot, or keep All to collapse origins together. A standpoint marker is drawn at the averaged camera position so you can see where the audience stood.
  • Aggregate / Two-stageAggregate draws direct gaze→mesh ribbons; Two-stage inserts the standpoint as a first column (standpoint → gaze → mesh) so you can read the full path.
  • Camera mode (Walk / Orbit / All) — restrict to free/walkable (first-person) or orbit (viewer) sessions, or show both. The panel defaults to Walk when the scene has first-person data, and shows an “orbit-dominated” hint linking to the view-direction dome when it doesn’t.

The Part-popularity leaderboard (#74) ranks meshes by how often people act on them, and adds two things the plain Mesh-rankings list doesn’t: a per-row trend sparkline (rising/falling over the active range) and an input-source split (mouse / touch / keyboard / gamepad / XR) you can expand per row. It reads GET /api/v1/meshes/sources for the source split and GET /api/v1/meshes/trend for the sparkline.

The Variant → conversion leaderboard (#150) is built for 3D product configurators: the colour, material, and trim swaps a scene emits as custom events. It ranks each variant (a custom event grouped by its name) by views, and shows distinct sessions and the mean dwell before the shopper switched variant or converted. It reads GET /api/v1/variant-leaderboard.

Pick a success event from the in-panel dropdown — its options are the variant names discovered in the current range — to reveal each variant’s conversion rate: the share of sessions that fired the success event at or after first viewing that variant. Until one is chosen, the conversion column shows . Nothing is authored or persisted; the variant and success events are the ones your scene already emits, so there is no schema change.

The Dead-zone report (#76) surfaces the negative space of a walkable scene: registered proxy meshes that visitors’ cameras never approached. It answers “which parts of my scene is nobody getting near?” — the inverse of the coverage heatmap.

How it works. The panel reads two things and intersects them in the browser (no extra server join):

  1. Scene coverageGET /api/v1/coverage returns the occupied camera-position voxels (a 1 world-unit grid) with a visit count each: where the audience actually stood.
  2. The registered scene proxy — the per-mesh bounding boxes (AABBs) from GET /api/v1/scenes/:id/representation (ADR 0014): the geometry that could be visited.

For each proxy mesh it sums the visit counts of every coverage voxel whose centre falls inside that mesh’s AABB, padded outward by one cell so “standing right next to it” still counts. That sum is the mesh’s nearby-samples score. Meshes are ranked coldest first; a mesh with zero nearby camera samples is flagged dead. The header reads e.g. “5 of 42 proxy meshes are dead” — meaning 5 registered meshes had no camera-position voxel land within a padded cell of their bounds over the selected range.

The panel needs both a walkable/first-person scene (so there are camera-position samples to bin) and a registered proxy (so there’s reference geometry to call dead). It renders a graceful empty-state when no proxy is registered, and a “widen the range” hint when there are no camera samples in range. See Scene proxy for registering a proxy with scanSceneProxy.

The Backtracking hotspots panel (#153) ranks scenes/areas by how often visitors re-walk the same area — a confusion signal that desire lines alone don’t show. A player who keeps returning to the same spot is a strong hint of a dead end, a missed cue, or a puzzle that isn’t reading clearly.

How it works. It reads GET /api/v1/backtrack, which reuses the same camera_sample position stream that feeds desire lines. Each session’s positions are binned onto a coarse X/Z grid (cellSize, default 2 world units); consecutive samples in one cell are collapsed into a single cell entry, so standing still or dwelling never counts. Re-entering a cell you had already left is a revisit. Per scene the panel pools backtrack_ratio = revisits ÷ entries across sessions and shows the ratio (bar + percent) alongside the raw revisits / entries and session count, worst first.

This is the coarse-grid revisit proxy — a deliberately cheap first cut, not true reverse-segment path retracing. Tune cellSize to your scene: a larger cell only flags returning to a broad area, a smaller cell catches tighter out-and-back loops.

The Reachability report (#151) answers “was anything interactive placed out of comfortable reach?” — critical for VR/first-person UIs where a panel or button can end up too far from where the visitor actually stood.

How it works. It reads GET /api/v1/meshes/reachability, which ASOF-joins each mesh_interaction world hit-point to the nearest preceding camera_sample in the same session and bins the standpoint→interaction distance into bucketSize-wide world-unit bands per mesh (default 0.5). The panel folds each mesh’s bands into a count-weighted mean distance, ranks meshes farthest-mean first, and flags any whose mean clears a comfortable-reach threshold (≈2 world units by default) so out-of-reach UI stands out.

Terminal window
pnpm --filter @uptimizr/dashboard build

Host it wherever you run Next.js. Point it at your collector’s public URL and provide the project API key through the environment. Because reads are scoped to the key’s project, you can run one dashboard per project or switch keys per environment.

Session replay reads the raw, ordered event stream from /api/v1/sessions/:id/events, which is gated behind ENABLE_RAW_SESSION_RETENTION on the collector. Enable it only if you intend to retain raw per-session events — see Privacy & configuration.