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_id null. 
 
 
 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 / MYSQL only). 
 
 
```