# События аналитики (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`. ## Хранение | Что | Где | Сколько | | --- | --- | --- | | События | 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; ```