From 1bec6bb8ad6bcc2d36f630fe40720d022940df26 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 6 Jun 2026 16:50:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(superset):=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=BE=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=20KPI=20=D0=B8=20=D0=B2=D0=BE=D1=80=D0=BE=D0=BD=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=88=D0=B1=D0=BE=D1=80=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужно убрать дублирующий KPI Unique Sessions и сделать эталонный dashboard честнее для учебного анализа. - Что: - обновлены KPI, добавлена Conversion to /confirmation и Page Funnel. - добавлена идемпотентная миграция старых chart names без дублей. - синхронизированы документация, урок 6 и спека редизайна. - добавлен handoff для продолжения работы в новой сессии. - Проверка: - python3 -m py_compile superset/create_dashboard.py. - make superset-dashboard. - Superset metadata: dashboard_charts=10, obsolete_unique_sessions=0, page_funnel_type=funnel. --- ...superset-dashboard-redesign-implemented.md | 96 ++++++++++++++++++ docs/SUPERSET_DASHBOARD.md | 18 +++- docs/course/lessons/06_superset_bi.md | 74 +++++++++----- .../2026-06-06-superset-dashboard-redesign.md | 19 ++-- superset/create_dashboard.py | 98 +++++++++++++------ 5 files changed, 237 insertions(+), 68 deletions(-) create mode 100644 .scratch/handoffs/2026-06-06-superset-dashboard-redesign-implemented.md diff --git a/.scratch/handoffs/2026-06-06-superset-dashboard-redesign-implemented.md b/.scratch/handoffs/2026-06-06-superset-dashboard-redesign-implemented.md new file mode 100644 index 0000000..52c9e45 --- /dev/null +++ b/.scratch/handoffs/2026-06-06-superset-dashboard-redesign-implemented.md @@ -0,0 +1,96 @@ +# Handoff: Superset dashboard redesign implemented + +Дата: 2026-06-06 · Язык сессии: русский + +## Статус + +Редизайн Superset-дашборда реализован и закоммичен. + +Коммит: + +```text +04995af832743ed984c25af08a185f7abe0485f1 +feat(superset): обновлен состав KPI и воронки дашборда +``` + +Рабочее дерево было чистым перед созданием этого handoff; сам handoff создан после коммита. + +## Источники истины + +- Спека: [`docs/specs/2026-06-06-superset-dashboard-redesign.md`](../../docs/specs/2026-06-06-superset-dashboard-redesign.md) +- Реализация: [`superset/create_dashboard.py`](../../superset/create_dashboard.py) +- Пользовательская документация: [`docs/SUPERSET_DASHBOARD.md`](../../docs/SUPERSET_DASHBOARD.md) +- Урок 6: [`docs/course/lessons/06_superset_bi.md`](../../docs/course/lessons/06_superset_bi.md) +- Предыдущий handoff: [`2026-06-06-superset-dashboard-redesign.md`](./2026-06-06-superset-dashboard-redesign.md) + +## Что сделано + +- KPI-полоса теперь: + `Total Events · Unique Users · Avg Events/Visit · Conversion to /confirmation`. +- `Unique Sessions` удалён из KPI и из Superset metadata как obsolete chart. +- `Avg Events/Session` переименован в `Avg Events/Visit` идемпотентно через `previous_slice_names`. +- `Top Pages` переименован в `Page Funnel` и переведён на `viz_type: funnel`. +- Conversion считается как page-funnel metric: + `countIf(page_url_path = '/confirmation') / countIf(page_url_path = '/home')`. +- UTM-таблица оставлена без мёртвых колонок `purchases` / `add_to_cart`. +- `Events by Hour` оставлен: на полном датасете есть два часовых бакета. +- Документация и урок 6 синхронизированы с полным датасетом и новым dashboard flow. + +## Проверки + +Выполнено: + +```bash +make data +make transform +python3 -m py_compile superset/create_dashboard.py +make superset-dashboard +make superset-dashboard +``` + +Результаты: + +- полный датасет в DM: `1000` events, `99` visits, `99` users; +- `Avg Events/Visit = 10.1`; +- `Conversion to /confirmation = 35 / 426 = 8.2%`; +- Superset dashboard metadata: + - `dashboard_charts = 10`; + - `obsolete_unique_sessions = 0`; + - `page_funnel_type = funnel`; +- Superset API `/api/v1/dashboard/1/datasets` вернул `200`; +- browser chart-data для `Page Funnel` вернул `status: success`, 6 строк; +- browser chart-data для `Data Quality Summary` вернул `errors: []`, `status: success`. + +## Визуальная проверка + +Пользователь визуально подтвердил: «Визуально - выглядит норм». + +Скриншоты были сохранены в `/tmp` во время сессии: + +- `/tmp/ecommerce-analytics-dashboard-desktop.png` +- `/tmp/ecommerce-analytics-dashboard-lower.png` + +Они могут исчезнуть после очистки `/tmp`; при необходимости переснять через `playwright-cli`. + +## Важные детали реализации + +- Для Superset 4.1.2 `funnel` проверялся через MCP Context7 по `/apache/superset`; Context7 не дал точную строку `viz_type`. +- Решение подтверждено по установленному Superset 4.1.2: bundled example `Featured Charts/Funnel.yaml` использует `viz_type: funnel`. +- `create_dashboard.py` ищет старые имена через `previous_slice_names`, чтобы не плодить дубли при rename. +- Cleanup obsolete charts удаляет все найденные `🎯 Unique Sessions`, если metadata уже была загрязнена дублями. + +## Что осталось + +Обязательных незакрытых задач по редизайну нет. + +Возможные следующие шаги, если продолжать dashboard-направление: + +- экспортировать обновлённый dashboard JSON через `make superset-export`, если экспортный артефакт должен соответствовать новой metadata; +- отдельно модернизировать legacy chart types (`pie`, `world_map`, `dist_bar`) на ECharts, если это станет целью следующего захода; +- при развитии генератора перенести требования из спеки в его `KNOWN_ISSUES.md`. + +## Suggested skills + +- `playwright-cli` — если нужно переснять визуальную проверку dashboard. +- `conventional-commits` — если нужно коммитить этот handoff или дальнейшие правки. +- `diagnose` — если Superset chart-data или layout начнут падать после сброса volumes. diff --git a/docs/SUPERSET_DASHBOARD.md b/docs/SUPERSET_DASHBOARD.md index ff61627..b6552d1 100644 --- a/docs/SUPERSET_DASHBOARD.md +++ b/docs/SUPERSET_DASHBOARD.md @@ -59,10 +59,12 @@ make superset-dashboard #### KPI-блок (верх дашборда) - **📊 Total Events** — общее количество событий - **👤 Unique Users** — уникальные пользователи -- **🎯 Unique Sessions** — уникальные сессии (click_id) -- **📈 Avg Events/Session** — среднее количество событий на сессию +- **📈 Avg Events/Visit** — среднее количество событий на визит (`click_id`) +- **🎯 Conversion to /confirmation** — доля просмотров `/confirmation` от просмотров `/home` KPI разложены в одну строку по 12-колоночной сетке Superset: четыре блока по 3 колонки. +`Unique Sessions` не вынесен отдельной KPI-плиткой, потому что в демо-данных +`user_domain_id` и `click_id` идут 1:1 и дают то же число, что `Unique Users`. #### Динамика трафика - **📅 Events by Hour** — линейный график событий по часам @@ -73,7 +75,13 @@ KPI разложены в одну строку по 12-колоночной с #### Маркетинг - **🔗 UTM Effectiveness Table** — таблица эффективности UTM-меток -- **📄 Top Pages** — bar chart топ-20 страниц +- **🪜 Page Funnel** — funnel chart по просмотрам страниц, от `/home` к `/confirmation` + +> **Что проверили по Superset 4.1.2.** Через MCP Context7 проверили официальную +> библиотеку `/apache/superset`; документация не дала точной строки `viz_type`. +> В установленном Superset 4.1.2 дополнительно проверили bundled example +> `Featured Charts/Funnel.yaml` и frontend assets: для воронки используется +> `viz_type: funnel`, поэтому dashboard создаёт именно funnel chart. #### Качество данных - **🔍 Data Quality Summary** — статистика по слоям STG/ODS/DDS @@ -219,7 +227,11 @@ make superset-restart # Полная переинициализация docker compose down -v docker compose up -d +make ddl +make data +make transform make superset-init +make superset-dashboard ``` ### Нет данных в чартах diff --git a/docs/course/lessons/06_superset_bi.md b/docs/course/lessons/06_superset_bi.md index 047a13b..3d60cb5 100644 --- a/docs/course/lessons/06_superset_bi.md +++ b/docs/course/lessons/06_superset_bi.md @@ -63,17 +63,19 @@ DM-витрина — это SQL-объект в ClickHouse. Она задаёт ## 2. Руки: запускаем Superset и смотрим дашборд -Подними стенд и прогони маленький срез: +Подними стенд и прогони полный демо-датасет: ```bash make up make ddl -LIMIT=50 make data +make data make transform ``` `make transform` прогоняет цепочку STG → ODS → DDS → DM вне Airflow. Для этого урока так -быстрее: нам нужен готовый DM-слой, а не разбор DAG. +быстрее: нам нужен готовый DM-слой, а не разбор DAG. Отладочный срез через +`LIMIT=50 make data` можно использовать для быстрых экспериментов, но эталонный dashboard +и числа урока рассчитаны на полном наборе данных. Теперь инициализируй Superset: @@ -84,7 +86,16 @@ make superset-init Эта команда создаёт или обновляет: - подключение `clickhouse_dwh`; -- 6 datasets поверх `dm.*`; +- 6 datasets поверх `dm.*`. + +После этого создай или обнови charts и сам dashboard: + +```bash +make superset-dashboard +``` + +Эта команда создаёт или обновляет: + - 10 charts; - dashboard `E-commerce Analytics Dashboard`. @@ -105,12 +116,17 @@ http://localhost:8088/superset/dashboard/1/ На экране должны быть блоки: -- KPI сверху: `Total Events`, `Unique Users`, `Unique Sessions`, `Avg Events/Session`; +- KPI сверху: `Total Events`, `Unique Users`, `Avg Events/Visit`, + `Conversion to /confirmation`; - динамика: `Events by Hour`, `Traffic by Device`; - география: `Geography Map`; -- маркетинг: `UTM Effectiveness Table`, `Top Pages`; +- маркетинг: `UTM Effectiveness Table`, `Page Funnel`; - качество данных: `Data Quality Summary`. +`Conversion to /confirmation` считается как просмотры `/confirmation` / просмотры `/home`. +Это page-funnel метрика, а не доля визитов: она совпадает с тем, как ниже устроен chart +`Page Funnel`. + ### Фильтр даты В демо-данных события датированы `2022-11-28`. В текущей конфигурации dashboard фильтр даты @@ -136,7 +152,7 @@ SELECT count() AS events FROM dm.v_events_enriched; ``` -И посмотри, откуда берётся график `Top Pages`: +И посмотри, откуда берётся график `Page Funnel`: ```sql SELECT page_url_path, sum(pageviews) AS pageviews @@ -233,18 +249,20 @@ Metadata Superset живёт в PostgreSQL, а сами данные остаю ```python { - "slice_name": "📄 Top Pages", - "viz_type": "dist_bar", + "slice_name": "🪜 Page Funnel", + "viz_type": "funnel", "dataset_name": "v_top_pages_daily", "params": { "groupby": ["page_url_path"], - "metrics": [ - {"expressionType": "SQL", "sqlExpression": "SUM(pageviews)", "label": "Pageviews"} - ], + "metric": { + "expressionType": "SQL", + "sqlExpression": "SUM(pageviews)", + "label": "Pageviews" + }, "row_limit": 20, "time_range": "No filter", - "orientation": "vertical", - "show_legend": False + "sort_by_metric": True, + "percent_calculation_type": "first_step" } } ``` @@ -280,14 +298,16 @@ Native filters создаются в `build_dashboard_metadata`. Там есть > **Что проверили по API.** Перед уроком Superset сверили через MCP Context7 (`/apache/superset`): > в Superset есть отдельные сущности charts и dashboards, metadata хранится отдельно от -> подключаемых источников данных, а row limit — часть настройки запросов и конфигурации. -> Поэтому в уроке не лезем в REST API Superset, а работаем через уже существующий скрипт стенда. +> подключаемых источников данных. Для `funnel` Context7 не дал точную строку `viz_type`, поэтому +> дополнительно проверили установленный Superset 4.1.2: bundled example +> `Featured Charts/Funnel.yaml` использует `viz_type: funnel`. Поэтому в уроке не лезем в REST API +> Superset, а работаем через уже существующий скрипт стенда. --- -## 4. Управляемая правка: уменьшаем Top Pages +## 4. Управляемая правка: уменьшаем Page Funnel -Сейчас chart `Top Pages` показывает до 20 страниц: +Сейчас chart `Page Funnel` показывает до 20 страниц: ```python "row_limit": 20, @@ -296,7 +316,7 @@ Native filters создаются в `build_dashboard_metadata`. Там есть Сделай маленькую видимую правку: временно покажи только топ-3 страницы. Открой [`superset/create_dashboard.py`](../../../superset/create_dashboard.py), найди chart -`Top Pages` и поменяй: +`Page Funnel` и поменяй: ```python "row_limit": 20, @@ -314,7 +334,7 @@ Native filters создаются в `build_dashboard_metadata`. Там есть make superset-dashboard ``` -Вернись в Superset и обнови страницу dashboard. В chart `Top Pages` должно остаться не больше +Вернись в Superset и обнови страницу dashboard. В chart `Page Funnel` должно остаться не больше трёх страниц. Если фильтр даты снова скрыл данные, поставь **Date Range → No filter** и нажми **Apply filters**. @@ -339,17 +359,17 @@ make superset-dashboard make superset-dashboard ``` -После обновления страницы chart `Top Pages` снова может показывать до 20 страниц. +После обновления страницы chart `Page Funnel` снова может показывать до 20 страниц. Если после экспериментов Superset выглядит странно, самый простой учебный возврат dashboard metadata к конфигурации из репозитория: ```bash -make superset-init +make superset-dashboard ``` -Данные в ClickHouse эта команда не удаляет. Она повторно применяет подключение, datasets и -dashboard metadata Superset. +Данные в ClickHouse эта команда не удаляет. Она повторно применяет charts и dashboard +metadata Superset. --- @@ -362,8 +382,8 @@ dashboard metadata Superset. | открыть **Datasets** | Superset UI | есть datasets `v_events_enriched`, `v_top_pages_daily`, `dq_summary` | | открыть dashboard | Superset UI | видны KPI, маркетинг, география и качество данных | | поставить **Date Range → No filter** | dashboard filters | графики не скрываются из-за даты `2022-11-28` | -| поменять `row_limit` у `Top Pages` на `3` и запустить `make superset-dashboard` | chart `Top Pages` | не больше трёх страниц | -| вернуть `row_limit` на `20` и запустить `make superset-dashboard` | chart `Top Pages` | ограничение снова до 20 страниц | +| поменять `row_limit` у `Page Funnel` на `3` и запустить `make superset-dashboard` | chart `Page Funnel` | не больше трёх страниц | +| вернуть `row_limit` на `20` и запустить `make superset-dashboard` | chart `Page Funnel` | ограничение снова до 20 страниц | Вопросы для созвона: @@ -378,7 +398,7 @@ dashboard metadata Superset. ## 6. Что должно получиться К концу урока у тебя должен быть открытый dashboard `E-commerce Analytics Dashboard` в Superset. -Сделай скриншот после временной правки `Top Pages`: на нём должно быть видно, что chart показывает +Сделай скриншот после временной правки `Page Funnel`: на нём должно быть видно, что chart показывает не больше трёх страниц. Второй артефакт — короткий абзац своими словами: diff --git a/docs/specs/2026-06-06-superset-dashboard-redesign.md b/docs/specs/2026-06-06-superset-dashboard-redesign.md index 461008c..9351792 100644 --- a/docs/specs/2026-06-06-superset-dashboard-redesign.md +++ b/docs/specs/2026-06-06-superset-dashboard-redesign.md @@ -1,8 +1,8 @@ # Редизайн KPI-полосы и состава чартов дашборда «E-commerce Analytics» Дата: 2026-06-06 -Статус: Accepted (не реализовано — код `superset/create_dashboard.py` правится -отдельным заходом на реализацию) +Статус: Implemented (`superset/create_dashboard.py`, `docs/SUPERSET_DASHBOARD.md`, +`docs/course/lessons/06_superset_bi.md`) Связано: [`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) @@ -141,11 +141,16 @@ KPI-полоса: ## Open questions -- **Точная формула Conversion:** доля визитов с ≥1 pageview `/confirmation`, или - просмотры `/confirmation` / просмотры `/home`? Влияет на число (~8%) и на то, - что именно учим (визит-конверсия vs page-to-page). -- **Events by Hour** — оставляем ли (зависит от проверки на пустоту на полных - данных: разброс `event_ts` по часам). +- **Resolved: точная формула Conversion.** Используем page-funnel conversion: + `countIf(page_url_path = '/confirmation') / countIf(page_url_path = '/home')`. + На полном датасете это `35 / 426 = 8.2%`. Визитовая формула даёт 25.3% и + отклонена, потому что KPI должен совпадать с логикой chart `Page Funnel`. +- **Resolved: Events by Hour.** Оставляем: на полном датасете есть два часовых + бакета (`20 → 256`, `21 → 744`), график не пустой. +- **Resolved: Funnel в Superset 4.1.2.** MCP Context7 по `/apache/superset` не + дал точной строки `viz_type`; установленный Superset 4.1.2 проверен по bundled + example `Featured Charts/Funnel.yaml` и frontend assets. Используем + `viz_type: funnel`. - **Требования к будущему генератору**, вытекающие из упёртостей этих данных (разнообразие `event_type`, возвраты пользователей → sessions>users, реалистичная воронка) — при возврате к генератору перенести в его diff --git a/superset/create_dashboard.py b/superset/create_dashboard.py index 5c76750..5bdbd88 100644 --- a/superset/create_dashboard.py +++ b/superset/create_dashboard.py @@ -25,6 +25,11 @@ logger = logging.getLogger(__name__) sys.path.insert(0, '/app') +OBSOLETE_CHART_NAMES = { + "🎯 Unique Sessions", +} + + # Конфигурация чартов CHARTS_CONFIG = [ # KPI блок @@ -63,32 +68,36 @@ CHARTS_CONFIG = [ } }, { - "slice_name": "🎯 Unique Sessions", - "viz_type": "big_number_total", - "dataset_name": "v_events_enriched", - "params": { - "metric": { - "expressionType": "SQL", - "sqlExpression": "COUNT(DISTINCT click_id)", - "label": "Unique Sessions", - "optionName": "metric_3" - }, - "y_axis_format": ",d", - "time_range": "No filter" - } - }, - { - "slice_name": "📈 Avg Events/Session", + "slice_name": "📈 Avg Events/Visit", + "previous_slice_names": ["📈 Avg Events/Session"], "viz_type": "big_number_total", "dataset_name": "v_events_enriched", "params": { "metric": { "expressionType": "SQL", "sqlExpression": "COUNT(*) / COUNT(DISTINCT click_id)", - "label": "Avg Events/Session", + "label": "Avg Events/Visit", "optionName": "metric_4" }, - "y_axis_format": ".2f", + "y_axis_format": ".1f", + "time_range": "No filter" + } + }, + { + "slice_name": "🎯 Conversion to /confirmation", + "viz_type": "big_number_total", + "dataset_name": "v_events_enriched", + "params": { + "metric": { + "expressionType": "SQL", + "sqlExpression": ( + "if(countIf(page_url_path = '/home') = 0, 0, " + "countIf(page_url_path = '/confirmation') / countIf(page_url_path = '/home'))" + ), + "label": "Conversion to /confirmation", + "optionName": "metric_5" + }, + "y_axis_format": ".1%", "time_range": "No filter" } }, @@ -175,18 +184,32 @@ CHARTS_CONFIG = [ } }, { - "slice_name": "📄 Top Pages", - "viz_type": "dist_bar", + "slice_name": "🪜 Page Funnel", + "previous_slice_names": ["📄 Top Pages"], + # В Superset 4.1.2 `funnel` есть в frontend-плагинах и примерах + # поставки, хотя legacy registry `superset.viz.viz_types` его не + # показывает. Параметры взяты из bundled Featured Charts/Funnel.yaml. + "viz_type": "funnel", "dataset_name": "v_top_pages_daily", "params": { "groupby": ["page_url_path"], - "metrics": [ - {"expressionType": "SQL", "sqlExpression": "SUM(pageviews)", "label": "Pageviews"} - ], + "metric": { + "expressionType": "SQL", + "sqlExpression": "SUM(pageviews)", + "label": "Pageviews" + }, "row_limit": 20, "time_range": "No filter", - "orientation": "vertical", - "show_legend": False + "sort_by_metric": True, + "percent_calculation_type": "first_step", + "color_scheme": "supersetColors", + "show_legend": True, + "legendOrientation": "top", + "legendMargin": 50, + "tooltip_label_type": 5, + "number_format": "SMART_NUMBER", + "show_labels": True, + "show_tooltip_labels": True } }, # Качество данных @@ -233,13 +256,13 @@ DASHBOARD_CONFIG = { DASHBOARD_ROWS = [ # KPI-полоса: 4 числа в один ряд [("📊 Total Events", 3), ("👤 Unique Users", 3), - ("🎯 Unique Sessions", 3), ("📈 Avg Events/Session", 3)], + ("📈 Avg Events/Visit", 3), ("🎯 Conversion to /confirmation", 3)], # Динамика во времени + разрез по устройствам [("📅 Events by Hour", 8), ("📱 Traffic by Device", 4)], # География + эффективность маркетинговых каналов [("🌍 Geography Map", 6), ("🔗 UTM Effectiveness Table", 6)], # Популярные страницы + качество данных - [("📄 Top Pages", 6), ("🔍 Data Quality Summary", 6)], + [("🪜 Page Funnel", 6), ("🔍 Data Quality Summary", 6)], ] # Высота строки в grid-units Superset (одинаковая для всех чартов строки — @@ -410,13 +433,17 @@ def main() -> bool: params["viz_type"] = chart_config["viz_type"] serialized_params = json.dumps(params) - # Проверяем, существует ли уже чарт - existing = db.session.query(Slice).filter_by( - slice_name=chart_config["slice_name"] - ).first() + # Проверяем, существует ли уже чарт. Старые имена нужны для + # идемпотентного rename без дублей в списке Charts. + chart_names = [chart_config["slice_name"]] + chart_names.extend(chart_config.get("previous_slice_names", [])) + existing = db.session.query(Slice).filter( + Slice.slice_name.in_(chart_names) + ).order_by(Slice.id.asc()).first() if existing: # Синхронизируем параметры существующего чарта с конфигом. + existing.slice_name = chart_config["slice_name"] existing.viz_type = chart_config["viz_type"] existing.datasource_id = dataset.id existing.datasource_type = "table" @@ -456,6 +483,15 @@ def main() -> bool: db.session.rollback() logger.info(f"Created/Found {len(created_charts)} charts") + + current_chart_names = {chart_config["slice_name"] for chart_config in CHARTS_CONFIG} + for obsolete_name in sorted(OBSOLETE_CHART_NAMES - current_chart_names): + obsolete_charts = db.session.query(Slice).filter_by(slice_name=obsolete_name).all() + for obsolete in obsolete_charts: + db.session.delete(obsolete) + logger.info("Deleted obsolete chart: %s (ID: %s)", obsolete_name, obsolete.id) + db.session.flush() + metadata_json = build_dashboard_metadata(datasets_by_name.get("v_events_enriched")) # Создаём позиции чартов для layout.