e4719cb9ae
Manual entry now takes one parent's sjukpenningnivå, lägstanivå and dubbeldagar left (plus optional reserved days) and derives the rest in build_snapshot, shared with the scraper. The scraper also reads the family panel and exposes a partner deduction; with two parents it is saved as a 'deduced' snapshot (migration 002). Parent count is the single source of entitlement: one member gets 480 days and 96 savable at age 4, two get 240 and 48 each. Workspaces are capped at two parents. Also lands the in-flight squash of migrations 002–011 into the initial schema and the removal of planning_mode. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
453 lines
16 KiB
Markdown
453 lines
16 KiB
Markdown
# New observability feature
|
|
|
|
Collect errors and crashes from frontend and backend, run nightly batch analysis via Opus to categorize and suggest fixes, and surface tickets for manual resolution.
|
|
|
|
## High-level flow
|
|
|
|
1. **Capture**: Frontend/backend emit errors → POST to `/errors` endpoint → stored in SQLite
|
|
2. **Cleanup**: When a ticket is created to address an error group, delete the raw error logs for that group
|
|
3. **Nightly categorization job**: Query new errors → classify by category
|
|
4. **Nightly analysis job**: Group errors by category + location → send to Opus via Batches API → generate tickets
|
|
5. **Frontend dashboard**: View tickets, filter, search, edit, delete
|
|
|
|
---
|
|
|
|
## 1. Frontend error capture
|
|
|
|
### TypeScript SDK
|
|
|
|
Lightweight error capture library (part of Föräldraledig Plannerare React/Vite frontend).
|
|
|
|
**Functionality:**
|
|
- Catch global errors: `window.onerror`, `unhandledrejection`
|
|
- Provide explicit `captureError(error, context?)` function for manual logging
|
|
- Attach context: current URL, user session ID (if available), browser/device info
|
|
- Queue errors with exponential backoff + retry logic before POSTing
|
|
- POST to the app's existing `/errors` endpoint (backend defined below)
|
|
- No external dependencies; vanilla TypeScript
|
|
|
|
**Captured fields:**
|
|
- `error_message` (e.g., "Cannot read property 'x' of undefined")
|
|
- `stack_trace` (full JS stack)
|
|
- `url` (current page URL)
|
|
- `user_agent` (browser + OS)
|
|
- `timestamp`
|
|
- `session_id` (if available)
|
|
- `environment` (e.g., "production", "staging")
|
|
|
|
---
|
|
|
|
## 2. Backend error endpoint
|
|
|
|
### `/errors` POST endpoint
|
|
|
|
**Location:** Föräldraledig Plannerare existing backend
|
|
|
|
**Request body:**
|
|
```json
|
|
{
|
|
"error_message": "string",
|
|
"stack_trace": "string",
|
|
"url": "string",
|
|
"user_agent": "string",
|
|
"timestamp": "ISO8601",
|
|
"session_id": "string or null",
|
|
"environment": "string"
|
|
}
|
|
```
|
|
|
|
**Behavior:**
|
|
- Validate and log the error
|
|
- Store in SQLite `errors` table (schema below)
|
|
- Return 200 OK immediately (don't block the client)
|
|
- Optionally rate-limit per session_id or IP to prevent spam
|
|
|
|
---
|
|
|
|
## 3. SQLite schema
|
|
|
|
### `errors` table
|
|
|
|
```sql
|
|
CREATE TABLE errors (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT NOT NULL, -- ISO8601, when error occurred
|
|
environment TEXT NOT NULL, -- 'production', 'staging', etc.
|
|
error_message TEXT NOT NULL, -- e.g., "Cannot read property 'x'"
|
|
stack_trace TEXT NOT NULL, -- Full stack trace
|
|
endpoint TEXT, -- URL or backend route (e.g., '/api/users')
|
|
location TEXT, -- File:line or component name (extracted from stack)
|
|
user_agent TEXT, -- Browser + OS
|
|
session_id TEXT, -- User session identifier
|
|
error_level TEXT, -- 'error', 'warning', 'fatal'
|
|
error_type TEXT, -- 'frontend' or 'backend'
|
|
severity INTEGER DEFAULT 1, -- 1=low, 2=medium, 3=high (Opus can set this)
|
|
error_hash TEXT, -- SHA256(stack_trace) for quick deduplication
|
|
first_seen TEXT, -- ISO8601, first occurrence of this group
|
|
last_seen TEXT, -- ISO8601, most recent occurrence
|
|
occurrence_count INTEGER DEFAULT 1, -- How many times in the time window
|
|
category TEXT, -- Set by categorization job (NULL initially)
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
has_ticket INTEGER DEFAULT 0 -- 1 if a ticket exists for this error group, 0 otherwise
|
|
);
|
|
|
|
CREATE INDEX idx_timestamp ON errors(timestamp);
|
|
CREATE INDEX idx_category ON errors(category);
|
|
CREATE INDEX idx_error_hash ON errors(error_hash);
|
|
CREATE INDEX idx_has_ticket ON errors(has_ticket);
|
|
```
|
|
|
|
### `tickets` table
|
|
|
|
```sql
|
|
CREATE TABLE tickets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL, -- Auto-generated by Opus, editable by user
|
|
description TEXT NOT NULL, -- How to fix, what changed, Opus analysis
|
|
error_category TEXT NOT NULL, -- The category this ticket addresses
|
|
error_location TEXT NOT NULL, -- Where in the code (e.g., 'frontend/auth', 'backend/api/users')
|
|
error_type TEXT NOT NULL, -- 'frontend' or 'backend'
|
|
error_count INTEGER, -- Total occurrences in the period
|
|
opus_suggestion TEXT, -- Raw suggestion from Opus
|
|
code_context TEXT, -- Git diff, recent commits, relevant file snippets
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
status TEXT DEFAULT 'open', -- 'open', 'in_progress', 'resolved', 'ignored'
|
|
resolved_at TEXT, -- When marked resolved
|
|
related_errors TEXT -- JSON array of error IDs this ticket groups
|
|
);
|
|
|
|
CREATE INDEX idx_status ON tickets(status);
|
|
CREATE INDEX idx_created_at ON tickets(created_at);
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Predefined error categories
|
|
|
|
The categorization job uses these predefined categories as a starting point. **Opus is encouraged to suggest new categories** if errors don't fit these patterns.
|
|
|
|
**Predefined categories:**
|
|
|
|
1. **Auth failure** — Authentication, authorization, token expiry, invalid credentials
|
|
2. **Null/undefined reference** — Null pointer, undefined property access, type mismatches
|
|
3. **Network error** — Fetch/HTTP failures, timeouts, 4xx/5xx responses, DNS issues
|
|
4. **Rate limit** — 429 responses, quota exceeded, throttling
|
|
5. **Validation error** — Input validation, schema mismatches, malformed data
|
|
6. **Resource not found** — 404, missing file/endpoint, nonexistent record
|
|
7. **Unhandled exception** — Unexpected errors, missing error handlers, caught but not handled
|
|
8. **Performance/timeout** — Slow operations, stack overflow, memory issues, request timeouts
|
|
9. **Data integrity** — Constraint violations, inconsistent state, corrupted records
|
|
10. **Configuration/setup** — Missing env vars, incorrect configuration, initialization failures
|
|
|
|
**New categories:** If Opus encounters errors that don't fit these, it should suggest a new category in the ticket description. Over time, review and add new predefined categories.
|
|
|
|
---
|
|
|
|
## 5. Nightly categorization job
|
|
|
|
**Frequency:** Nightly (e.g., 1 AM)
|
|
|
|
**Trigger:** Manually via cron or scheduled task runner (e.g., `APScheduler` in Python)
|
|
|
|
**Steps:**
|
|
|
|
1. Query all errors where `category IS NULL` and `created_at` >= last run timestamp
|
|
2. For each error:
|
|
- Extract likely category from `error_message`, `stack_trace`, and `error_level`
|
|
- Use simple regex or rule-based matching to assign a predefined category
|
|
- If no match, leave `category = NULL` for the analysis job to handle
|
|
3. Commit changes to `errors.category`
|
|
|
|
**Example logic:**
|
|
- If message contains "Unauthorized" or "403" → "Auth failure"
|
|
- If message contains "null is not an object" or "Cannot read property" → "Null/undefined reference"
|
|
- If message contains "timeout" or "ECONNREFUSED" → "Network error"
|
|
- If HTTP status 429 → "Rate limit"
|
|
|
|
---
|
|
|
|
## 6. Nightly analysis and ticket generation job
|
|
|
|
**Frequency:** Nightly (e.g., 1:30 AM, after categorization job)
|
|
|
|
**Tool:** Python with Anthropic SDK, using the Batches API for cost efficiency
|
|
|
|
**Steps:**
|
|
|
|
### 6.1 Group errors
|
|
|
|
Query all errors where `has_ticket = 0` and `category IS NOT NULL` (use categorization job results).
|
|
|
|
**Grouping strategy:** Errors are grouped by **`(category, error_type, location)`** to avoid treating the same class of error (e.g., "Null/undefined reference in frontend/auth") as separate issues across occurrences.
|
|
|
|
- `category`: The classified error type (e.g., "Null/undefined reference")
|
|
- `error_type`: Whether error originated in frontend or backend
|
|
- `location`: Where in the code the error occurred (extracted from stack trace, e.g., `components/LoginForm.tsx` or `services/user_service.py`)
|
|
|
|
**Deduplication within a group:**
|
|
- Count total occurrences of errors in this group (sum `occurrence_count` from all matching error rows)
|
|
- Collect **representative error instances** (e.g., top 3 most recent or most severe)
|
|
- Discard duplicate raw instances within the group to avoid redundant analysis
|
|
- **Opus sees only a count + sample errors, not every instance**
|
|
|
|
**Output:** A list of error groups, each with:
|
|
- `category`
|
|
- `error_type`
|
|
- `location`
|
|
- `total_count` (total occurrences)
|
|
- `sample_errors` (list of representative error details)
|
|
- `time_window` (when these errors occurred)
|
|
|
|
### 6.2 Gather context for Opus
|
|
|
|
For each error group, collect **surrounding code and git context** so Opus can make informed suggestions:
|
|
|
|
**For frontend errors:**
|
|
- The exact error messages and stack traces from the sample errors
|
|
- Extract file path + line number from stack trace
|
|
- Fetch the relevant code snippet from the file (±10 lines from the error location)
|
|
- Get recent commits affecting that file: `git log --oneline -n 10 -- <file>`
|
|
- Get git diff of recent changes: `git diff HEAD~5 -- <file>` (last 5 commits)
|
|
- Browser/environment info from user_agent
|
|
|
|
**For backend errors:**
|
|
- The exact error messages and stack traces
|
|
- Extract file path + line number from stack trace
|
|
- Fetch the relevant code snippet from the file (±10 lines from the error location)
|
|
- Get recent commits affecting that file: `git log --oneline -n 10 -- <file>`
|
|
- Get git diff of recent changes: `git diff HEAD~5 -- <file>`
|
|
- Related logs or request context (if available)
|
|
|
|
**Data structure sent to Opus:**
|
|
```json
|
|
{
|
|
"category": "Null/undefined reference",
|
|
"error_type": "frontend",
|
|
"location": "components/LoginForm.tsx",
|
|
"total_count": 47,
|
|
"time_window": "2026-09-18 to 2026-09-19",
|
|
"sample_errors": [
|
|
{
|
|
"error_message": "Cannot read property 'email' of undefined",
|
|
"stack_trace": "...",
|
|
"timestamp": "2026-09-19T02:15:00Z",
|
|
"url": "http://app/login"
|
|
},
|
|
{
|
|
"error_message": "Cannot read property 'email' of undefined",
|
|
"stack_trace": "...",
|
|
"timestamp": "2026-09-19T01:30:00Z",
|
|
"url": "http://app/login"
|
|
}
|
|
],
|
|
"code_context": {
|
|
"file": "components/LoginForm.tsx",
|
|
"snippet": "const LoginForm = ({ user }) => {\n return <div>{user.email}</div>;\n}",
|
|
"recent_commits": [
|
|
"abc1234 - Fix login form prop handling",
|
|
"def5678 - Update auth flow"
|
|
],
|
|
"git_diff": "..."
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6.3 Send to Opus via Batches API
|
|
|
|
Batch all error groups into a single Batches API request (for cost efficiency).
|
|
|
|
**Prompt for Opus:**
|
|
|
|
```
|
|
You are analyzing error logs from a web application (Föräldraledig Plannerare).
|
|
For each error group below, provide:
|
|
|
|
Root cause analysis
|
|
Suggested fix (code changes or steps)
|
|
Affected systems/users
|
|
Priority (low, medium, high)
|
|
|
|
Error group:
|
|
{error data from 6.2}
|
|
|
|
Respond in JSON format:
|
|
{
|
|
"title": "Brief title for the issue",
|
|
"root_cause": "What caused this",
|
|
"suggested_fix": "How to fix it (code changes or steps)",
|
|
"affected_scope": "Frontend/backend, number of users, systems impacted",
|
|
"priority": "low|medium|high",
|
|
"reasoning": "Why this priority"
|
|
}
|
|
```
|
|
|
|
|
|
### 6.4 Parse Opus response and create tickets
|
|
|
|
For each error group with Opus analysis:
|
|
|
|
1. Create a row in `tickets` table with:
|
|
- `title` = Opus's suggested title
|
|
- `description` = Combination of root cause, suggested fix, and affected scope
|
|
- `error_category` = The error group's category
|
|
- `error_location` = The file/component location
|
|
- `error_type` = frontend or backend
|
|
- `error_count` = total_count from the group
|
|
- `opus_suggestion` = Full raw response from Opus
|
|
- `code_context` = Git diff and file snippets (for reference)
|
|
- `related_errors` = JSON array of error IDs in this group
|
|
|
|
2. Update all error rows in the group: `has_ticket = 1`
|
|
|
|
3. **After ticket creation:** Delete all error rows where `has_ticket = 1` (cleanup raw errors once addressed by a ticket)
|
|
|
|
---
|
|
|
|
## 7. Frontend dashboard MVP
|
|
|
|
**Technology:** React + Vite (part of Föräldraledig Plannerare frontend)
|
|
|
|
**Access:** Run with `npm run observability` or similar command
|
|
|
|
**Features:**
|
|
|
|
### View
|
|
- **Tickets list**: All open/in-progress tickets (paginated, ~20 per page)
|
|
- **Ticket detail**: Expand to show full description, error context, code snippets, priority
|
|
- **Filter**:
|
|
- By status: open, in_progress, resolved, ignored
|
|
- By error_type: frontend, backend
|
|
- By error_category: (all predefined categories + custom)
|
|
- By date range
|
|
- **Search**: By ticket title or description (substring match)
|
|
- **Trends chart**: Line chart showing error count over time (last 7 days)
|
|
|
|
### Actions
|
|
- **Edit ticket**: User can update `title` and `description` (e.g., clarify the fix, add notes)
|
|
- **Change status**: Mark as in_progress, resolved, ignored
|
|
- **Delete ticket**: Remove a bad or duplicate ticket
|
|
- **View related errors**: Show the raw error logs (with timestamps, counts) that this ticket groups
|
|
|
|
### Data source
|
|
- All ticket data queried from backend `/tickets` GET endpoint (with optional filters)
|
|
- All error data queried from backend `/errors` GET endpoint (with filters)
|
|
|
|
---
|
|
|
|
## 8. Backend API endpoints (existing app backend)
|
|
|
|
### `POST /errors`
|
|
*Receive error reports from the frontend SDK*
|
|
|
|
Request: Error object (see Section 2)
|
|
Response: `{ success: true }`
|
|
|
|
---
|
|
|
|
### `GET /tickets`
|
|
*Fetch tickets with optional filters*
|
|
|
|
Query params:
|
|
- `status` (open, in_progress, resolved, ignored)
|
|
- `error_type` (frontend, backend)
|
|
- `error_category` (predefined category name)
|
|
- `date_from` (ISO8601)
|
|
- `date_to` (ISO8601)
|
|
- `search` (substring search in title/description)
|
|
- `page` (pagination, default 1)
|
|
- `limit` (per page, default 20)
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"tickets": [
|
|
{
|
|
"id": 1,
|
|
"title": "Login form crashes on null user object",
|
|
"description": "...",
|
|
"error_category": "Null/undefined reference",
|
|
"error_type": "frontend",
|
|
"error_count": 47,
|
|
"status": "open",
|
|
"created_at": "2026-09-19T01:45:00Z",
|
|
"code_context": "..."
|
|
}
|
|
],
|
|
"total": 42,
|
|
"page": 1
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `GET /errors`
|
|
*Fetch raw error logs (for dashboard context)*
|
|
|
|
Query params:
|
|
- `ticket_id` (optional; show errors related to a specific ticket)
|
|
- `category` (optional; filter by category)
|
|
- `error_type` (optional; frontend or backend)
|
|
- `date_from`, `date_to` (optional; filter by date range)
|
|
- `has_ticket` (optional; 0 or 1)
|
|
- `limit` (default 100)
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"errors": [
|
|
{
|
|
"id": 123,
|
|
"timestamp": "2026-09-19T02:15:00Z",
|
|
"error_message": "Cannot read property 'email' of undefined",
|
|
"location": "components/LoginForm.tsx",
|
|
"occurrence_count": 5,
|
|
"category": "Null/undefined reference"
|
|
}
|
|
],
|
|
"total": 200
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `PATCH /tickets/:id`
|
|
*Update a ticket (title, description, status)*
|
|
|
|
Request:
|
|
```json
|
|
{
|
|
"title": "Updated title",
|
|
"description": "Updated description",
|
|
"status": "in_progress"
|
|
}
|
|
```
|
|
|
|
Response: Updated ticket object
|
|
|
|
---
|
|
|
|
### `DELETE /tickets/:id`
|
|
*Delete a ticket*
|
|
|
|
Response: `{ success: true }`
|
|
|
|
After deletion, the related error rows should have `has_ticket = 0` set again (so they can be re-analyzed in a future batch run if desired).
|
|
|
|
---
|
|
|
|
## 9. Summary of jobs and scheduling
|
|
|
|
| Job | Frequency | Trigger | Input | Output |
|
|
|-----|-----------|---------|-------|--------|
|
|
| **Categorization** | Nightly (1 AM) | Cron/scheduler | Uncategorized errors | Errors with `category` set |
|
|
| **Analysis + ticket generation** | Nightly (1:30 AM) | Cron/scheduler | Categorized errors + git context | New tickets, deletion of raw errors with tickets |
|
|
|
|
---
|
|
|
|
## 10. Implementation notes
|
|
|
|
- **Python setup**: Use Anthropic Python SDK for Batches API calls
|
|
- **Scheduling**: Use APScheduler or similar to run jobs on a schedule
|
|
- **Error deduplication**: Use SHA256(stack_trace) for quick lookups; grouping by (category, error_type, location) for analysis
|
|
- **Cost efficiency**: Batches API ensures Opus analysis is cost-effective at scale
|
|
- **Self-hosting**: All components (SDKs, backend, jobs, frontend) are self-hosted on the existing Föräldraledig Plannerare infrastructure |