From c3b3cafcdd1eb794ebe785168bb3038655549d54 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 17 Jun 2026 12:00:58 +0200 Subject: [PATCH] =?UTF-8?q?feat(observability):=20add=20the=20Scrabble=20?= =?UTF-8?q?=E2=80=94=20Messages=20Grafana=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visualises the chat read-receipt metrics added with the read-receipt feature (the dashboard was deferred there per the owner): posted rate by kind, the unread backlog (chat_unread_messages gauge), and the publish-to-read latency (chat_read_duration โ€” average by kind plus overall p50/p95). Mirrors the game-domain dashboard's structure; the file provider auto-discovers it. --- PRERELEASE.md | 2 +- deploy/grafana/dashboards/messages.json | 47 +++++++++++++++++++++++++ docs/ARCHITECTURE.md | 3 +- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 deploy/grafana/dashboards/messages.json diff --git a/PRERELEASE.md b/PRERELEASE.md index 709c373..98890f4 100644 --- a/PRERELEASE.md +++ b/PRERELEASE.md @@ -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) diff --git a/deploy/grafana/dashboards/messages.json b/deploy/grafana/dashboards/messages.json new file mode 100644 index 0000000..a6ce20c --- /dev/null +++ b/deploy/grafana/dashboards/messages.json @@ -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" } + ] + } + ] +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fa0f803..9120572 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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).