128 lines
7.9 KiB
Markdown
128 lines
7.9 KiB
Markdown
# События аналитики (Postgres)
|
||
|
||
Бот пишет значимые действия в Postgres, схема `analytics`. Дашборд (Superset) и ETL —
|
||
отдельный финальный этап; здесь описан **контракт** событий: какие бывают `event_type`
|
||
и что лежит в `payload`.
|
||
|
||
> Источник правды в коде — `common/analytics/events.py` (`EventType`).
|
||
> Расхождение со списком ниже ловит `tests/test_analytics.py`.
|
||
> Добавляешь событие — добавь строку в таблицу.
|
||
|
||
## Схема в Postgres
|
||
|
||
DDL — `sql/V26__analytics_schema.sql`.
|
||
|
||
- `analytics.events` — события, `PARTITION BY RANGE (event_time)` по дням.
|
||
Партиции создаёт приложение (бот и админка) на несколько дней вперёд;
|
||
**старые партиции не удаляются** — история хранится бессрочно.
|
||
- `analytics.messages_daily` — количество сообщений пользователя по дням:
|
||
`day`, `user_id`, `username`, `chat_id`, `count`; PK `(day, user_id, chat_id)`.
|
||
На каждое сообщение выполняется `INSERT ... ON CONFLICT DO UPDATE count + 1`,
|
||
поэтому вместо строки на каждое сообщение растёт только счётчик за день.
|
||
|
||
Сообщения **не** пишутся в `analytics.events` отдельным событием `message`.
|
||
Построчная таблица `bot.messages` удалена миграцией `V27`: старые строки один раз
|
||
пересчитаны в `analytics.messages_daily`, новые пишутся только счётчиком.
|
||
|
||
## Хранение
|
||
|
||
| Что | Где | Сколько |
|
||
| --- | --- | --- |
|
||
| События | Postgres, `analytics.events` | бессрочно (партиции по дням) |
|
||
| Счётчики сообщений | Postgres, `analytics.messages_daily` | бессрочно (партиции по дням) |
|
||
|
||
Бэкап — общий бэкап Postgres. Отдельного пайплайна ClickHouse/Parquet больше нет.
|
||
При росте базы нужно следить за диском: автоматической очистки нет.
|
||
|
||
## Уведомления в чат логов
|
||
|
||
Помимо записи в Postgres часть событий дублируется сообщением в `ALERT_CHAT_ID`
|
||
(если он задан; `0` — выключено). Отправка не критична: ошибки только логируются.
|
||
|
||
| Уведомление | Когда | Событие |
|
||
| --- | --- | --- |
|
||
| Вход участника | кто-то вошёл в основной чат | `join` |
|
||
| Выход участника | кто-то вышел из основного чата | `leave` |
|
||
| Очищение | успешное `/очищение` (`+опыт`, `+монеты`) | `purification` |
|
||
| Пинг «бот живой» | раз в час | `bot_alive` |
|
||
| Попытка доступа извне | не участник чата стучится в бота | `access_denied` |
|
||
|
||
## Поля события
|
||
|
||
| Поле | Тип | Описание |
|
||
| --- | --- | --- |
|
||
| **event_time** | `timestamptz` | Момент события (UTC) |
|
||
| **event_type** | `text` | Тип события (см. каталог ниже) |
|
||
| **user_id** | `bigint` | Telegram-ID пользователя (0 — системное событие) |
|
||
| **username** | `text` | `@username` на момент события (может быть пустым) |
|
||
| **chat_id** | `bigint` | ID чата (обычно основной чат клана) |
|
||
| **payload** | `jsonb` | Дополнительные данные события |
|
||
| **bot_version** | `text` | Версия бота из `common/version.py` |
|
||
| **prod** | `boolean` | `true` — прод-контур, `false` — тестовый |
|
||
|
||
## Каталог событий
|
||
|
||
| Тип события | Когда пишется | `payload` |
|
||
| --- | --- | --- |
|
||
| `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`, `kicked`, `by_id`, `by_username` |
|
||
| `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` |
|
||
|
||
## Поведение при недоступности Postgres
|
||
|
||
Аналитика — **некритичная** подсистема: её сбой не должен влиять на бота.
|
||
|
||
- события и счётчики копятся в буфере и записываются батчами (по размеру и по таймеру);
|
||
- если Postgres недоступен, буфер не растёт бесконечно: при переполнении самые
|
||
старые события отбрасываются;
|
||
- если записать батч не удалось, он выводится в stdout (`WARNING`, первые несколько
|
||
строк + общее количество) и отбрасывается — данные не пропадают молча;
|
||
- при остановке бота выполняется финальный сброс буфера;
|
||
- выключение целиком — `ANALYTICS_ENABLED=false` (тогда `record()`/`record_message()` — no-op).
|
||
|
||
## Примеры запросов
|
||
|
||
```sql
|
||
-- события по дням
|
||
SELECT date_trunc('day', event_time) AS day, count(*)
|
||
FROM analytics.events GROUP BY day ORDER BY day;
|
||
|
||
-- топ команд за неделю
|
||
SELECT payload->>'command' AS cmd, count(*) AS uses
|
||
FROM analytics.events
|
||
WHERE event_type = 'command' AND event_time >= now() - interval '7 days'
|
||
GROUP BY cmd ORDER BY uses DESC;
|
||
|
||
-- покупки за месяц
|
||
SELECT event_type, count(*), sum((payload->>'price')::int)
|
||
FROM analytics.events
|
||
WHERE event_type IN ('purchase_title', 'purchase_donat')
|
||
AND event_time >= now() - interval '30 days'
|
||
GROUP BY event_type;
|
||
|
||
-- сообщения по дням
|
||
SELECT day, sum(count) FROM analytics.messages_daily
|
||
WHERE day >= current_date - 30 GROUP BY day ORDER BY day;
|
||
|
||
-- ошибки
|
||
SELECT event_time, payload->>'error_type', payload->>'error'
|
||
FROM analytics.events WHERE event_type = 'error'
|
||
ORDER BY event_time DESC LIMIT 50;
|
||
```
|