# GameStats Protocol v1 — сбор анонимной статистики из игры

Документ для разработчика (или ИИ-ассистента), который подключает сбор статистики к новой игре/приложению.
Сервер: `GameStats.Server` (папка `GameStats` рядом с играми). Протокол универсален: **обязательно только `app` и `events`**,
всё остальное передаётся, если игра это умеет. Сервер сам разберёт, что пришло, и покажет в дашборде.

---

## 1. Что нужно сделать (краткий чек-лист)

1. Получить у владельца: **`APP_ID`** (латиница, например `yarnpull`), **адрес сервера** (`ENDPOINT`), при необходимости **`API_KEY`**.
2. Сделать в игре небольшой модуль статистики с функцией `Track(name, props, value)`.
3. Копить события **в очереди на диске**, отправлять **пачками** в фоне (см. §5). Не слать каждое событие мгновенно.
4. Нет интернета → ничего не делать, не показывать ошибок, хранить очередь и ждать.
5. Не собирать идентификаторы устройства, рекламные id, имя/почту, точные координаты. Только анонимный `install` (см. §3).
6. Подключить события из §7 (какие есть в игре) и свои, строго по правилам именования (§6).
7. Отразить сбор в политике конфиденциальности (§9).

---

## 2. Транспорт

```
POST {ENDPOINT}/api/v1/ingest
Content-Type: application/json
X-Api-Key: <API_KEY>              (только если ключ выдан)
Content-Encoding: gzip            (необязательно; тело можно сжимать)
```

Пример `ENDPOINT`: `https://stats.example.com`. Тело — один JSON-объект («пачка»).

### Ответы

| Код | Значение | Что делать клиенту |
|-----|----------|--------------------|
| 200 | `{"ok":true,"accepted":N}` пачка принята | удалить отправленные события из очереди |
| 400 | неверный JSON/поля (`{"ok":false,"error":"…"}`) | **удалить пачку** (повтор не поможет), при отладке записать в лог |
| 401 | неверный/нет `X-Api-Key` | не удалять; повторить позже (ключ могли ещё не выдать) |
| 404 | приложение не зарегистрировано | не удалять; повторить позже |
| 413 | пачка больше лимита (512 КБ) | разбить пополам или уменьшить размер пачки |
| 429 | слишком часто | увеличить паузу (backoff) |
| 5xx / нет сети / таймаут | сервер недоступен | не удалять, повторить с нарастающей паузой |

Повторная отправка той же пачки безопасна: сервер отбрасывает дубли. Лимиты: тело ≤ 512 КБ, ≤ 500 событий в пачке (рекомендуется ≤ 100).

---

## 3. Формат пачки

```json
{
  "v": 1,
  "app": "yarnpull",
  "ver": "0.1.0",
  "build": "12",
  "platform": "Android",
  "os": "Android 14",
  "lang": "ru",
  "country": "RU",
  "install": "3f2c9a…",
  "session": "a1b2c3d4e5f6",
  "day": 3,
  "sent": 1762500000,
  "env":  { "screen": "1080x2400", "ram_mb": 8192, "quality": "high", "model": "Pixel 7" },
  "user": { "premium": false, "level": 14 },
  "events": [
    { "n": "level_win", "t": 1762499950, "id": "17", "v": 3, "p": { "level": 5, "stars": 3, "sec": 42 } }
  ]
}
```

### Поля верхнего уровня

| Поле | Тип | Обяз. | Описание |
|------|-----|:----:|----------|
| `v` | int | нет | версия протокола, сейчас `1` |
| **`app`** | string | **да** | ID приложения: `a-z 0-9 _ -`, 2–40 символов, строчные |
| `ver` | string | нет | версия игры (`Application.version`) |
| `build` | string | нет | номер сборки |
| `platform` | string | нет | `Android`, `IPhonePlayer`, `WindowsPlayer`, `WebGL`, `Steam`… |
| `os` | string | нет | версия ОС |
| `lang` | string | нет | язык интерфейса/системы (`ru`, `en`) |
| `country` | string | нет | страна ISO-2, если игра её знает (сервер GeoIP **не** делает) |
| `install` | string | желательно | **анонимный id установки**: случайный GUID, создаётся при первом запуске, хранится локально (PlayerPrefs/файл), **не** связан с устройством. Нужен для DAU, удержания, воронок. Без него игроки не различаются |
| `session` | string | желательно | случайный id сессии, новый на каждый запуск (≈12 символов). Нужен для числа сессий и их длины |
| `day` | int | нет | сколько дней прошло с первого запуска (0 в день установки) |
| `sent` | int | желательно | unix-время (сек) **отправки** пачки по часам устройства. По нему сервер исправляет неверные часы игрока |
| `env` | object | нет | техническое окружение (скаляры). Любые ключи: экран, ОЗУ, качество графики, GPU-класс… **Не** кладите сюда серийные номера и рекламные id |
| `user` | object | нет | анонимные свойства игрока (скаляры): уровень, платящий ли, группа A/B теста… |
| **`events`** | array | **да** | список событий |

Значения `env`/`user`: строка, число или bool. Ключи: `A-Za-z0-9_.:-`, до 40 символов, не более ~48 штук суммарно. Сервер хранит **последние** присланные значения для установки и позволяет группировать аудиторию по любому ключу.

### Событие

| Поле | Тип | Обяз. | Описание |
|------|-----|:----:|----------|
| **`n`** | string | **да** | имя события (§6) |
| `t` | int | желательно | unix-время (секунды; миллисекунды тоже понимаются) момента события по часам устройства. Нет `t` → время получения |
| `id` | string | желательно | порядковый номер события внутри сессии (`"1"`, `"2"`…). Нужен, чтобы два одинаковых события в одну секунду не слились как дубль |
| `v` | number | нет | **главное числовое значение** события: сумма покупки, секунды, очки, доход от рекламы. Дашборд суммирует и усредняет `v` |
| `p` | object | нет | параметры события: скаляры (строка/число/bool), до 32 ключей, строки ≤ 200 симв. Вложенные объекты не поддерживаются |

Упрощённый вариант для лёгких клиентов (например, `JsonUtility` в Unity без словарей): `p` можно передать **строкой** `"level=5;stars=3;sec=42"` — сервер разберёт её в объект, числа распознаются автоматически.

**Всё частичное**: любое необязательное поле можно пропустить. Игра без уровней просто не шлёт `level_*`, игра без покупок — `purchase`. Дашборд построит то, что есть.

---

## 4. Приватность и анонимность

- `install` — случайная строка, **не** производная от устройства/аккаунта/рекламного id. Удалили приложение → новый id.
- Не отправлять: IMEI, MAC, GAID/IDFA/IDFV, `SystemInfo.deviceUniqueIdentifier`, e-mail, имя, телефон, IP (сервер IP не сохраняет), координаты.
- Модель устройства (`env.model`) — допустима, но по желанию; для детских приложений лучше не слать.
- Если игра рассчитана на детей (COPPA/Families) — отправляйте минимум: без `country`, без `user`, без модели.

---

## 5. Как отправлять (рекомендуемое поведение клиента)

1. **Очередь на диске** (`persistentDataPath/stats_queue.json` или аналог). Событие записывается в память сразу, на диск — не чаще раза в ~10 с, а также при сворачивании/выходе. Лимит очереди ≈ 1000 событий, при переполнении отбрасывать самые старые.
2. **Пачками**: отправка раз в ~60 с, при сворачивании приложения (`OnApplicationPause`), при выходе и при старте. Размер пачки ≤ 100 событий. Если очередь больше — отправлять остальное следующей пачкой.
3. **Только при наличии сети** (`Application.internetReachability != NotReachable` в Unity; `NWPathMonitor`/`ConnectivityManager` нативно). Нет сети — молча ждать.
4. **Ошибки молча**: никаких всплывающих окон/логов для игрока. При неудаче — пауза удваивается (1 мин → 2 → 4 … максимум 10 мин), после успеха сбрасывается.
5. **Не блокировать игру**: отправка асинхронная, таймаут 15 с.
6. Удалять из очереди **только** те события, что вошли в успешно принятую пачку (200) или в пачку с 400.
7. В каждую пачку класть `sent` = текущее время устройства (unix, секунды).
8. Первое событие после установки — `first_open`. Первое событие каждого запуска — `session_start`. Последнее при выходе/сворачивании — `session_end` (если успеваете; не критично).

Идентификаторы: `install` создаётся один раз и **не** обнуляется при «сбросе прогресса» в игре, иначе статистика посчитает нового игрока.

---

## 6. Именование событий и параметров

- Имя события: `a-z 0-9 _ . : -`, до 64 символов, **snake_case**, глагол в прошедшем времени или существительное: `level_win`, `ad_shown`, `tutorial_step`.
- Имена параметров: тот же алфавит, до 40 символов, snake_case. **Одно и то же значение называйте одинаково во всех играх** (`level`, `sec`, `stars`, `item`, `currency`, `placement`), тогда дашборд даст сводку по всем играм сразу.
- Не используйте в именах события переменные части (`level_5_win`). Переменное — в параметры: `level_win` + `{"level":5}`.
- Не плодите уникальные значения в параметрах (id игрока, текст ошибок целиком, время): их сводка бесполезна. Для «много значений» используйте числа (`level`) — дашборд построит распределение.
- Не более 30–40 разных имён событий на игру.

---

## 7. Стандартные события (каталог)

Используйте эти имена, если в игре есть соответствующая механика. Подчёркнутые параметры — рекомендуемые.

### Жизненный цикл
| Событие | Когда | Параметры / `v` |
|---|---|---|
| `first_open` | первый запуск после установки (один раз) | — |
| `session_start` | запуск или возврат из фона после долгой паузы (> 30 мин) | `level` (текущий прогресс) |
| `session_end` | выход/сворачивание | `sec` = длительность, `v` = `sec` |
| `app_update` | первый запуск после обновления | `from_ver` |

### Прогресс и уровни
| Событие | Когда | Параметры / `v` |
|---|---|---|
| `level_start` | начало уровня/раунда/матча | `level` (число или id), `mode` |
| `level_win` | успешное завершение | `level`, `sec`, `stars`, `score`; `v` = `stars` или `score` |
| `level_fail` | проигрыш | `level`, `sec`, `reason`, `pulled` (прогресс) |
| `level_quit` | вышел, не закончив | `level`, `sec` |
| `progress` | веха прогресса | `stage`, `value` |
| `checkpoint` | контрольная точка | `name` |
| `tutorial_step` | шаг обучения | `step` (число), `name`; завершение — `tutorial_complete` |

### Использование функций
| Событие | Параметры |
|---|---|
| `feature_used` | `feature` (имя функции/инструмента), `level` |
| `hint_used`, `booster_used` | `level`, `kind` |
| `screen_view` | `screen` (menu, shop, pause, settings…) |
| `setting_changed` | `key`, `value` (sound, vibro, lang…) |
| `share`, `rate_prompt`, `rate_done` | `where` |
| `achievement` | `id` |

### Экономика
| Событие | Параметры / `v` |
|---|---|
| `currency_earn` | `currency` (coins, gems), `source` (level, ad, gift), `v` = количество |
| `currency_spend` | `currency`, `sink` (shop, hint), `item`, `v` = количество |
| `shop_open` | `from` |
| `purchase` | реальные деньги: `item`, `currency` (USD/RUB/EUR), `store`; `v` = сумма. Не отправляйте данные карты/чека |
| `purchase_failed`, `purchase_canceled` | `item`, `reason` |

### Реклама (если есть)
| Событие | Параметры / `v` |
|---|---|
| `ad_requested`, `ad_loaded`, `ad_failed` | `placement`, `format` (interstitial/rewarded/banner), `network`, `reason` |
| `ad_shown` | `placement`, `format`, `network`; `v` = оценка дохода за показ, если известна |
| `ad_clicked`, `ad_closed` | `placement` |
| `ad_reward` | `placement`, `reward` |

### Техническое
| Событие | Параметры / `v` |
|---|---|
| `perf` | `fps`, `mem_mb`, `load_ms`, `scene` — снимок раз в сессию или при просадке |
| `load_time` | `scene`, `ms`; `v` = `ms` |
| `error` | `code`, `where` (короткий код/место, **без** стека и персональных данных) |
| `crash` | `code`, `where` — при восстановлении после аварии |
| `net_error` | `code`, `endpoint` |

### Своё
Игровые специфичные события — по правилам §6, например `skin_equipped {"skin":"cat_wizard"}`, `scarf_done {"total":3}`.

---

## 8. Что показывает дашборд (чтобы знать, какие данные ценны)

- **DAU / WAU / MAU, stickiness** — нужны `install` и любые события.
- **Новые установки, удержание D1/D3/D7/D14/D30** — `install` (+ `first_open` для явной метки).
- **Сессии, длина сессии** — `session` и время событий (`t`).
- **События**: топ, динамика по дням, сводка по параметрам, **распределение по числовому параметру** (например `level_fail` по `level` — где игроки застревают).
- **Воронки** по любой цепочке событий (`first_open → tutorial_complete → level_win`).
- **Аудитория**: платформа, версия, язык, страна и любой ключ из `env`/`user`.
- **Живая лента** событий в реальном времени.

---

## 9. Политика конфиденциальности (минимальный текст)

> Приложение собирает анонимную статистику использования (запуски, пройденные уровни, использованные функции, технические параметры устройства — экран, версия ОС, язык) и случайный идентификатор установки, не связанный с устройством или личностью. Данные нужны для улучшения игры и не передаются третьим лицам. Идентификатор удаляется при удалении приложения.

В анкетах магазинов: Google Play → Data safety: *App activity*, *App info and performance*, *Device or other IDs* (случайный id установки), «данные не передаются третьим лицам», «шифруются при передаче» (если используется HTTPS). App Store → App Privacy: *Usage Data*, *Identifiers → Device ID не используется; User ID — нет*, «не для отслеживания».

---

## 10. Эталонная реализация (C#, Unity)

Рабочий пример: `YarnPull/Assets/Scripts/Analytics.cs`. Ключевые части:

```csharp
// Инициализация (один раз при старте):
Analytics.Init();                       // создаёт install, session, читает очередь с диска
Analytics.Track("session_start", "level=" + level);
// В игровом коде:
Analytics.Track("level_win", "level=5;stars=3;sec=42");   // p строкой «k=v;k=v»
// Очередь пишется в persistentDataPath, отправка пачками раз в 60 с и при сворачивании,
// только при NetworkReachability != NotReachable, при ошибке пауза растёт до 10 мин.
```

Минимальный формат `curl` для проверки сервера:

```bash
curl -X POST "$ENDPOINT/api/v1/ingest" -H "Content-Type: application/json" -H "X-Api-Key: $KEY" \
  -d '{"v":1,"app":"myapp","install":"abc","session":"s1","sent":'$(date +%s)',
       "events":[{"n":"first_open","t":'$(date +%s)'},{"n":"level_win","t":'$(date +%s)',"v":3,"p":{"level":1,"stars":3}}]}'
# → {"ok":true,"accepted":2}
```

### Псевдокод для любой платформы

```
state: install (из хранилища; если нет — создать GUID и сохранить), session = random(12), queue = load(file)
Track(n, p, v):  queue.add({n, t: now(), id: ++seq, v, p});  dirty = true
каждые 10 с:     если dirty → сохранить queue в файл
каждые 60 с / onPause / onQuit / onStart:
    если нет сети или идёт отправка или очередь пуста → выйти
    batch = первые ≤100 событий
    POST ingest {v:1, app, ver, platform, lang, install, session, day, sent: now(), events: batch}
    200 или 400 → удалить batch из queue, backoff = 60 с
    иначе       → backoff = min(backoff*2, 600 с)
```

---

## 11. Проверка интеграции

1. В дашборде «Приложения и ключи» убедитесь, что приложение зарегистрировано (при включённой авто-регистрации оно появится само).
2. Запустите игру, сыграйте уровень, сверните игру → через ~минуту во вкладке «Живая лента» появятся события.
3. Проверьте: `first_open` приходит один раз; `session` меняется на каждый запуск; `install` — нет; у `level_win` есть `level`.
4. Выключите сеть, сыграйте, включите — события должны дойти без дублей.
