Skip to main content

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)

  1. Jar analytics-api: другой плагин вызывает track, строка появляется в Postgres. Если analytics не установлен — вызов no-op, не падает.

  2. Velocity Master пишет заходы с каноническим именем сервера.

  3. Paper Creative пишет плоты, заявки ранга, staff-ревью/рейтинг, марки PlotMark.

  4. 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) Воронка новичка (как смотрит аналитик)

  1. Зашёл в сеть → заход с server=hub

  2. Попал на Creative → заход с server=creative

  3. Взял плот → plot.claimedактивация

  4. Подал /donerank.request_submitted (/rankup — только меню, не этот шаг)

  5. Заявка закрылась → rank.request_status_changed (APPROVED / REJECTED / WITHDRAWN / DELETED)

  6. Получил ранг → 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)

  1. DataExtension Plan: счётчики, first/last, урезанная история (лимиты Plan: 4 колонки, 50 символов).

  2. Отчёт воронки / retention по когортам (SQL или отдельный сервис).

  3. Закрытие дыры «взял плот, не строит».

  4. Velocity Shadow, если станет основным входом.

  5. TTL/партиции, когда будет ясен объём.


13) Сознательно отвергнутое

  • Хранение в БД Plan через Query API.

  • DataExtension как «запись в БД» — он не пишет лог.

  • build_battle.joined — дубль захода на ивентовый server.

  • PlayerEnterPlotEvent — ходьба, не ключ.

  • Каждый Paper join без дедупа по network-сессии.

  • PostgreSQL через Plan API — не поддерживается (SQLITE / MYSQL only).

```