feat: harden analytics dashboard
This commit is contained in:
71
docs/database.md
Normal file
71
docs/database.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Database Contract
|
||||
|
||||
This app reads an existing New API PostgreSQL schema. It does not own migrations.
|
||||
|
||||
## Required Environment
|
||||
|
||||
```bash
|
||||
PG_CONNECTION_STRING=postgres://user:password@host:5432/database
|
||||
```
|
||||
|
||||
## Tables Read By The App
|
||||
|
||||
### `logs`
|
||||
|
||||
Primary analytics source. Only rows with `type = 2` are included.
|
||||
|
||||
Expected columns:
|
||||
|
||||
- `id`: monotonically increasing log id, used for latest-first log pagination.
|
||||
- `created_at`: Unix timestamp in seconds.
|
||||
- `type`: log type; analytics filters to `2`.
|
||||
- `user_id`: user id.
|
||||
- `username`: username captured on the log row.
|
||||
- `model_name`: requested model name.
|
||||
- `channel_id`: upstream channel id.
|
||||
- `prompt_tokens`: input token count.
|
||||
- `completion_tokens`: output token count.
|
||||
- `quota`: New API quota units.
|
||||
- `use_time`: request latency in milliseconds.
|
||||
- `is_stream`: stream flag.
|
||||
- `token_name`: API token display name.
|
||||
- `other`: JSON text with optional `upstream_model_name`, `cache_creation_tokens`, and `cache_tokens`.
|
||||
|
||||
### `users`
|
||||
|
||||
Used for display-name enrichment.
|
||||
|
||||
Expected columns:
|
||||
|
||||
- `id`
|
||||
- `username`
|
||||
- `display_name`
|
||||
|
||||
### `channels`
|
||||
|
||||
Used for channel-name enrichment.
|
||||
|
||||
Expected columns:
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
|
||||
## Recommended Indexes
|
||||
|
||||
For large `logs` tables, add or verify indexes that match dashboard filters:
|
||||
|
||||
```sql
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_type_created_at
|
||||
ON logs (type, created_at);
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_type_username_created_at
|
||||
ON logs (type, username, created_at);
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_type_channel_created_at
|
||||
ON logs (type, channel_id, created_at);
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_logs_type_id_desc
|
||||
ON logs (type, id DESC);
|
||||
```
|
||||
|
||||
Model filtering uses the real-model expression derived from `other::jsonb->>'upstream_model_name'` with fallback to `model_name`. Add an expression index only after validating the exact production column type and query plan.
|
||||
29
docs/metrics.md
Normal file
29
docs/metrics.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Metrics Contract
|
||||
|
||||
All analytics endpoints filter to `logs.type = 2`.
|
||||
|
||||
## Core Metrics
|
||||
|
||||
- **Calls:** count of matching log rows.
|
||||
- **Input tokens:** sum of `prompt_tokens`.
|
||||
- **Output tokens:** sum of `completion_tokens`.
|
||||
- **Cache creation tokens:** `other.cache_creation_tokens`, parsed as bigint when present, otherwise `0`.
|
||||
- **Cache read tokens:** `other.cache_tokens`, parsed as bigint when present, otherwise `0`.
|
||||
- **Total tokens:** input + output + cache creation + cache read.
|
||||
- **Quota:** sum of `quota`.
|
||||
- **Cost:** quota converted to USD by `quota / 500000`.
|
||||
|
||||
The quota conversion constant is defined in `lib/metrics.ts` as `QUOTA_UNITS_PER_USD`.
|
||||
|
||||
## Dimensions
|
||||
|
||||
- **User:** grouped by `logs.user_id` and `logs.username`, displayed with `users.display_name` when present.
|
||||
- **Model:** grouped by real model name. Real model is `other.upstream_model_name` when present, otherwise `logs.model_name`.
|
||||
- **Channel:** grouped by `logs.channel_id`, displayed with `channels.name` when present.
|
||||
- **Token name:** grouped by trimmed `logs.token_name`; blank values display as the localized unnamed-token label.
|
||||
|
||||
## Time Handling
|
||||
|
||||
Date ranges are Unix timestamps in seconds. Day, week, month, and hour buckets are computed in the `Asia/Shanghai` timezone.
|
||||
|
||||
Invalid timestamps or reversed ranges return HTTP 400.
|
||||
Reference in New Issue
Block a user