Analytics Dashboard
A read-only analytics dashboard that queries SunGlasses events stored as Parquet — either directly from S3/MinIO or via Starfish local-sync. All queries run in DuckDB-WASM inside the browser; no backend is required.
SunGlasses client ──push──▶ Starfish /v1/analytics ──▶ object store
│
ingest-server ──flush──▶ S3/MinIO ──────────────────────┘
│
analytics-dashboard (browser) ──DuckDB-WASM──▶ read_parquet
Prerequisites
- Node.js 20+ and pnpm 9+ (dev only)
- A populated Parquet dataset
Starfish mode: a running Starfish sync server with starfish-events ≥ 3.0.0-alpha.44, analytics collection with listable: true, and either admin cap-cert or public read enabled. starfish-events ≥ 3.0.0-alpha.62 additionally assigns a server-side sortable batch id, which the dashboard uses to sync incrementally (see step 3 below) — on an older server, syncing still works but always re-lists the full batch history.
Direct S3 mode: the bucket must allow the browser origin via CORS (see the dashboard README for the bucket CORS policy).
Setup
pnpm install
pnpm --filter analytics-dashboard dev
Open http://localhost:5174, choose Starfish or Direct S3, and enter credentials.
Data sources
Starfish local-sync
Use when Garage/S3 is internal-only and the public entry point is the sync server.
- Wire the analytics namespace on your sync server (
listable: true, and eitherread_roles: ["admin"]with a platform admin enricher, orread_roles: ["public"]for unauthenticated reads). - In the dashboard setup screen, select Starfish and provide:
- Sync base URL — full external API root including the
/sync/v1/<namespace>prefix, e.g.https://sync.example.com/sync/v1/analytics. Do not enter the bare host. - App slugs — one or more slugs comma-separated, e.g.
my-app, other-app(matches theappfield on theSunglassesAnalyticsAdapter). - Admin cap-cert + device Ed25519 private key (hex) — when
read_rolesincludesadmin; or enable Public read when the collection allows anonymous list/pull.
- Sync base URL — full external API root including the
- The browser lists batches (
GET /list/events/{app}), pulls Parquet bytes into browser memory, registers them with DuckDB-WASM, and queries across all configured apps combined.
Click Refresh data to pull new batches. Refresh only fetches batches written since the last sync — it resumes /list from the last-seen batch id (persisted in the per-app manifest, see below) instead of re-listing the whole collection, so a Refresh with nothing new does a single lightweight /list call per app rather than paging through the full history.
Multiple apps: all configured apps are synced and aggregated. Use the app filter dropdown in the top bar to narrow to a single app. After connecting you can also add or remove apps live from the sidebar without reconnecting.
Load on reload (lazy background sync)
On reload with a saved connection the dashboard renders immediately — the sidebar, filters, and sections appear at once rather than behind a full-screen "Connecting…" spinner. The engine first registers the IndexedDB-cached batches (local, no network), so cached data is queryable straight away, then pulls any new batches in the background. Download progress appears in a top-bar indicator (Downloading X / Y…), and mounted sections re-query automatically once new data lands. A background-sync failure surfaces in the same indicator without hiding the dashboard — the cached data stays usable. First-ever connections (empty cache) show a brief "Preparing cached data…" placeholder in the content area while the engine initializes; Direct S3 reads live and has nothing to pre-download.
Persistent local cache (IndexedDB)
Parquet batch bytes are cached in IndexedDB (sunglasses-dashboard database, parquet store), keyed per app. On reload the dashboard reads bytes from IndexedDB first and only downloads batches that are genuinely new — eliminating full re-downloads on every page load.
A per-app manifest in localStorage (starfish-manifest-{app}) tracks which filenames have been synced, plus the incremental-listing cursor (listCursor — the last-seen server-assigned sortable batch id). The cursor only advances once every batch from that sync has downloaded successfully, so an interrupted sync resumes cleanly next time instead of skipping the missed batches. Legacy batches written before starfish-events v3.0.0-alpha.62 carry client-minted random UUID ids; those are never used as the cursor (a random UUID usually sorts above every server-assigned id and would hide all newer batches), so on a mixed-history collection each sync re-lists the legacy ids — they are only skipped, never re-downloaded — and the single-/list-call steady state applies once all batches are server-assigned. A poisoned cursor persisted by dashboard 0.14.1 repairs itself automatically on the next sync. Disconnecting (clicking "Change connection") clears both the manifests and the IndexedDB byte cache so data does not linger.
Direct S3
DuckDB-WASM loads the httpfs extension and reads Parquet directly from s3://<bucket>/<prefix>/**/*.parquet. Provide explicit access keys (IAM / credential-chain is not available in the browser).
For MinIO or other S3-compatible stores, set the Custom endpoint URL in the setup form.
Production
pnpm --filter analytics-dashboard build
The output (dist/) is a small static SPA. Deploy to any static host (Cloudflare Workers, Vercel, Netlify, an S3 bucket). The DuckDB WASM bundles are fetched from cdn.jsdelivr.net on first use — the browser must be able to reach that CDN.
Privacy
- DAU and retention queries use
anonymous_id— neverdistinct_id, raw user traits, or PII. - Parquet bytes are cached locally in IndexedDB and cleared on disconnect. No event data is sent to any server.
- The Query tab runs arbitrary
SELECT/WITHagainst your data and can expose raw columns. Deploy for yourself or trusted users only.