- Зачем:
- экран отвечал только графиками: итога за период не было, доли категорий
читались по высоте столбиков, а известные пользователи лежали на нуле
рядом с посетителями.
- Что:
- добавлены ряд крупных чисел (выручка, заказы, средний чек, посетители),
кольцо долей категорий и линия доли известных пользователей;
- средний чек и доля известных заведены метриками датасетов, знаменатель
через `nullIf`: на пустом периоде деление Decimal на ноль в ClickHouse
роняет панель;
- фильтр «Период» сужает все панели, включая сверку по `order_day`;
- `label_colors` закрепляет цвет за категорией и за рядами трафика;
- деньги без копеек, единица в названии панели; подписи дат читаются как
даты; сводная сверки развернута классами по строкам и уместилась без
прокрутки;
- обновлены справочник Superset и глава путеводителя.
- Проверка:
- `make lint`;
- `superset import-directory /app/import --overwrite` на живом стенде, экран
снят в браузере: девять панелей отрисованы, цвет категории одинаков на
столбиках и в кольце;
- период 03.06–06.06 сузил все панели, включая сводную по `order_day`;
- `SUM(revenue) / nullIf(SUM(orders), 0)` на периоде без заказов возвращает
NULL, без `nullIf` — исключение 153.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
255 lines
20 KiB
Markdown
255 lines
20 KiB
Markdown
# Superset: витрины глазами
|
||
|
||
Документ описывает зону BI: куда Superset ходит за данными и за своими
|
||
метаданными, из чего собран стендовый дашборд, как снять свежий экспорт после
|
||
правки в интерфейсе и почему правка руками живет только до перезапуска.
|
||
|
||
Состав сюжетов и колонок витрин здесь не повторяется: он живет в
|
||
[спеке](../specs/2026-07-30-stand-v2-realism.md), раздел 7, и в
|
||
[доке слоя DM](dm.md).
|
||
|
||
## Устройство зоны
|
||
|
||
Superset читает ClickHouse пользователем `bi` **на ноде 2** — так задано
|
||
подключением `ClickHouse` (`clickhousedb://bi@clickhouse-02:8123`). Нода 2
|
||
здесь — ловушка правильных ошибок из
|
||
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел «Роли нод»: DDL идет
|
||
с ноды 1, и забытый `ON CLUSTER` виден в дашборде.
|
||
|
||
Пароля в подключении нет. Его отдает штатное хранилище паролей Superset
|
||
(`SQLALCHEMY_CUSTOM_PASSWORD_STORE` в `infra/superset/superset_config.py`),
|
||
которое читает `CLICKHOUSE_BI_PASSWORD` из окружения. Поэтому файл подключения
|
||
безопасно лежит в git, и экспорт из интерфейса не должен возвращать пароль
|
||
обратно в YAML.
|
||
|
||
Свои метаданные — подключения, датасеты, графики, дашборды, пользователи —
|
||
Superset держит в базе `superset` служебного `postgres-metadata`, рядом с базой
|
||
Airflow. В ClickHouse он не пишет ничего.
|
||
|
||
## Дерево импорта
|
||
|
||
`infra/superset/import/` — это дашборд стенда в виде файлов. Каталог
|
||
смонтирован в контейнер как `/app/import`.
|
||
|
||
| Файл | Что описывает |
|
||
|---|---|
|
||
| `metadata.yaml` | версия формата, `1.0.0`; больше в нем ничего нет |
|
||
| `databases/clickhouse.yaml` | подключение к ClickHouse: URI без пароля и постоянный uuid |
|
||
| `datasets/ClickHouse/*.yaml` | три витрины: `revenue_daily_v`, `purchase_vs_orders_v`, `daily_traffic_v` |
|
||
| `charts/*.yaml` | девять графиков: четыре крупных числа, столбики выручки и кольцо долей, сводная таблица сверки, линии трафика и доля известных |
|
||
| `dashboards/business_stories.yaml` | дашборд «Бизнес-сюжеты» и раскладка панелей |
|
||
|
||
Связаны файлы не путями, а uuid. Датасет называет `database_uuid`, график —
|
||
`dataset_uuid`, а дашборд не называет графики вовсе: единственная связь с ними
|
||
лежит в его `position`, где узел `CHART-*` несет `meta.uuid` нужного графика.
|
||
График, которого нет в `position`, импортируется молча и не появляется нигде.
|
||
|
||
Uuid у стенда розданы рукой: общее начало
|
||
`19600000-0000-4000-8000-`, дальше номер объекта в последнем блоке — `1`–`3`
|
||
у датасетов, `101`–`109` у графиков, `201` у дашборда
|
||
(`…-000000000101`). Экспорт из интерфейса приносит свои, сгенерированные;
|
||
их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо
|
||
обновления первой.
|
||
|
||
Датасет несет русские имена колонок, формат чисел и метрики — а график только
|
||
ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а
|
||
не в каждом графике. Два исключения проверены на живом стенде, Superset 6.1.
|
||
Подписи оси график берет не из датасета, а из своего `y_axis_format`: без него
|
||
ось выручки уходит в «1.2M». И разрез сводной таблицы подписан именем колонки
|
||
(`mismatch_class`), хотя колонке даты в той же таблице русское имя достается.
|
||
|
||
Датасеты объявлены физическими (`schema: dm`, `sql: null`), хотя за именем стоит
|
||
представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и
|
||
представлением не знает, а виртуальный датасет со своим SQL просто повторил бы
|
||
запрос, который уже живет в `sql/ddl/60-dm-views.sql`.
|
||
|
||
## Из чего собран экран
|
||
|
||
Четыре строки панелей:
|
||
|
||
- ряд крупных чисел — выручка, заказы, средний чек, посетители;
|
||
- выручка по дням и категориям, рядом доля категорий кольцом;
|
||
- сводная сверки purchase и заказов;
|
||
- посетители и известные пользователи, рядом доля известных.
|
||
|
||
Правая панель пары отвечает на вопрос «сколько из целого» и берет цвета левой:
|
||
категория окрашена одинаково на столбиках и в кольце, доля известных — как
|
||
линия известных пользователей рядом. Цвет закрепляет `label_colors` в
|
||
`metadata` дашборда. Шесть категорий выписаны в него руками по каталогу
|
||
генератора (`generator/src/clickstream_generator/catalog.py`): седьмая
|
||
останется без закрепленного цвета, пока файл не поправят.
|
||
|
||
Средний чек и доля известных считаются метриками датасета —
|
||
`SUM(revenue) / nullIf(SUM(orders), 0)` и то же для доли известных. Знаменатель
|
||
обернут не зря: на периоде без заказов деление Decimal на ноль в ClickHouse —
|
||
не `nan`, а исключение, и панель показала бы ошибку вместо пустоты. У витрины
|
||
выручки есть готовая колонка `aov`, но среднее по ней дало бы другое число:
|
||
день с тремя заказами получил бы тот же вес, что день с тремя сотнями.
|
||
|
||
«Период» — единственный фильтр дашборда, тип `filter_time` с пустой целью
|
||
(`targets: [{}]`). Такой фильтр колонку не называет: он подставляет выбранный
|
||
период в то временное условие, которое уже стоит у каждого графика. Поэтому
|
||
сверка сужается по `order_day`, а остальные панели — по `report_date`.
|
||
|
||
## Импорт при каждой инициализации
|
||
|
||
Последняя строка `infra/superset/init.sh`:
|
||
|
||
```
|
||
superset import-directory /app/import --overwrite
|
||
```
|
||
|
||
Служба `superset-init` отрабатывает перед подъемом самого Superset, то есть при
|
||
каждом `make up`. Без `--overwrite` второй прогон падал бы на первом же
|
||
знакомом uuid; с ним объект обновляется на месте, и повторная инициализация
|
||
проходит без ошибок.
|
||
|
||
Отсюда правило зоны: **стендовый дашборд не расширяют руками в интерфейсе.**
|
||
Правка живет до следующей инициализации, а потом молча заменяется тем, что
|
||
лежит в git. Пробовать интерфейс менти никто не мешает — но все, что должно
|
||
пережить перезапуск, приезжает файлом.
|
||
|
||
Это правило относится только к стендовым объектам. Свои датасеты, графики и
|
||
дашборды менти живут в той же базе метаданных Superset, а импорт трогает
|
||
только объекты с известными ему uuid. Поэтому свои объекты переживают
|
||
`make up` и `make rebuild-storage`. Их стирает только `make clean` или
|
||
адресный сброс базы метаданных по рецепту ниже.
|
||
|
||
## Как снять свежий экспорт
|
||
|
||
Маршрут один: правишь на живом стенде, забираешь экспорт, кладешь дерево в git.
|
||
|
||
1. Собрать или поправить график и дашборд в интерфейсе — на стенде, а не в
|
||
файлах. Ключи `params` графиков велики, недокументированы и меняются от
|
||
версии к версии; неверный ключ не ошибается, а рисует пустоту. Интерфейс
|
||
здесь единственный надежный оракул.
|
||
2. Снять экспорт и достать его из контейнера:
|
||
|
||
```
|
||
docker compose exec superset superset export-dashboards -f /tmp/export.zip
|
||
docker compose cp superset:/tmp/export.zip /tmp/export.zip
|
||
unzip -d /tmp/superset-export /tmp/export.zip
|
||
```
|
||
|
||
Внутри архива — один каталог `dashboard_export_<дата>` ровно той же формы,
|
||
что и дерево импорта. Команда выгружает все дашборды сразу; на стенде он
|
||
один.
|
||
3. Вычистить экспортный сор перед коммитом:
|
||
- выбросить экспортный `databases/` — наш `clickhouse.yaml` остается как
|
||
есть, с постоянным uuid и без пароля;
|
||
- оставить в `metadata.yaml` одну строку `version: 1.0.0`;
|
||
- убрать из графиков `params.datasource`, `params.slice_id` и
|
||
`params.dashboards` — это идентификаторы строк того Superset, где
|
||
снимали, а импорт выводит связи из uuid;
|
||
- убрать `query_context` — снимок запроса оттуда же. Импорт его не
|
||
требует и обратно не заполняет: после переимпорта он остается пустым, на
|
||
отрисовку это не влияет;
|
||
- перезаписать русские строки читаемыми. Экспорт кодирует их
|
||
`\uXXXX`-последовательностями, и после каждого круга их приходится
|
||
раскодировать заново: имена графиков и подписи колонок читает менти, и в
|
||
файле, и в диффе. Годится любой повторный дамп YAML с
|
||
`allow_unicode=True`;
|
||
- вернуть постоянные uuid из схемы выше.
|
||
4. Скопировать вычищенное дерево в `infra/superset/import/` и проверить
|
||
импортом **на пустых метаданных** — иначе прогон проверит только обновление
|
||
уже стоящих объектов, а молчаливый пропуск нового спрячется за старым.
|
||
Активы Superset лежат в базе `superset` службы `postgres-metadata`, поэтому
|
||
адресный сброс — эта база:
|
||
|
||
```
|
||
docker compose stop superset
|
||
docker compose exec postgres-metadata sh -c \
|
||
'psql -U "$POSTGRES_USER" -d postgres \
|
||
-c "DROP DATABASE superset WITH (FORCE)" \
|
||
-c "CREATE DATABASE superset OWNER \"$SUPERSET_METADATA_USER\""'
|
||
docker compose run --rm superset-init
|
||
docker compose up -d superset
|
||
```
|
||
|
||
Базу приходится создавать тут же: скрипт `infra/postgres/init/` заводит ее
|
||
один раз, при первой инициализации тома Postgres. `make rebuild-storage`
|
||
для этой проверки не годится — он гоняет `world_recreate` в ClickHouse и
|
||
метаданных Superset не касается. `make clean` сбросит и их, но заодно снесет
|
||
весь стенд, включая метаданные Airflow в той же службе.
|
||
|
||
После импорта дашборд смотрят глазами: это единственное доказательство —
|
||
см. ловушку ниже.
|
||
|
||
## Ловушки
|
||
|
||
**Импорт молчит.** График, чей `dataset_uuid` не разрешился, отбрасывается без
|
||
единого слова; дашборд, у которого не сошлись ссылки на графики,
|
||
пропускается. Ошибки уходят в журнал, и `init.sh`
|
||
завершается нулем в любом случае. Поэтому импорт проверяют глазами или
|
||
запросом к API — но не тем, что команда отработала.
|
||
|
||
**Знак рубля приходит не из формата валюты.** Символ Superset берет у
|
||
`Intl.NumberFormat` по коду валюты и английской локали стенда: код `RUB`
|
||
печатается как «RUB», а знак «₽» вызов роняет, и форматирование молча
|
||
откатывается к числу без валюты. Поэтому единица стоит в названии панели —
|
||
«Выручка, ₽», — а метрика несет обычный формат `,d`.
|
||
|
||
**`label_colors` знает имя ряда, а не подпись.** Для разреза имя ряда — это
|
||
значение колонки («Бытовая техника»), для графика без разреза — имя метрики
|
||
(`known_share`), а не ее русская подпись. Цвет, закрепленный за подписью
|
||
«Доля известных», не применяется.
|
||
|
||
**`--overwrite` синхронизирует датасет разрушительно.** Колонки и метрики
|
||
берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки
|
||
исчезнет при следующем старте контейнера. Правка датасета — это правка файла.
|
||
|
||
## Что проверяет стенд
|
||
|
||
`make check-services` входит в Superset, находит подключение `ClickHouse`,
|
||
сверяет его uuid и безопасный URI и требует схему `dm` — то есть подтверждает
|
||
живое подключение и хранилище пароля. Сами графики и дашборд ни одна цель не
|
||
проверяет: их смотрят глазами после переинициализации. Полная карта целей — в
|
||
[доке проверок](testing.md).
|
||
|
||
## Отклоненные варианты
|
||
|
||
Оживить дашборд предложат снова, поэтому отказы записаны с доводами. Замерено
|
||
на живом стенде 7 сентября 2026 года, Superset 6.1.
|
||
|
||
**Карта регионов России.** Замысел стоял на клике: щелчок по региону
|
||
перестраивает весь экран. Плагин Country Map кросс-фильтр не отдает вовсе —
|
||
`behaviors` не объявлены, `setDataMask` не вызывается, а щелчок приближает
|
||
карту. Без клика остается картинка, и она рассказывает меньше, чем кажется: 16
|
||
городов стенда сворачиваются в 14 регионов из 83, и больше половины сессий
|
||
приходится на один. Карта отдана факультативу «Добавь названия регионов в
|
||
отчет»: там читатель собирает справочник сам, и она становится наградой за
|
||
него.
|
||
|
||
**Регион как общее измерение витрин.** Чтобы фильтр по региону действовал на
|
||
весь экран, регион пришлось бы завести в каждой витрине: Superset датасеты
|
||
между собой не соединяет, а фильтр по колонке, которой нет в датасете панели,
|
||
отбрасывается с причиной `COL_NOT_IN_DATASOURCE`. У заказа географии нет — она
|
||
бралась бы от человека через карту идентичностей и приписывалась бы сущности, у
|
||
которой ее в источнике не было. Вдобавок не сходятся ритмы: витрина от карты
|
||
идентичностей не замерзает никогда, а выручка живет окном последнего слепка
|
||
([дока DM](dm.md)). Стендовый словарь регионов отклонен раньше и по своей
|
||
причине — он повторил бы урок `dictGet` каталога вторым разом.
|
||
|
||
**Кросс-фильтрация между панелями.** Общих измерений у витрин почти нет:
|
||
категория есть только у выручки, а день у сверки называется `order_day` против
|
||
`report_date` у остальных. Клик перестроил бы часть экрана и оставил другую
|
||
нетронутой, а вывод из этого читатель делает про данные, а не про устройство
|
||
витрин. Панель фильтров этой болезнью не болеет — там колонка задается явно для
|
||
каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация
|
||
нет.
|
||
|
||
**Вкладки и блоки «что смотреть».** Вкладки были условным вариантом: их
|
||
заводят, когда экран перестает читаться одним взглядом. Четыре строки панелей
|
||
читаются сразу, поэтому вкладки спрятали бы половину экрана за щелчком без
|
||
выигрыша. Блок с текстом «что смотреть» рядом с панелью означал
|
||
бы, что панель сама себя не объясняет: тогда чинить надо название и подписи, а
|
||
не дописывать к ним пояснение.
|
||
|
||
**Пороги и раскраска панелей.** Правило
|
||
[спеки](../specs/2026-07-30-stand-v2-realism.md), раздел 9: на дашборд смотрят
|
||
глазами, красное и зеленое живет в проверках.
|
||
|
||
Поводы вернуться к разговору: плагин карты начинает отдавать кросс-фильтр; у
|
||
заказа появляется адрес доставки; витрины получают общее измерение по своей
|
||
причине, не ради дашборда.
|