125 lines
8.6 KiB
Markdown
125 lines
8.6 KiB
Markdown
# События аналитики (ClickHouse)
|
||
|
||
Бот пишет все значимые действия в ClickHouse — единую широкую таблицу
|
||
`analytics.events`. Дашборд (Superset) и ETL — отдельный финальный этап; здесь
|
||
описан **контракт** событий: какие бывают `event_type` и что лежит в `payload`.
|
||
|
||
> Источник правды в коде — `common/analytics/events.py` (`EventType`).
|
||
> Расхождение со списком ниже ловит `tests/test_analytics.py`.
|
||
> Добавляешь событие — добавь строку в таблицу.
|
||
|
||
## Схема в ClickHouse
|
||
|
||
DDL — `clickhouse/init/001_events.sql` (применяется при первой инициализации контейнера).
|
||
|
||
- `analytics.events` — сырые события, `MergeTree`, `PARTITION BY toYYYYMMDD(event_time)`,
|
||
`ORDER BY (event_time, event_type, user_id)`. Партиционирование по дню выбрано специально:
|
||
сырые партиции ежедневно удаляются после бэкапа (дешёвый `DROP PARTITION`, без mutation).
|
||
- `analytics.events_daily` — агрегат (`SummingMergeTree`): `day`, `event_type`, `user_id`,
|
||
`chat_id`, `events`. Строится материализованным представлением `events_daily_mv`.
|
||
- `analytics.money_daily` — агрегат по деньгам: `day`, `event_type`, `user_id`,
|
||
`amount_sum`, `events` (из `payload.amount` событий `money_accrual`/`money_spend`).
|
||
|
||
Агрегаты остаются на VPS навсегда, поэтому активность, топ команд и денежные потоки
|
||
доступны и после удаления сырых событий. Запросы к агрегатам суммируют `events`/`amount_sum`
|
||
(фон слияний `SummingMergeTree` не гарантирует полную свёртку в любой момент).
|
||
|
||
## Хранение и бэкап
|
||
|
||
| Что | Где | Сколько |
|
||
| --- | --- | --- |
|
||
| Сырые события | VPS, `analytics.events` | последние `BACKUP_GRACE_DAYS` дней (по умолчанию 7) |
|
||
| Агрегаты | VPS | постоянно |
|
||
| Полная история | домашний сервер, свой ClickHouse | архивы `events_YYYY-MM-DD.parquet.gz` (ежедневно) |
|
||
|
||
Каждую ночь (по Москве) `scripts/clickhouse_backup.sh` выгружает вчерашнюю партицию,
|
||
передаёт её на домашний сервер, импортирует и **сверяет количество строк**; только после
|
||
успешной сверки на VPS удаляются партиции старше грейса. Ошибка бэкапа данные не удаляет.
|
||
Подробности — `DEPLOY.md`.
|
||
|
||
## Уведомления в чат логов
|
||
|
||
Помимо записи в ClickHouse часть событий дублируется сообщением в `ALERT_CHAT_ID`
|
||
(если он задан; `0` — выключено). Отправка не критична: ошибки только логируются.
|
||
|
||
| Уведомление | Когда | Событие в ClickHouse |
|
||
| --- | --- | --- |
|
||
| Вход участника | кто-то вошёл в основной чат | `join` |
|
||
| Выход участника | кто-то вышел из основного чата | `leave` |
|
||
| Очищение | успешное `/очищение` (`+опыт`, `+монеты`) | `purification` |
|
||
| Пинг «бот живой» | раз в час | `bot_alive` |
|
||
| Попытка доступа извне | не участник чата стучится в бота | `access_denied` |
|
||
|
||
## Поля события
|
||
|
||
| Поле | Тип | Описание |
|
||
| --- | --- | --- |
|
||
| **event_time** | `DateTime('UTC')` | Момент события (UTC, без таймзоны) |
|
||
| **event_type** | `LowCardinality(String)` | Тип события (см. каталог ниже) |
|
||
| **user_id** | `Int64` | Telegram-ID пользователя (0 — системное событие) |
|
||
| **username** | `String` | `@username` на момент события (может быть пустым) |
|
||
| **chat_id** | `Int64` | ID чата (обычно основной чат клана) |
|
||
| **payload** | `String` | Дополнительные данные события в виде JSON |
|
||
| **bot_version** | `String` | Версия бота из `common/version.py` |
|
||
| **prod** | `UInt8` | 1 — прод-контур, 0 — тестовый |
|
||
|
||
## Каталог событий
|
||
|
||
| Тип события | Когда пишется | `payload` |
|
||
| --- | --- | --- |
|
||
| `message` | Любое сообщение в основном чате | `message_id`, `chat_type`, `content_type`, `text_len`, `is_command` |
|
||
| `command` | Использование команды (`/инфо`, `/очищение`, …) | `command`, `args_count` |
|
||
| `purification` | Ежедневное очищение (`/очищение`) | `success`, `exp_add`, `money_add` |
|
||
| `exp_accrual` | Начисление опыта за очищение | `amount`, `streak` |
|
||
| `level_up` | Повышение уровня по итогам очищения | `level_from`, `level_to` |
|
||
| `money_accrual` | Начисление валюты (монетка за активность, награда за очищение) | `amount`, `reason`, `action_id` |
|
||
| `money_spend` | Трата валюты (покупка, обнуление баланса при удалении) | `amount`, `reason`, `product_id` |
|
||
| `eggs` | Получение яичек (`/яички`) | `amount` |
|
||
| `purchase_title` | Покупка звания | `title_id`, `price`, `success` |
|
||
| `purchase_donat` | Покупка доната | `donat_id`, `price`, `success` |
|
||
| `join` | Вход участника в основной чат | `chat_title` |
|
||
| `leave` | Выход участника из основного чата | `chat_title` |
|
||
| `nick_set` | Смена ника (`/ник`) | `nick` |
|
||
| `subscribe` | Подписка на рассылки (`/подписка`) | — |
|
||
| `unsubscribe` | Отписка от рассылок (`/отписка`) | — |
|
||
| `access_denied` | Отказ в доступе (не участник чата и не админ) | `reason` |
|
||
| `bot_start` | Старт бота | `app_env` |
|
||
| `bot_stop` | Остановка бота | `app_env`, `uptime_seconds` |
|
||
| `bot_alive` | Ежечасный пинг «бот живой» | `app_env`, `uptime_seconds` |
|
||
| `error` | Необработанная ошибка при обработке апдейта | `error_type`, `error`, `update_type` |
|
||
|
||
## Поведение при недоступности ClickHouse
|
||
|
||
Аналитика — **некритичная** подсистема: её сбой не должен влиять на бота.
|
||
|
||
- события копятся в буфере и отправляются батчами (по размеру и по таймеру);
|
||
- если ClickHouse недоступен, буфер не растёт бесконечно: при переполнении самые
|
||
старые записи отбрасываются со счётчиком в логе;
|
||
- если записать батч не удалось, события выводятся в stdout (`WARNING`, первые
|
||
несколько строк + общее количество) и отбрасываются — данные не пропадают молча;
|
||
- при остановке бота выполняется финальный сброс буфера;
|
||
- выключение целиком — `CLICKHOUSE_ENABLED=false` (тогда `record()` — no-op).
|
||
|
||
## Примеры запросов
|
||
|
||
```sql
|
||
-- активность по дням
|
||
SELECT toDate(event_time) AS day, count() FROM analytics.events GROUP BY day ORDER BY day;
|
||
|
||
-- топ команд за неделю
|
||
SELECT JSONExtractString(payload, 'command') AS cmd, count() AS uses
|
||
FROM analytics.events
|
||
WHERE event_type = 'command' AND event_time >= now() - INTERVAL 7 DAY
|
||
GROUP BY cmd ORDER BY uses DESC;
|
||
|
||
-- покупки за месяц
|
||
SELECT event_type, count(), sum(JSONExtractInt(payload, 'price'))
|
||
FROM analytics.events
|
||
WHERE event_type IN ('purchase_title', 'purchase_donat') AND event_time >= now() - INTERVAL 30 DAY
|
||
GROUP BY event_type;
|
||
|
||
-- ошибки
|
||
SELECT event_time, JSONExtractString(payload, 'error_type'), JSONExtractString(payload, 'error')
|
||
FROM analytics.events WHERE event_type = 'error' ORDER BY event_time DESC LIMIT 50;
|
||
```
|