Бран · продюсер «Хроник Пограничья»
Хочу понять, откуда игроки приходят в магазин и что мы им там показываем. Программисты говорят, что всё пишется в
events. Я им верю, а ты? Если отчёт соврёт, на совете директоров краснеть буду я.
В прошлом уроке мы раскладывали выручку «Кубика» на множители. Такой разбор точен ровно настолько, насколько точно записаны данные, а в продукте данные это прежде всего события. Сегодня спускаемся на этаж ниже: какие события нужны, как их назвать и как SQL-запросом поймать трекинг на вранье. База прежняя, guild, таблица events, которую мы уже резали на воронку.
Бортовой журнал
Вот две записи из events. Обе про одно и то же: игрок открыл магазин.
name | platform | props
------------+----------+---------------------------------------------------------------------------
store_open | ios | {"source": "main_menu", "app_version": "2.0.1", "offers_shown": [3, 15, 12, 2]}
store_open | android | {"Source": "quest_reward", "app_version": "2.0.0", "offers_shown": [18, 4]}
Заметил разницу? Во второй записи ключ Source с большой буквы. Человек прочтёт обе одинаково. Запрос props->>'source' вторую не прочтёт: для JSON это два разных ключа.
Так бывает с бортовым журналом корабля. Если каждый вахтенный пишет по-своему («шли на норд», «курс 0°», «вверх по карте»), по журналу не восстановить рейс. Поэтому у флота есть форма записи, общая для всех вахт.
У продукта такая форма называется план трекинга. Аналитик пишет его до того, как программист добавит первую строчку отправки события, и потом сверяет с ним данные.
От вопроса к событию
План начинается не с событий, а с вопросов. Событие без вопроса никто не прочтёт, а вопрос без события останется без ответа. Для «Хроник» три вопроса Брана превращаются в такой план:
| вопрос продукта | событие | обязательные свойства |
|---|---|---|
| сколько новичков проходят обучение и за сколько | tutorial_start, tutorial_complete | app_version; у завершения ещё duration_sec |
| откуда игроки приходят в магазин и что видят | store_open | app_version, source, offers_shown |
| что покупают первым и за сколько | first_purchase | app_version, offer_id, price_rub, currency |
Плюс install со свойствами device и utm_source (откуда пришёл игрок) и first_quest_complete. Шесть событий на весь первый день, 15 897 строк в базе.
Три правила хорошего плана:
- Имена в одном стиле. В «Хрониках» это
объект_действиемаленькими буквами через подчёркивание:store_open,tutorial_complete. НеOpenStore, неstore opened, немагазин_открыт. Один стиль значит, чтоname LIKE 'tutorial_%'найдёт всё обучение, а опечатку видно сразу. - Обязательные свойства записаны явно. Свойство, без которого событие не отвечает на свой вопрос, обязательно.
store_openбезsourceне скажет, откуда пришёл игрок, и в отчёте про источники бесполезен. - Версия приложения в каждом событии. Когда метрика сломается, первым делом спросят «в какой версии», и без
app_versionответить нечем. В «Хрониках» это1.9.0-1.9.4до релиза 14 мая 2026,2.0.0первую неделю после него и2.0.1дальше.
Свойства лежат в jsonb
У событий разный набор свойств, поэтому их хранят не отдельными столбцами, а одним столбцом props типа jsonb. Как его читать, мы разбирали в уроке про JSON в событиях: -> достаёт вложенный JSON, ->> достаёт значение текстом.
SELECT props->>'app_version' AS app_version,
props->'device'->>'model' AS model,
props->>'utm_source' AS utm_source
FROM events
WHERE name = 'install'
LIMIT 3;
Для проверки качества главный оператор из того же урока ?: props ? 'source' отвечает, есть ли в JSON такой ключ. Это не то же самое, что props->>'source' IS NULL: ключа может не быть вовсе, а может быть ключ со значением null. Для плана трекинга важно первое.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.