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:

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

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:

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

Query

The dashboard queries BQ for the date range you select:

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

Limitations

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:

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

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 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.