docs(superset): спека редизайна дашборда и handoff

- Зачем:
  - дашборд-эталон показывал две одинаковые KPI-плитки; нужен честный состав метрик
    на полных данных, зафиксированный до реализации.
- Что:
  - docs/specs/2026-06-06-...: KPI Events/Users/Avg per Visit/Conversion, Top Pages → Funnel, триаж чартов.
  - .scratch/handoffs/2026-06-06-...: handoff для реализации в новой сессии.
- Проверка:
  - числа спеки сверены с прямым запросом в ClickHouse на полном датасете.
This commit is contained in:
2026-06-06 16:12:10 +03:00
parent 79481a9351
commit 7c7e9e5a7c
2 changed files with 213 additions and 0 deletions
@@ -0,0 +1,152 @@
# Редизайн KPI-полосы и состава чартов дашборда «E-commerce Analytics»
Дата: 2026-06-06
Статус: Accepted (не реализовано — код `superset/create_dashboard.py` правится
отдельным заходом на реализацию)
Связано: [`CONTEXT.md`](../../CONTEXT.md), [ADR-0002](../adr/0002-specs-as-durable-design-docs.md),
урок [`docs/course/lessons/06_superset_bi.md`](../course/lessons/06_superset_bi.md)
## Problem
Дашборд-эталон показывает две KPI-плитки — `Unique Users` и `Unique Sessions`с
одинаковым числом (26 на срезе, 99 на полных данных), потому что в датасете
`user_domain_id ↔ click_id` строго 1:1. Две одинаковые цифры читаются как баг
расчёта и ничему не учат менти, который копирует дашборд как образец. Нужен
честный, копируемый состав KPI и чартов на полных данных.
## Context
- Дашборд — учебный эталон для менти. Требований обратной совместимости нет:
состав можно менять свободно, критерий — учебная ценность.
- Доменные термины зафиксированы в `CONTEXT.md`: **пользователь** =
`user_domain_id`, **визит/сессия** = `click_id`, **событие** = `event_id`.
- Источник данных — статический сид `data/*.jsonl`. Синтетический генератор
(ветка `feature/data-generator`) источником **не** является и семантику ломает
(см. `generator/KNOWN_ISSUES.md` на той ветке).
## Goals
- KPI-полоса из 4 различающихся честных метрик (без дубля одинаковых чисел).
- Превратить слабые места данных в учебные объекты, а не маскировать их.
- Консистентность с `CONTEXT.md` и уроком 6.
## Non-goals
- Не модернизируем **типы** виджетов (`pie`/`world_map`/`dist_bar` → ECharts) —
отдельный косметический заход.
- Не трогаем эмодзи в тайтлах, секции-заголовки, языковой винегрет.
- Не трогаем генератор.
- Не правим код `create_dashboard.py` в рамках этой спеки — это реализация.
## Current state
Полный датасет (`make data` без `LIMIT`):
```
события: 1000 | визиты (click_id): 99 | пользователи (user_domain_id): 99 (1:1)
avg событий на визит: 10.10 | bounce (визиты с 1 событием): 4/99 = 4.0%
event_type: 100% pageview (других типов НЕТ)
page_url_path — воронка: /home 426 → /product_a 236 + /product_b 157 →
/cart 93 → /payment 53 → /confirmation 35 (~8% в конец)
geo_country: 40 | device_type: 2 (Mobile/Computer) | utm_source: 5 | utm_medium: 4
```
Текущие чарты и триаж по учебной ценности:
| Чарт | Тип | Вердикт |
|---|---|---|
| Total Events, Unique Users | big_number_total | оставить (KPI) |
| Unique Sessions | big_number_total | **убрать** (дубль Users) |
| Avg Events/Session | big_number_total | оставить, переименовать `/Visit` |
| Top Pages | dist_bar | **апгрейд в Funnel** (центральный учебный объект) |
| UTM Effectiveness | table | оставить; **выкинуть колонки purchases/add_to_cart** (всегда 0) |
| Geography Map | world_map | оставить (40 стран; тип модернизировать отдельно) |
| Traffic by Device | pie | оставить |
| Events by Hour | line | **проверить на пустоту**, иначе дропнуть |
| Data Quality Summary | dist_bar | оставить |
## Options considered
- **A — честная воронка вовлечённости (стандартный KPI-strip).** 4 плитки
объём→охват→глубина→качество; различие user/session уходит в текст.
- **B — оставить обе плитки + Markdown-панель**, объясняющая равенство 1:1.
- **C — strip как форма модели:** воронка `Events → Visits → Users` вместо
бизнес-KPI; вырожденное 1:1 становится наглядным.
## Recommended approach
**A для KPI-полосы + идея C отдельным чартом-воронкой.** KPI-полоса —
стандартная и чистая (копируемый паттерн); иерархия модели подаётся отдельным
чартом, а `Top Pages` превращается в **Funnel** по страницам.
KPI-полоса:
```
📊 Total Events 1000 · 👤 Unique Users 99 · 📈 Avg Events/Visit 10.1 · 🎯 Conversion to /confirmation ~8%
```
## Rationale
- Конверсия через `page_url_path` честнее блёклого bounce (4%) и превращает
«всё pageview» из слабости в сильный урок: воронка реально затухает.
- Различие user/session учим текстом (`CONTEXT.md` + урок 6), а не двумя
одинаковыми числами — это убирает сигнал «баг».
- Стандартный KPI-strip ценнее для копирования, чем нестандартный вариант C;
но идею C сохраняем как отдельный наглядный чарт.
## Decisions and rejected alternatives
- **Данные: полный датасет** и для дашборда, и для урока 6. Срез `LIMIT=50`
отклонён: числа эталона разойдутся с тем, что менти получит у себя. Урок 6 —
убрать `LIMIT=50 make data`. (Срез `LIMIT` оставить в доке как явный приём
отладки, не как основной прогон. Замечание: ограничение `head -n` исходно
возникло, чтобы агентский контекст не давился полным jsonl — это ограничение
для агента, не для пайплайна.)
- **Убрать `Unique Sessions` из KPI** — дубль `Unique Users` при 1:1.
- **4-я KPI = Conversion**, не bounce. Bounce 4% отклонён как блёклый.
- **Top Pages → Funnel.** Воронка по страницам — главный учебный объект.
- **UTM-таблица: убрать колонки purchases/add_to_cart** — всегда 0, т.к.
`event_type` только `pageview`.
- **Гео/device/DQ не удаляем** — данные оправдывают ценность; модернизация их
типов виджетов — отдельно (non-goal здесь).
- **«Снести всё legacy»** на практике = убрать дубль-KPI + мёртвые колонки +
проверить Events by Hour. Инсайты не вырезаем — у большинства есть данные.
## Risks and mitigations
- **Funnel в Superset 4.1.2** — проверить доступность `viz_type` воронки до
реализации; при отсутствии — упорядоченный bar.
- **Формула Conversion** — определить точно и валидировать SQL (см. Open
questions).
- **Идемпотентность `create_dashboard.py`** при смене состава (чарты по
`slice_name`, дашборд по `slug`) — переименование `Unique Sessions`/`Top Pages`
не должно плодить дубли; проверить апдейт на месте.
## Validation
- KPI-полоса: 4 разных числа, дубля нет.
- `GET /api/v1/dashboard/<id>/datasets` → 200; DQ Summary без
`Columns missing in datasource`.
- Визуальная проверка через playwright-cli (скриншот в `/tmp`).
- Числа на дашборде сходятся с прямым запросом в ClickHouse.
## Documentation impact
- `docs/SUPERSET_DASHBOARD.md` — обновить раздел «Структура дашборда»
(состав чартов/KPI/фильтров) **в момент реализации**, чтобы пользовательская
дока не описывала несуществующие чарты.
- `docs/course/lessons/06_superset_bi.md` — убрать `LIMIT=50`, синхронизировать
состав чартов и скриншоты.
- `CONTEXT.md` — уже отражает user/visit/event; правок не требует.
## Open questions
- **Точная формула Conversion:** доля визитов с ≥1 pageview `/confirmation`, или
просмотры `/confirmation` / просмотры `/home`? Влияет на число (~8%) и на то,
что именно учим (визит-конверсия vs page-to-page).
- **Events by Hour** — оставляем ли (зависит от проверки на пустоту на полных
данных: разброс `event_ts` по часам).
- **Требования к будущему генератору**, вытекающие из упёртостей этих данных
(разнообразие `event_type`, возвраты пользователей → sessions>users,
реалистичная воронка) — при возврате к генератору перенести в его
`KNOWN_ISSUES.md`.