# Realtime, Web Vitals & JS Errors

Three operator-facing dashboards run off the same event stream but use different storage backends because their access patterns are different.

## Realtime Dashboard

**Web Analytics > Realtime** shows the last 30 minutes of activity, refreshed every 10 seconds. It does **not** hit BigQuery — every poll would melt the cost envelope. Instead, it reads from a per-tenant Mongo capped collection.

### How It Works

Every successful `/collect` insert calls `realtimeService.appendToBuffer({ tenantConnection, siteId, rows })` via `setImmediate` (so the HTTP response returns first). The buffer is:

- **`realtimeBufferModel`** — a Mongo capped collection sized at 256 MB / 500K documents.
- Capped means inserts overwrite the oldest documents in FIFO order when the cap is hit. No deletes, no TTL — just a circular buffer.
- Indexed on `(siteId, receivedAt desc)` for the dashboard's range queries.
- Lazily created on first event ingest per tenant.

The dashboard polls `GET /api/analytics/sites/:siteId/realtime` every 10 s. The endpoint:

1. Computes "last 30 minutes" from now.
2. Aggregates by `event_name`, `page_path`, and `visitor_id` against the buffer.
3. Returns counts + a per-minute time-series for the last 30 minutes.
4. Honors the same per-tenant rate-limit middleware as every other dashboard endpoint.

Because the buffer is capped, high-traffic sites that exceed 500K events in 30 minutes will see the oldest minutes drop off — the dashboard auto-detects this and shows the actual time-window covered ("Showing last 14 min of data, buffer is full").

### What the Dashboard Shows

- **Active visitors** — distinct `visitor_id` count in last 5 minutes.
- **Events per minute** — sparkline.
- **Top pages** — `page_path` counts.
- **Top events** — `event_name` counts.
- **Live event ticker** — most recent 20 events with full payload (collapsible).
- **Geo dots** — last 100 events plotted on a world map by country (only present when geo-enrichment is configured).

### Independence from BigQuery

This is the dashboard you check when the BQ side seems wrong — if events show in Realtime but the daily counts look low in Funnels, see **Fleet Dashboard & Troubleshooting** (the most common cause is `events_dlq` insert failures).

## Web Vitals Dashboard

**Web Analytics > Web Vitals** shows LCP, CLS, and INP percentile distributions per page.

### Source Data

Web Vitals events are auto-collected by the tracker (`web_vitals` event name with `metric ∈ {lcp, cls, inp}`). They're flushed:

- On `pagehide` / `visibilitychange: hidden` for the current page.
- On every SPA navigation, with `page_url` / `page_path` set to the **previous** page (so per-page vitals are correctly attributed in SPAs).

The collector writes them to the standard `events` table — no special table.

### Query

The dashboard queries BQ for the date range you select:

```sql
SELECT
  page_path,
  metric,
  APPROX_QUANTILES(value, 100)[OFFSET(50)] AS p50,
  APPROX_QUANTILES(value, 100)[OFFSET(75)] AS p75,
  APPROX_QUANTILES(value, 100)[OFFSET(95)] AS p95,
  COUNT(*) AS samples
FROM events
WHERE tenant_id = @tenant
  AND site_id   = @site
  AND event_name = 'web_vitals'
  AND event_ts BETWEEN @from AND @to
GROUP BY page_path, JSON_VALUE(properties, '$.metric')
```

Rendered as a per-page table with the standard Web Vitals thresholds color-coded:

| Metric | Good | Needs improvement | Poor |
| --- | --- | --- | --- |
| LCP | < 2.5 s | 2.5–4 s | > 4 s |
| CLS | < 0.1 | 0.1–0.25 | > 0.25 |
| INP | < 200 ms | 200–500 ms | > 500 ms |

### What the Dashboard Adds

- **Page filter** — focus on a single `page_path` or a regex.
- **Device split** — break down per LCP by `device_type` (`mobile` / `tablet` / `desktop`).
- **Trend** — daily p75 line for each metric over the selected range.
- **Element drill-down** — for LCP, the most common `element_selector` per page. Tells you what's slow (the hero image? the headline?).

### Limitations

- CLS is reported per-page-view, not per-element. The `element_selector` on a CLS event identifies the *largest contributing* layout shift, not every shift.
- INP requires Chromium ≥ 96 (most browsers since early 2022). Safari support landed in 17.x. Older browsers report no INP — they just don't fire the event.
- The tracker batches vitals locally; flushes are best-effort on unload. Mobile Safari occasionally drops the unload-time `sendBeacon` — expect ~2–5% missing samples on iOS.

## JS Error Tracking

**Web Analytics > Errors** is an error-aware view onto the same `events` table, filtered to `event_name = 'js_error'`.

### What's Captured

Per the **Auto-Collected Events** doc, every `js_error` carries:

- `message` (first 1 KB).
- `source`, `lineno`, `colno`.
- `stack` (first 4 KB).
- `error_kind` (`'error'` or `'unhandledrejection'`).

Errors are deduped per-page-view via `DEDUPE_LIMIT = 10` — the tracker won't fire more than 10 distinct `js_error` events per page-view to avoid runaway loops.

### Dashboard

- **Top error groups** — grouped by `(message, source, lineno)`. The same Chrome stack trace bug-class collapses into one row regardless of inline column offsets.
- **Visitor impact** — for each group, how many distinct visitors saw it in the date range.
- **Sample stack** — expandable view of one full stack trace for the group.
- **First seen / last seen** — for triaging "is this a regression?".
- **Affected pages** — top `page_path` values for the group.

### Symbolication

The tracker does not symbolicate stack traces. For minified production bundles, the stack will reference the minified line:column. Two options:

1. Ship source maps and tell your error-monitoring of choice (Sentry, Bugsnag, etc.) to fetch them — Karmaflow.ai doesn't natively symbolicate.
2. Use the AI element picker's same Gemini channel to ask "what does this stack trace point to in `<paste source>`?" — workable for small sites, not for high-volume bug triage.

This module is intentionally minimal — full crash-reporting is out of MVP scope. The goal here is **awareness** ("the site is throwing errors and traffic is correlated with them"), not **resolution** ("here's the offending commit").

### Linking Errors to Pages

The dashboard JOINs `js_error` events with `page_view` events on `(visitor_id, session_id)` within a 5-minute window so you can see "this error occurred during a session that landed on `/checkout`". This is how you spot regressions tied to a single deploy or a single route.
