8b21af3296
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
5.7 KiB
Markdown
111 lines
5.7 KiB
Markdown
# Statistics System
|
|
|
|
## Overview
|
|
|
|
The stats system provides workspace-level analytics about tasks, agent sessions, repository activity, and git commits. It computes everything on-demand from existing tables - there are no dedicated stats tables.
|
|
|
|
**Route**: `/stats` (with `?range=week|month|all`)
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Frontend Backend Database
|
|
─────── ─────── ────────
|
|
/stats (server component) GET /api/v1/workspaces/:id/ SQLite
|
|
└─ fetchStats() stats?range=week
|
|
└─ stats-page-client.tsx stats_handlers.go Source tables:
|
|
├─ Overview cards └─ httpGetStats() ├─ tasks
|
|
├─ Heatmap ├─ GetGlobalStats() ├─ task_sessions
|
|
├─ Completed chart ├─ GetTaskStats() ├─ task_session_turns
|
|
├─ Model usage ├─ GetDailyActivity() ├─ task_session_messages
|
|
├─ Repo stats ├─ GetCompletedTaskActivity()├─ task_session_commits
|
|
└─ Workload ├─ GetModelUsage() ├─ task_repositories
|
|
├─ GetRepositoryStats() ├─ repositories
|
|
└─ GetGitStats() └─ workflow_steps
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
### Write path (automatic during task execution)
|
|
|
|
| Event | Table | Key columns |
|
|
|-------|-------|-------------|
|
|
| Session start | `task_sessions` | task_id, agent_profile_id, started_at |
|
|
| Turn start | `task_session_turns` | task_session_id, started_at |
|
|
| Message sent | `task_session_messages` | task_session_id, author_type, type |
|
|
| Git commit | `task_session_commits` | session_id, files_changed, insertions, deletions |
|
|
| Turn end | `task_session_turns` | completed_at (UPDATE) |
|
|
| Session end | `task_sessions` | completed_at, state (UPDATE) |
|
|
|
|
Write code lives in `apps/backend/internal/task/repository/sqlite/` (session.go, message.go, git_snapshots.go).
|
|
|
|
### Read path (stats page load)
|
|
|
|
The handler at `apps/backend/internal/analytics/handlers/stats_handlers.go` calls 7 repository methods sequentially, each running a SQL aggregation query against the source tables. Results are assembled into a single `StatsResponse` JSON.
|
|
|
|
## Key Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `apps/backend/internal/analytics/handlers/stats_handlers.go` | HTTP handler, DTO conversion |
|
|
| `apps/backend/internal/analytics/repository/sqlite/stats.go` | All SQL queries |
|
|
| `apps/backend/internal/analytics/repository/sqlite/repository.go` | Init + auto-created indexes |
|
|
| `apps/backend/internal/analytics/models/models.go` | Go data models |
|
|
| `apps/web/app/stats/page.tsx` | Server component (data fetching) |
|
|
| `apps/web/app/stats/stats-page-client.tsx` | Client component (all UI) |
|
|
| `apps/web/app/stats/loading.tsx` | Loading skeleton |
|
|
| `apps/web/lib/api/domains/stats-api.ts` | Frontend API client |
|
|
|
|
## Metrics
|
|
|
|
### How key metrics are computed
|
|
|
|
| Metric | Method |
|
|
|--------|--------|
|
|
| Completed tasks | Tasks whose workflow step has `step_type = 'done'` |
|
|
| In-progress tasks | Tasks with `state = 'IN_PROGRESS'` |
|
|
| Duration | `SUM((julianday(completed_at) - julianday(started_at)) * 86400000)` on turns |
|
|
| Tool calls | Messages with `type LIKE 'tool_%'` |
|
|
| User messages | Messages with `author_type = 'user'` |
|
|
| Model | `json_extract()` of `model` (then `model_name`, `llm`) from `agent_profile_snapshot` on sessions. Usage is grouped by this value rather than by agent profile: a profile gets renamed and repointed at other models, so its sessions carry several snapshots and no single one describes the group. |
|
|
| Git stats | Aggregated from `task_session_commits` (only in-session commits) |
|
|
|
|
### Time ranges
|
|
|
|
| Range | Backend behavior |
|
|
|-------|-----------------|
|
|
| `week` | `start = now - 7 days`, daily activity = 7 days |
|
|
| `month` | `start = now - 30 days`, daily activity = 30 days |
|
|
| `all` | `start = nil` (no filter), daily activity = 365 days |
|
|
|
|
Range is parsed in `parseStatsRange()`. The `start` value is passed to each query as `WHERE (? IS NULL OR timestamp >= ?)`.
|
|
|
|
## Performance
|
|
|
|
### Indexes
|
|
|
|
Performance indexes are **automatically created** on startup in `repository.go` via `ensureStatsIndexes()`. Uses `CREATE INDEX IF NOT EXISTS` so it's idempotent.
|
|
|
|
Key indexes: tasks(workspace_id, created_at), task_sessions(task_id, started_at), task_session_turns(task_session_id, started_at, completed_at), task_session_messages(task_session_id, author_type, type), task_session_commits(session_id, committed_at).
|
|
|
|
### Query planner statistics
|
|
|
|
`PRAGMA optimize` runs automatically on database connection close (see `persistence/provider.go`). This keeps SQLite's query planner statistics up to date by only re-analyzing tables whose stats are stale. No manual `ANALYZE` is needed.
|
|
|
|
### Query patterns
|
|
|
|
- Separate subqueries to avoid row multiplication from JOINs
|
|
- `COUNT(DISTINCT id)` to prevent duplicate counting across LEFT JOINs
|
|
- `COALESCE(..., 0)` for NULL safety
|
|
- Recursive CTE for date series (heatmap/charts fill in zero-activity days)
|
|
|
|
## Known Limitations
|
|
|
|
1. **No caching** - every page load runs all 7 queries
|
|
2. **No real-time updates** - single fetch on page load, no WebSocket/polling
|
|
3. **Git tracking gaps** - only commits made during task sessions are tracked
|
|
4. **Duration precision** - `julianday()` returns float, cast to int64 (minor precision loss)
|
|
5. **Incomplete turns** - turns with NULL `completed_at` count in totals but contribute 0ms to duration
|
|
6. **"Month" = 30 days** - not a calendar month
|
|
7. **Sequential queries** - the 7 repo calls in the handler run sequentially, not in parallel
|