feat(observability): Scrabble — Messages Grafana dashboard #78

Merged
developer merged 1 commits from feature/messages-dashboard into development 2026-06-17 10:05:16 +00:00
3 changed files with 50 additions and 2 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ the edge before prod. Each phase maps back to the owner's raw pre-release TODO l
| AI | Honest AI opponent in quick game: an explicit 🤖 AI / 👤 random selector (AI default); the robot is seated and moves at once; 7-day inactivity loss (the per-turn timeout reused); chat/nudge disabled, no statistics; the opponent is shown as 🤖 everywhere | owner ad-hoc | **done** |
| AD | Advertising banner ("ad network"): server-driven weighted campaigns (percent weight + validity window; the perpetual default fills the remainder up to 100%), bilingual messages shown by bot (`service_language`); eligibility = free account + empty hint wallet + no `no_banner` role (guests included); the resolved feed rides `profile.get` with a `notify` `banner` re-poll on eligibility change; `/_gm/banners` admin + global display timings; client smooth-weighted-round-robin rotation + fade-out/gap/fade-in UX. A single `app.load` bootstrap aggregator was considered and **deferred** (see ARCHITECTURE §10). | owner ad-hoc | **done** (PR1 backend+admin, PR2 UI rotation) |
| GL | Simultaneous quick-game cap (10): grey "New Game" + a lobby notice at the cap; backend gate on quick enqueue + invitation creation (409 `game_limit_reached`), accepting invitations exempt; `at_game_limit` rides `games.list` | owner ad-hoc | **done** |
| CR | In-game chat read receipts: per-message `unread_seats` bitmask (migration `00008`); a per-viewer unread **dot** in the lobby + game header (a nudge counts and clears when its recipient moves); reading = opening the move history (the 💬 fade-blinks twice) or the chat, acked (`chat.read`) only when unread; `chat_read_duration` + `chat_unread_messages` metrics + tracing; admin unread-only filter / read column / per-seat read card | owner ad-hoc | **done** |
| CR | In-game chat read receipts: per-message `unread_seats` bitmask (migration `00008`); a per-viewer unread **dot** in the lobby + game header (a nudge counts and clears when its recipient moves); reading = opening the move history (the 💬 fade-blinks twice) or the chat, acked (`chat.read`) only when unread; `chat_read_duration` + `chat_unread_messages` metrics + tracing + the **Scrabble — Messages** Grafana dashboard (follow-up PR); a message to a disguised robot opponent is born read; admin unread-only filter / read column / per-seat read card | owner ad-hoc | **done** |
| → | Stage 18 — prod contour deploy | — | see [`PLAN.md`](PLAN.md) |
## Key findings (these reshaped the raw list — read before starting a phase)
+47
View File
@@ -0,0 +1,47 @@
{
"uid": "scrabble-messages",
"title": "Scrabble — Messages",
"tags": ["scrabble"],
"timezone": "",
"schemaVersion": 39,
"version": 1,
"refresh": "30s",
"time": { "from": "now-24h", "to": "now" },
"panels": [
{
"type": "timeseries",
"title": "Chat entries posted (rate by kind)",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [{ "refId": "A", "expr": "sum(rate(chat_messages_total[15m])) by (kind)", "legendFormat": "{{kind}}" }]
},
{
"type": "timeseries",
"title": "Unread chat entries (backlog)",
"description": "Chat entries with at least one recipient seat that has not read them yet (chat_unread_messages gauge).",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [{ "refId": "A", "expr": "sum(chat_unread_messages)", "legendFormat": "unread" }]
},
{
"type": "timeseries",
"title": "Publish → read latency (avg by kind)",
"description": "Mean time from a chat entry being posted to a recipient reading it (move-history / chat open, or a nudge answered by moving). A born-read message to a robot records no read, so it does not enter this average.",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
"fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [{ "refId": "A", "expr": "sum(rate(chat_read_duration_sum[15m])) by (kind) / sum(rate(chat_read_duration_count[15m])) by (kind)", "legendFormat": "{{kind}}" }]
},
{
"type": "timeseries",
"title": "Publish → read latency (p50 / p95)",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
"fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [
{ "refId": "A", "expr": "histogram_quantile(0.5, sum(rate(chat_read_duration_bucket[15m])) by (le))", "legendFormat": "p50" },
{ "refId": "B", "expr": "histogram_quantile(0.95, sum(rate(chat_read_duration_bucket[15m])) by (le))", "legendFormat": "p95" }
]
}
]
}
+2 -1
View File
@@ -761,7 +761,8 @@ edits take effect on the next `profile.get` (open/reconnect/foreground), not mid
turn-timeout seat drop), `chat_messages_total` (`kind` = message/nudge) and
`robot_games_finished_total`; a histogram `chat_read_duration` (chat publish-to-read
latency by `kind`); observable gauges `game_cache_active` and `chat_unread_messages`
(chat entries with `unread_seats <> 0`); the gateway
(chat entries with `unread_seats <> 0`, both chat metrics surfaced on the **Scrabble —
Messages** dashboard); the gateway
`edge_request_duration` (the UI-perceived roundtrip, by `message_type`/`result`);
and Go runtime/heap metrics. Game-scoped metrics carry a `variant` attribute
(scrabble_en/scrabble_ru/erudit_ru).