All checks were successful
Release / Test backend (push) Successful in 40s
Release / Check ui (push) Successful in 1m5s
Release / Docker / caddy (push) Successful in 1m9s
Release / Docker / backend (push) Successful in 3m14s
Release / Docker / runner (push) Successful in 4m7s
Release / Upload source maps (push) Successful in 2m11s
Release / Docker / ui (push) Successful in 2m23s
Release / Gitea Release (push) Successful in 40s
- Add @grafana/faro-web-sdk to UI; wire initializeFaro in hooks.client.ts gated on PUBLIC_FARO_COLLECTOR_URL (no-op in dev) - Add Grafana Alloy service (faro.receiver) to homelab compose; Faro endpoint → alloy:12347 (faro.libnovel.cc via cloudflared) - Add PUBLIC_FARO_COLLECTOR_URL env var to docker-compose.yml UI service - Add Web Vitals dashboard (web-vitals.json): LCP/INP/CLS/TTFB/FCP p75 stats + LCP/TTFB time-series + Faro exception logs from Loki - Fix runner.json: strip libnovel_ prefix from all metric names - Fix backend.json: replace 5 dead http_client_* panels with spanmetrics-based equivalents (Request Rate by Span Name + Latency by Span Name p95) - Fix OTel collector: add service.telemetry.metrics.address: 0.0.0.0:8888 so Prometheus can scrape collector self-metrics - Add Grafana link to admin nav external tools; add admin_nav_grafana message key to all 5 locale files; recompile paraglide
285 lines
11 KiB
JSON
285 lines
11 KiB
JSON
{
|
|
"uid": "libnovel-web-vitals",
|
|
"title": "Web Vitals (RUM)",
|
|
"description": "Real User Monitoring — Core Web Vitals (LCP, CLS, INP, TTFB, FCP) from @grafana/faro-web-sdk. Data flows: browser → Alloy faro.receiver → Tempo (traces) + Loki (logs).",
|
|
"tags": ["libnovel", "frontend", "rum", "web-vitals"],
|
|
"timezone": "browser",
|
|
"refresh": "1m",
|
|
"time": { "from": "now-24h", "to": "now" },
|
|
"schemaVersion": 39,
|
|
"panels": [
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "LCP — p75 (Largest Contentful Paint)",
|
|
"description": "Good < 2.5 s, needs improvement < 4 s, poor ≥ 4 s.",
|
|
"gridPos": { "x": 0, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "ms",
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "color": "green", "value": null },
|
|
{ "color": "yellow", "value": 2500 },
|
|
{ "color": "red", "value": 4000 }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*lcp|LCP\"}[1h])) by (le)) * 1000",
|
|
"legendFormat": "LCP p75",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 2,
|
|
"type": "stat",
|
|
"title": "INP — p75 (Interaction to Next Paint)",
|
|
"description": "Good < 200 ms, needs improvement < 500 ms, poor ≥ 500 ms.",
|
|
"gridPos": { "x": 4, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "ms",
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "color": "green", "value": null },
|
|
{ "color": "yellow", "value": 200 },
|
|
{ "color": "red", "value": 500 }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*inp|INP\"}[1h])) by (le)) * 1000",
|
|
"legendFormat": "INP p75",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 3,
|
|
"type": "stat",
|
|
"title": "CLS — p75 (Cumulative Layout Shift)",
|
|
"description": "Good < 0.1, needs improvement < 0.25, poor ≥ 0.25.",
|
|
"gridPos": { "x": 8, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "short",
|
|
"decimals": 3,
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "color": "green", "value": null },
|
|
{ "color": "yellow", "value": 0.1 },
|
|
{ "color": "red", "value": 0.25 }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*cls|CLS\"}[1h])) by (le))",
|
|
"legendFormat": "CLS p75",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 4,
|
|
"type": "stat",
|
|
"title": "TTFB — p75 (Time to First Byte)",
|
|
"description": "Good < 800 ms, needs improvement < 1800 ms, poor ≥ 1800 ms.",
|
|
"gridPos": { "x": 12, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "ms",
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "color": "green", "value": null },
|
|
{ "color": "yellow", "value": 800 },
|
|
{ "color": "red", "value": 1800 }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*ttfb|TTFB\"}[1h])) by (le)) * 1000",
|
|
"legendFormat": "TTFB p75",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 5,
|
|
"type": "stat",
|
|
"title": "FCP — p75 (First Contentful Paint)",
|
|
"description": "Good < 1.8 s, needs improvement < 3 s, poor ≥ 3 s.",
|
|
"gridPos": { "x": 16, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "background", "graphMode": "none" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "ms",
|
|
"thresholds": {
|
|
"mode": "absolute",
|
|
"steps": [
|
|
{ "color": "green", "value": null },
|
|
{ "color": "yellow", "value": 1800 },
|
|
{ "color": "red", "value": 3000 }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*fcp|FCP\"}[1h])) by (le)) * 1000",
|
|
"legendFormat": "FCP p75",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 6,
|
|
"type": "stat",
|
|
"title": "Active Sessions (30 min)",
|
|
"gridPos": { "x": 20, "y": 0, "w": 4, "h": 4 },
|
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
|
"fieldConfig": {
|
|
"defaults": {
|
|
"unit": "short",
|
|
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
|
}
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "sum(rate(traces_spanmetrics_calls_total{service=\"libnovel-ui\"}[30m]))",
|
|
"legendFormat": "sessions",
|
|
"instant": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 10,
|
|
"type": "timeseries",
|
|
"title": "LCP over time (p50 / p75 / p95)",
|
|
"gridPos": { "x": 0, "y": 4, "w": 12, "h": 8 },
|
|
"options": { "tooltip": { "mode": "multi" }, "legend": { "displayMode": "list", "placement": "bottom" } },
|
|
"fieldConfig": {
|
|
"defaults": { "unit": "ms", "custom": { "lineWidth": 2, "fillOpacity": 10 } },
|
|
"overrides": [
|
|
{ "matcher": { "id": "byName", "options": "Good (2.5s)" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }, { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [4, 4] } }] },
|
|
{ "matcher": { "id": "byName", "options": "Poor (4s)" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }, { "id": "custom.lineStyle", "value": { "fill": "dash", "dash": [4, 4] } }] }
|
|
]
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.50, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*lcp|LCP\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p50"
|
|
},
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*lcp|LCP\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p75"
|
|
},
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.95, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*lcp|LCP\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p95"
|
|
},
|
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "2500", "legendFormat": "Good (2.5s)" },
|
|
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "4000", "legendFormat": "Poor (4s)" }
|
|
]
|
|
},
|
|
{
|
|
"id": 11,
|
|
"type": "timeseries",
|
|
"title": "TTFB over time (p50 / p75 / p95)",
|
|
"gridPos": { "x": 12, "y": 4, "w": 12, "h": 8 },
|
|
"options": { "tooltip": { "mode": "multi" }, "legend": { "displayMode": "list", "placement": "bottom" } },
|
|
"fieldConfig": {
|
|
"defaults": { "unit": "ms", "custom": { "lineWidth": 2, "fillOpacity": 10 } }
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.50, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*ttfb|TTFB\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p50"
|
|
},
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.75, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*ttfb|TTFB\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p75"
|
|
},
|
|
{
|
|
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
|
"expr": "histogram_quantile(0.95, sum(rate(traces_spanmetrics_latency_bucket{service=\"libnovel-ui\", span_name=~\"faro.*ttfb|TTFB\"}[5m])) by (le)) * 1000",
|
|
"legendFormat": "p95"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 20,
|
|
"type": "logs",
|
|
"title": "Frontend Errors & Exceptions",
|
|
"description": "JS exceptions and console errors captured by Faro and shipped to Loki.",
|
|
"gridPos": { "x": 0, "y": 12, "w": 24, "h": 10 },
|
|
"options": {
|
|
"showTime": true,
|
|
"showLabels": true,
|
|
"wrapLogMessage": true,
|
|
"prettifyLogMessage": true,
|
|
"enableLogDetails": true,
|
|
"sortOrder": "Descending",
|
|
"dedupStrategy": "none"
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "loki", "uid": "loki" },
|
|
"expr": "{service_name=\"libnovel-ui\"} | json | kind =~ `(exception|error)`",
|
|
"legendFormat": ""
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": 21,
|
|
"type": "logs",
|
|
"title": "Frontend Logs (all Faro events)",
|
|
"gridPos": { "x": 0, "y": 22, "w": 24, "h": 10 },
|
|
"options": {
|
|
"showTime": true,
|
|
"showLabels": false,
|
|
"wrapLogMessage": true,
|
|
"prettifyLogMessage": true,
|
|
"enableLogDetails": true,
|
|
"sortOrder": "Descending",
|
|
"dedupStrategy": "none"
|
|
},
|
|
"targets": [
|
|
{
|
|
"datasource": { "type": "loki", "uid": "loki" },
|
|
"expr": "{service_name=\"libnovel-ui\"}",
|
|
"legendFormat": ""
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|