A self-hosted service for collecting browser security reports and Web Vitals, with a dashboard for understanding your site's health.
reportd receives data from two browser APIs and presents it in a dashboard:
- Web Vitals (LCP, CLS, INP, FCP, TTFB) -- performance metrics sent by the web-vitals library
- Browser Reports (CSP violations, deprecation warnings, interventions, crashes, COOP/COEP violations, permissions policy violations) -- sent automatically by browsers via the Reporting API
Data is stored in both Postgres (for fast dashboard queries via GORM) and BigQuery (for long-term analytics).
| Type | Source | Description |
|---|---|---|
| CSP violation | report-to / Reporting API |
Content Security Policy violations |
| Expect-CT | report-to |
Certificate Transparency violations (legacy) |
| Deprecation | Reporting API | Usage of deprecated browser APIs |
| Permissions Policy | Reporting API | Blocked access to browser features (camera, geolocation, etc.) |
| Intervention | Reporting API | Browser blocked something for performance/UX |
| Crash | Reporting API | Tab crash (OOM) or unresponsive page |
| COEP | Reporting API | Cross-Origin-Embedder-Policy violations |
| COOP | Reporting API | Cross-Origin-Opener-Policy violations |
| Document Policy | Reporting API | Document-Policy violations |
Unknown report types are stored as raw JSON for forward compatibility.
reportd is configured via environment variables (prefix REPORTD_) or command-line flags:
| Variable | Flag | Required | Description |
|---|---|---|---|
REPORTD_DATABASE_URL |
--database_url |
Yes | Postgres connection string |
REPORTD_PROJECT |
--project |
Yes | GCP project ID for BigQuery |
REPORTD_DATASET |
--dataset |
Yes | BigQuery dataset name |
REPORTD_ANALYTICS_TABLE |
--analytics_table |
Yes | BigQuery table for Web Vitals |
REPORTD_REPORTS_TABLE |
--reports_table |
Yes | BigQuery table for Report-To data |
REPORTD_REPORTS_V2_TABLE |
--reports_v2_table |
No | BigQuery table for Reporting API v1 data |
PORT |
-- | No | HTTP port (default: 8080) |
docker run -p 8080:8080 \
-e REPORTD_DATABASE_URL=postgres://user:pass@host/reportd \
-e REPORTD_PROJECT=my-gcp-project \
-e REPORTD_DATASET=reporting \
-e REPORTD_ANALYTICS_TABLE=analytics \
-e REPORTD_REPORTS_TABLE=reports \
-e REPORTD_REPORTS_V2_TABLE=reports_v2 \
ghcr.io/icco/reportd# Start Postgres
docker run -d --name reportd-pg -p 5432:5432 \
-e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=reportd \
postgres:17
# Run reportd
export REPORTD_DATABASE_URL=postgres://postgres:postgres@localhost/reportd
export REPORTD_PROJECT=my-project
export REPORTD_DATASET=my-dataset
export REPORTD_ANALYTICS_TABLE=analytics
export REPORTD_REPORTS_TABLE=reports
export REPORTD_REPORTS_V2_TABLE=reports_v2
go run main.goAdd this snippet to your site to send Web Vitals to reportd:
<script type="module">
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'https://unpkg.com/web-vitals@5?module';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
(navigator.sendBeacon && navigator.sendBeacon('https://your-reportd-instance/analytics/yoursite', body)) ||
fetch('https://your-reportd-instance/analytics/yoursite', { body, method: 'POST', keepalive: true });
}
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);
</script>Add these HTTP headers to your site's responses:
Reporting-Endpoints: default="https://your-reportd-instance/reporting/yoursite"
Content-Security-Policy: ...; report-to default;
Cross-Origin-Opener-Policy: same-origin; report-to="default"
Cross-Origin-Embedder-Policy: require-corp; report-to="default"
Permissions-Policy: camera=(), geolocation=(); report-to="default"
For legacy Report-To support:
Report-To: {"group":"default","max_age":10886400,"endpoints":[{"url":"https://your-reportd-instance/report/yoursite"}]}
| Endpoint | Content-Type | Description |
|---|---|---|
POST /analytics/{service} |
application/json |
Web Vitals data |
POST /report/{service} |
application/csp-report, application/expect-ct-report+json, application/reports+json |
Legacy Report-To data |
POST /reporting/{service} |
application/reports+json |
Reporting API v1 data |
| Endpoint | Description |
|---|---|
GET / |
Service index with health indicators |
GET /view/{service} |
Dashboard for a specific service |
GET /api/vitals/{service} |
JSON: p75 summaries and daily time series |
GET /api/reports/{service} |
JSON: report counts, recent reports, top violated directives |
GET /analytics/{service} |
JSON: daily average Web Vitals |
GET /reports/{service} |
JSON: daily report counts |
GET /services |
JSON: list of all services |
GET /healthz |
Health check |
The service view page provides:
- Core Web Vitals cards with p75 values rated against Google's thresholds (good / needs improvement / poor)
- Time-series charts for each metric with threshold bands
- Report volume chart showing report counts by type over time
- Recent CSP violations table with violated directive, blocked URI, document URI, and source location
- Recent reports table for deprecation warnings, interventions, crashes, and other browser reports
- Top violated directives bar chart showing the most frequently violated CSP directives