Three operator-facing dashboards run off the same event stream but use different storage backends because their access patterns are different.
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.
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.(siteId, receivedAt desc) for the dashboard's range queries.The dashboard polls GET /api/analytics/sites/:siteId/realtime every 10 s. The endpoint:
event_name, page_path, and visitor_id against the buffer.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").
visitor_id count in last 5 minutes.page_path counts.event_name counts.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 Analytics > Web Vitals shows LCP, CLS, and INP percentile distributions per page.
Web Vitals events are auto-collected by the tracker (web_vitals event name with metric ∈ {lcp, cls, inp}). They're flushed:
pagehide / visibilitychange: hidden for the current page.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.
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 |
page_path or a regex.device_type (mobile / tablet / desktop).element_selector per page. Tells you what's slow (the hero image? the headline?).element_selector on a CLS event identifies the largest contributing layout shift, not every shift.sendBeacon — expect ~2–5% missing samples on iOS.Web Analytics > Errors is an error-aware view onto the same events table, filtered to event_name = 'js_error'.
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.
(message, source, lineno). The same Chrome stack trace bug-class collapses into one row regardless of inline column offsets.page_path values for the group.The tracker does not symbolicate stack traces. For minified production bundles, the stack will reference the minified line:column. Two options:
<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").
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.