# Error Center API Centralized error reporting across frontend (TS/TSX), backend (Go), and infra (future: Docker). This document specifies the request/response schema and endpoints implemented in this repository so you can integrate additional producers and build the admin UI. ## Overview - Public ingest endpoint: POST /api/v1/errors - Admin browsing: GET /api/v1/admin/errors, GET /api/v1/admin/errors/:id - Storage model: `error_events` table via GORM model `ErrorEvent` - Capture hooks in-app: - Backend: panic recovery + 5xx response reporter - Frontend: window.onerror, unhandledrejection, React ErrorBoundary, Axios 5xx interceptor - Rate limiting: per-IP+path, applied to ingest endpoint ## Data Model: ErrorEvent Returned by admin endpoints. JSON field names match below. ``` { id: number, created_at: string, // ISO timestamp (ingestion time) updated_at: string, // ISO timestamp origin: string, // e.g. "frontend" | "backend" | "docker" language: string, // e.g. "ts" | "tsx" | "go" severity: string, // e.g. "error" | "warn" | "fatal" message: string, stack: string, component: string, file: string, line: number, column: number, url: string, method: string, // HTTP method when applicable status: number, // HTTP status when applicable request_id: string, // correlates backend <-> frontend user_id: number | null, session_token: string, tags: object, // JSON map (string -> string) context: object, // JSON map (string -> any) env: string, // e.g. "development" | "production" version: string, hostname: string, occurred_at: string // ISO timestamp of when error actually happened } ``` ## Endpoint: POST /api/v1/errors (Public ingest) Ingests a single error event. - Method: POST - Path: `/api/v1/errors` - Headers: - `Content-Type: application/json` - Optional: `Authorization: Bearer ` (not required by default; used if you proxy to external collectors) - Rate limit: 120 requests per minute per IP+path. On limit exceeded: `429 Too Many Requests` and `Retry-After` header (seconds). ### Request body (JSON) ``` { origin: string, // required, e.g. "frontend" | "backend" | "docker" language?: string, // e.g. "ts" | "tsx" | "go" severity?: string, // "error" | "warn" | "fatal" (default depends on sender) message: string, // required stack?: string, component?: string, file?: string, line?: number, column?: number, url?: string, method?: string, status?: number, request_id?: string, user_id?: number, session_token?: string, tags?: { [k: string]: string }, context?: { [k: string]: any }, env?: string, version?: string, hostname?: string, occurred_at?: string // ISO (RFC3339). Defaults to server time if omitted } ``` ### Responses - 201 Created ``` { "id": } ``` - 400 Bad Request (invalid JSON/payload) ``` { "error": "invalid payload" } ``` - 429 Too Many Requests (rate limited) ``` { "error": "Příliš mnoho požadavků, zkuste to prosím později." } ``` - 500 Internal Server Error (storage issue) ``` { "error": "failed to store" } ``` ### cURL example ``` curl -X POST http://localhost:8080/api/v1/errors \ -H 'Content-Type: application/json' \ -d '{ "origin":"frontend", "language":"tsx", "severity":"error", "message":"TypeError: cannot read properties of undefined", "stack":"at Component (App.tsx:42:7)", "url":"/admin/errors?filter=today", "method":"GET", "status":500, "request_id":"b28a8a2b-2f3d-452a-9a5d-1ec74e53a2d4", "context": {"userAgent":"...","viewport":{"w":1440,"h":900}}, "env":"development", "occurred_at":"2025-11-06T12:00:00Z" }' ``` ## Endpoint: GET /api/v1/admin/errors (Admin list) Lists error events with filtering and pagination. - Method: GET - Path: `/api/v1/admin/errors` - Auth: Admin role required (standard admin authentication of this app) - Query params: - `origin`: string (exact match: frontend | backend | docker) - `severity`: string (exact match: fatal | error | warn) - `method`: string (exact match: GET | POST | PUT | PATCH | DELETE) - `status`: number (exact match HTTP status) - `search`: string (substring, ILIKE on message, stack, url) - `from`: string (RFC3339, filters `occurred_at >= from`) - `to`: string (RFC3339, filters `occurred_at <= to`) - `page`: number (default 1, min 1) - `limit`: number (default 20, min 1, max 200) ### Response - 200 OK ``` { "items": [ ErrorEvent, ... ], "total": } ``` ### Example ``` curl -b cookies.txt \ 'http://localhost:8080/api/v1/admin/errors?origin=frontend&severity=error&search=TypeError&page=1&limit=50' ``` ## Endpoint: GET /api/v1/admin/errors/:id (Admin detail) Returns a single error event by ID. - Method: GET - Path: `/api/v1/admin/errors/:id` - Auth: Admin role required ### Responses - 200 OK — returns the `ErrorEvent` JSON - 404 Not Found — `{ "error": "not found" }` - 500 Internal Server Error — `{ "error": "failed to load" }` ## Built-in Producers (in this repo) - Frontend reporter (src/services/errorReporter.ts) - Sends errors to `/api/v1/errors` by default - Uses localStorage overrides when present: - `fc_error_ingest_url`, `fc_error_ingest_token` - Or build-time env vars: - `REACT_APP_ERROR_INGEST_URL`, `REACT_APP_ERROR_INGEST_TOKEN` - Installed in index.tsx: - Global handlers: `window.onerror`, `unhandledrejection` - ErrorBoundary calls reporter on render errors - Axios 5xx interceptor (src/services/api.ts) reports server errors - Backend reporter (internal/services/error_reporter.go) - Panic recovery middleware and 5xx status reporter send events - Destination: - If `ERROR_INGEST_URL` configured, report to that URL with `ERROR_INGEST_TOKEN` if set - Otherwise, fallback to this server’s public API base + `/errors` ## Configuration - Backend env: - `ERROR_INGEST_URL` (optional): external collector URL - `ERROR_INGEST_TOKEN` (optional): bearer token for external collector - Frontend runtime overrides (optional): - `localStorage.setItem('fc_error_ingest_url', 'https://example.com/ingest')` - `localStorage.setItem('fc_error_ingest_token', 'YOUR_TOKEN')` - Defaults require no configuration; all errors post to this app at `/api/v1/errors`. ## Correlation and Metadata - `X-Request-ID` header is exposed; frontend interceptor forwards it in payload as `request_id`. - `occurred_at` denotes when the error happened; `created_at` is ingestion time. - `tags` is a flat string map; `context` allows arbitrary structured metadata. ## Rate Limiting Details (Ingest) - Scope: per client IP and request path - Window: 60 seconds - Max: 120 requests per window - Exceeding limit: `429 Too Many Requests` with `Retry-After` (seconds remaining) ## Notes - CORS: The ingest endpoint follows the app’s global CORS configuration. - PII: Consider redacting sensitive data before sending in `message`, `stack`, `context`. - Deduplication: The frontend reporter applies a basic burst de‑duplication (1.5s window) client-side; server stores all events. --- Last updated: 2025-11-06