Colombino Analytics — ТЗ
Проект Plane: пока CREATIVE (отдельный проект аналитики не заведён). Источник: обсуждение 25.08.2026 (retention, ключевые действия, история плотов, Plan). Документ — контракт v1, не экран в Plan.
Плагин-шина ключевых действий игроков. Другие плагины вызывают API, события пишутся в свою PostgreSQL. Plan (stats.binomc.net), DataExtension и отчёт воронки в v1 нет.
0) Техническая информация
-
Название:
ColombinoAnalytics -
Платформа: Paper (Hub, Creative, ивентовые) + Velocity Master
-
Сборка: один репозиторий, три модуля:
-
analytics-api— тонкий jar,compileOnlyу других плагинов -
analytics-paper -
analytics-velocity
-
-
БД: отдельная database PostgreSQL (тот же хост, что Plane/Bookstack, не их схемы и не БД Plan)
-
Soft-depend (Paper): PlotSquared, BinoRankup, PlotMark — нет плагина, нет слушателя, API живёт
1) Зачем
Три цели на одном логе. Creative — UGC: активация не «зашёл», а взял слот. Ивентовые режимы (Build Battle и др.) в логе не отдельные продукты, а значение server у захода.
| Цель | Что даёт лог | Когда на экране |
|---|---|---|
| Шина | API для других плагинов | v1 |
| Модерация | История владения плотом | данные в v1, UI позже |
| Воронка новичка | Где обрыв до активации и ранга | данные в v1, отчёт позже |
| Staff | Ревью, staff-rate, марки PlotMark | данные в v1, UI позже |
2) Готово когда (v1)
-
Jar
analytics-api: другой плагин вызываетtrack, строка появляется в Postgres. Если analytics не установлен — вызов no-op, не падает. -
Velocity Master пишет заходы с каноническим именем сервера.
-
Paper Creative пишет плоты, заявки ранга, staff-ревью/рейтинг, марки PlotMark.
-
Paper стоит на Hub и ивентовых бэкендах (API и будущие типы; заходы в v1 пишет прокси).
Не в приёмке v1: DataExtension и страницы Plan, дашборд retention, BlockPlace, плагин на Shadow, TTL/чистка лога, Redis.
3) Термины
| Термин | Смысл |
|---|---|
| Network-сессия | Пока игрок в сети (Velocity), не «пока на этом Paper» |
Канон server |
Имя сервера как в Velocity: hub, creative, имя ивентового |
| Активация | plot.claimed — взял слот, не заход на Creative |
| DataExtension | API Plan для снимка на сайте. Лог не пишет. В v1 не используем |
| Query API Plan | SQL в БД Plan. Типы движка: SQLite или MySQL. Postgres через Plan нет. В v1 не используем |
4) Архитектура
Почему не таблица в БД Plan: DataExtension не пишет события (4 колонки на игрока, 50 символов в ячейке). Query API — SQLite/MySQL. Заходы со всех серверов не кладём в боевую БД Plan: патчи, /plan db clear и move не знают чужие таблицы; воронки будут конкурировать с вебом.
Поток: Velocity Master и Paper пишут в одну PostgreSQL player_events. Прокси шлёт на Paper plugin message канала colombino:analytics (session_id + канон server). Плот, ранг и марки прокси не слушает.
5) Воронка новичка (как смотрит аналитик)
-
Зашёл в сеть → заход с
server=hub -
Попал на Creative → заход с
server=creative -
Взял плот →
plot.claimed← активация -
Подал
/done→rank.request_submitted(/rankup— только меню, не этот шаг) -
Заявка закрылась →
rank.request_status_changed(APPROVED/REJECTED/WITHDRAWN/DELETED) -
Получил ранг →
rank.granted
Дыра между 3 и 4: PlotSquared не видит «начал строить». Без BlockPlace на своём плоте не отличить «взял и бросил» от «строит третий день». В v1 дыру помечаем, не закрываем.
«Поучаствовал в Build Battle» = заход на ивентовый сервер. Отдельного типа build_battle.joined нет.
6) Каталог событий v1 (закрыт)
Строка: player_uuid, event_type, occurred_at, server, session_id, extra (JSONB строго по типу). Неизвестный тип API отвергает. Свободных строк типов нет.
6.1 Заходы (пишет только Velocity)
Hub → Creative → Hub → Creative в одном визите: по одному session_joined на hub и на creative. Возврат на Creative в той же сессии не пишем.
| Тип | Смысл | Extra |
|---|---|---|
player.first_joined_server |
Первый раз в жизни на этом server |
server |
player.session_joined_server |
Первый раз на этом server в network-сессии |
server |
6.2 Плоты (PlotSquared, Creative)
Активация и история владения. PlayerEnterPlotEvent не пишем. Trusted/members в v1 нет. plot_id — идентификатор из PlotSquared API. width — ширина в блоках.
| Тип | Источник | Extra |
|---|---|---|
plot.claimed |
PlotClaimedNotifyEvent (wasAuto() → method: auto | claim) |
plot_id, world, width, method |
plot.unclaimed |
unclaim | plot_id, world, width |
plot.deleted |
delete | plot_id, world, width |
plot.cleared |
clear | plot_id, world, width |
plot.transferred |
PlotChangeOwnerEvent |
то же + from_uuid, to_uuid |
6.3 Ранг — новичок (BinoRankup)
Слушаем Bukkit API плагина, хуки не выдумываем. categories — как отдаёт Rankup (JSON-массив строк). Имена классов сверить с репозиторием при реализации; смысл событий не менять.
| Тип | Слушаем | Extra |
|---|---|---|
rank.request_submitted |
RequestSubmitEvent |
request_id, from_rank, to_rank, plot_id, world, width, categories |
rank.request_status_changed |
RequestStatusChangedEvent |
request_id, from_status, to_status |
rank.granted |
PlayerRankGrantedEvent |
from_rank, to_rank, request_id |
6.4 Ранг — staff (основная метрика команды)
| Тип | Слушаем | Extra |
|---|---|---|
rank.review_submitted |
ReviewSubmitEvent |
request_id, vote (APPROVE/REJECT), voter_role как в конфиге Rankup, plot_id, world |
rank.rating_submitted |
RatingSubmitEvent только если у голосующего роль Helper / Mentor / Senior Mentor |
request_id, plot_id, world |
Review без voter_role не пишем: вес голоса в Rankup от роли. Обычный игроковый /rate в лог не идёт (отсекаем на входе).
6.5 Марки (PlotMark, Creative)
См. Метки на плотах — ТЗ. PlotMark вызывает Analytics.track или отдаёт Bukkit-событие с тем же extra.
| Тип | Extra |
|---|---|
staff.mark_placed |
mark_type: pointer / edit / rework; plot_id, world |
staff.mark_resolved |
то же + resolved_by: player | staff |
7) Хранение
Таблица player_events, append-only.
| Колонка | Тип | Назначение |
|---|---|---|
id |
bigserial |
PK |
occurred_at |
timestamptz |
время события, не вставки |
received_at |
timestamptz |
время записи в Postgres |
player_uuid |
uuid |
игрок |
event_type |
text |
только каталог §6 |
server |
text |
канон Velocity |
session_id |
uuid nullable |
network-сессия; null если прокси не доставил |
extra |
jsonb |
контракт типа |
Индексы: (player_uuid, occurred_at DESC); (event_type, occurred_at); (server, event_type, occurred_at); expression (extra->>'plot_id'), (extra->>'request_id'). GIN по всему extra в v1 нет.
Уникальность: first_joined — (player_uuid, event_type, server). session_joined — (session_id, event_type, server) WHERE session_id IS NOT NULL (без WHERE в Postgres несколько NULL не дедупятся). Повтор — ON CONFLICT DO NOTHING. Плот/ранг/марки не дедупим.
TTL сырого лога в v1 нет. Политика хранения — отдельное решение по факту объёма.
8) Кто пишет что
8.1 Velocity Master
-
Выдаёт
session_idна время пребывания в сети. -
Пишет два типа заходов (§6.1). First: кэш + уникальный индекс (гонка → conflict ignore).
-
После коннекта/switch на Paper — plugin message с
session_idи канономserver.
8.2 Paper
-
Не считает first/session.
track()дописывает текущиеsession_idиserver. -
Нет сообщения с прокси — событие пишем,
session_id = null. Фактplot.claimedважнее склейки с сессией. -
Состояние: карта UUID → сессия до quit. Тот же
session_idпри каждом switch. -
Гонка: событие раньше plugin message — допустимо,
session_idnull.
Shadow: в v1 не ставим. Иначе два источника сессий. Игрок через Shadow = как без прокси.
Не в v1: Redis, слушатели плотов на прокси.
9) API
boolean track(UUID player, String eventType, Map<String, Object> extra)
-
Нет paper/velocity плагина на сервере → no-op,
false. -
Тип не из каталога или нет обязательных ключей extra →
false, warn в лог, в очередь не кладём. -
Успешный enqueue →
true(ещё не значит «уже в Postgres»).
Игровой поток только enqueue. Фоновый writer — batch insert. Главный поток JDBC не ждёт.
10) Ошибки
| Ситуация | Поведение |
|---|---|
| Postgres недоступен при старте | Плагин включается; track enqueue; периодический warn |
| Postgres лёг в рантайме | Очередь в памяти, потолок 10 000. Overflow: drop + счётчик. Игрок не лагает |
| После подъёма БД | Flush очереди |
| Failover на диск/SQLite | Нет в v1 (два источника правды) |
| Битый plugin message | Игнор |
| Неверный extra / тип | Отказ до очереди |
11) Тесты (CI без живого MC)
-
Контракт extra: полный набор — ok; дырявый — reject.
-
Дедуп
first_joinedиsession_joined. -
rank.rating_submitted: Helper/Mentor/Senior Mentor — пишем; игрок без роли — нет. -
rank.review_submittedбезvoter_role— нет. -
Запрос-заготовка воронки: цепочка типов по uuid (не UI).
12) Следующие этапы (не v1)
-
DataExtension Plan: счётчики, first/last, урезанная история (лимиты Plan: 4 колонки, 50 символов).
-
Отчёт воронки / retention по когортам (SQL или отдельный сервис).
-
Закрытие дыры «взял плот, не строит».
-
Velocity Shadow, если станет основным входом.
-
TTL/партиции, когда будет ясен объём.
13) Сознательно отвергнутое
-
Хранение в БД Plan через Query API.
-
DataExtension как «запись в БД» — он не пишет лог.
-
build_battle.joined— дубль захода на ивентовыйserver. -
PlayerEnterPlotEvent— ходьба, не ключ. -
Каждый Paper join без дедупа по network-сессии.
-
PostgreSQL через Plan API — не поддерживается (
SQLITE/MYSQLonly).