Как две минуты первой сессии предсказывают судьбу вашей игры (и 4 SQL-запроса для анализа)
Анализ первой сессии -- это "момент истины" для твоей игры.
Представь, что игрок только что установил твою игру. У него нет лояльности, нет эмоциональной связи с проектом. Его первая сессия -- это как первое свидание: либо возникает магия, либо вы больше не увидитесь.
Ниже я расскажу, как на основе длительности этого "свидания", можно давать оценку и строить гипотезы. А еще прикреплю SQL запросы и поделюсь аналитикой по своей веб-игре Ферма на острове (https://yandex.ru/games/app/336608).
Что показывает длительность первой игровой сессии
1. Эффективность онбординга
Короткая сессия (менее 3-5 минут) часто означает, что игрок "уперся в стену" -- не понял механики, запутался в интерфейсе или столкнулся с техническими проблемами
Оптимальная длительность (5-15 минут) говорит о плавном вхождении в игру
Длинная сессия (15+ минут) — игрок "зацепился", он получил достаточно мотивации продолжать
2. Прогноз долгосрочного удержания
По моим данным (я их округлил для наглядности), игроки с первой сессией:
Менее 2 минут — 80% никогда не возвращаются
5-10 минут — 45% возвращаются на следующий день
15+ минут — 65% играют через 7 дней
3. Качество маркетингового трафика
Разная длительность сессии по источникам:
Рекламные сети — часто короткие сессии, если реклама не соответствует геймплею
Рекомендации друзей — более длинные сессии, так как есть социальное доверие
Органический поиск — самые вовлеченные пользователи
Сбор данных и SQL запросы
Данные я собираю в Unity Analytics.
Анализ длительности первой сессии за последние 90 дней
WITH first_sessions AS (
SELECT
USER_ID,
EVENT_JSON:sessionID::STRING as session_id,
MIN(EVENT_TIMESTAMP) as first_session_start
FROM EVENTS
WHERE EVENT_JSON:eventName::STRING = 'newPlayer'
GROUP BY USER_ID, EVENT_JSON:sessionID::STRING
),
session_durations AS (
SELECT
fs.USER_ID,
fs.session_id,
fs.first_session_start,
MIN(e.EVENT_TIMESTAMP) as session_start,
MAX(e.EVENT_TIMESTAMP) as session_end,
DATEDIFF(second, MIN(e.EVENT_TIMESTAMP), MAX(e.EVENT_TIMESTAMP)) as duration_seconds
FROM first_sessions fs
JOIN EVENTS e ON fs.USER_ID = e.USER_ID
AND fs.session_id = e.EVENT_JSON:sessionID::STRING
WHERE e.EVENT_DATE > CURRENT_DATE-90
GROUP BY fs.USER_ID, fs.session_id, fs.first_session_start
),
percentiles AS (
SELECT
APPROX_PERCENTILE(duration_seconds, 0.1) as p10_seconds,
APPROX_PERCENTILE(duration_seconds, 0.5) as p50_seconds,
APPROX_PERCENTILE(duration_seconds, 0.9) as p90_seconds,
AVG(duration_seconds) as avg_duration_seconds,
COUNT(*) as total_sessions
FROM session_durations
WHERE duration_seconds >= 0 -- исключаем отрицательные значения
)
SELECT
ROUND(p10_seconds, 2) as p10_duration_seconds,
ROUND(p50_seconds, 2) as median_duration_seconds,
ROUND(p90_seconds, 2) as p90_duration_seconds,
ROUND(avg_duration_seconds, 2) as average_duration_seconds,
total_sessions
FROM percentiles;
Анализ длительности первой сессий по платформам за последние 90 дней
Если аналитика по твоей игре собирается сразу для нескольких платформ, то будет полезно посмотреть на них отдельно.
WITH first_sessions AS (
SELECT
USER_ID,
EVENT_JSON:sessionID::STRING as session_id,
MIN(EVENT_TIMESTAMP) as first_session_start,
EVENT_JSON:platform::STRING as platform
FROM EVENTS
WHERE EVENT_JSON:eventName::STRING = 'newPlayer'
GROUP BY USER_ID, EVENT_JSON:sessionID::STRING, EVENT_JSON:platform::STRING
),
session_durations AS (
SELECT
fs.USER_ID,
fs.session_id,
fs.platform,
fs.first_session_start,
MIN(e.EVENT_TIMESTAMP) as session_start,
MAX(e.EVENT_TIMESTAMP) as session_end,
DATEDIFF(second, MIN(e.EVENT_TIMESTAMP), MAX(e.EVENT_TIMESTAMP)) as duration_seconds
FROM first_sessions fs
JOIN EVENTS e ON fs.USER_ID = e.USER_ID
AND fs.session_id = e.EVENT_JSON:sessionID::STRING
WHERE e.EVENT_DATE > CURRENT_DATE-90
AND fs.platform IS NOT NULL
GROUP BY fs.USER_ID, fs.session_id, fs.platform, fs.first_session_start
)
SELECT
COALESCE(platform, 'Не указана') as platform,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.1), 2) as p10_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.5), 2) as median_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.9), 2) as p90_duration_seconds,
ROUND(AVG(duration_seconds), 2) as average_duration_seconds,
COUNT(*) as total_sessions
FROM session_durations
GROUP BY platform
ORDER BY total_sessions DESC;
Анализ длительности первой сессии понедельно за последние три квартала
Иногда может быть полезно посмотреть как изменилась длина первой сессии с течением времени.
WITH first_sessions AS (
SELECT
USER_ID,
EVENT_JSON:sessionID::STRING as session_id,
MIN(EVENT_TIMESTAMP) as first_session_start,
EVENT_JSON:platform::STRING as platform,
YEAR(EVENT_TIMESTAMP) as session_year,
WEEK(EVENT_TIMESTAMP) as session_week
FROM EVENTS
WHERE EVENT_JSON:eventName::STRING = 'newPlayer'
GROUP BY USER_ID, EVENT_JSON:sessionID::STRING, EVENT_JSON:platform::STRING,
YEAR(EVENT_TIMESTAMP), WEEK(EVENT_TIMESTAMP)
),
session_durations AS (
SELECT
fs.USER_ID,
fs.session_id,
fs.platform,
fs.first_session_start,
fs.session_year,
fs.session_week,
MIN(e.EVENT_TIMESTAMP) as session_start,
MAX(e.EVENT_TIMESTAMP) as session_end,
DATEDIFF(second, MIN(e.EVENT_TIMESTAMP), MAX(e.EVENT_TIMESTAMP)) as duration_seconds
FROM first_sessions fs
JOIN EVENTS e ON fs.USER_ID = e.USER_ID
AND fs.session_id = e.EVENT_JSON:sessionID::STRING
WHERE e.EVENT_DATE > CURRENT_DATE-270
AND fs.platform IS NOT NULL
GROUP BY fs.USER_ID, fs.session_id, fs.platform, fs.first_session_start,
fs.session_year, fs.session_week
)
SELECT
session_year,
session_week,
CONCAT(session_year, '-W', LPAD(session_week, 2, '0')) as year_week,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.1), 2) as p10_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.5), 2) as median_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.9), 2) as p90_duration_seconds,
ROUND(AVG(duration_seconds), 2) as average_duration_seconds,
COUNT(*) as total_sessions
FROM session_durations
WHERE session_year IS NOT NULL
AND session_week IS NOT NULL
GROUP BY session_year, session_week
ORDER BY year_week ASC;
Анализ длительности первой сессии по версиям за последний год
И еще интереснее сравнить длительность первой сессии для каждой версии игры за последнее время.
WITH first_sessions AS (
SELECT
USER_ID,
EVENT_JSON:sessionID::STRING as session_id,
MIN(EVENT_TIMESTAMP) as first_session_start,
EVENT_JSON:clientVersion::STRING as version
FROM EVENTS
WHERE EVENT_JSON:eventName::STRING = 'newPlayer'
GROUP BY USER_ID, EVENT_JSON:sessionID::STRING, EVENT_JSON:clientVersion::STRING
),
session_durations AS (
SELECT
fs.USER_ID,
fs.session_id,
fs.version,
fs.first_session_start,
MIN(e.EVENT_TIMESTAMP) as session_start,
MAX(e.EVENT_TIMESTAMP) as session_end,
DATEDIFF(second, MIN(e.EVENT_TIMESTAMP), MAX(e.EVENT_TIMESTAMP)) as duration_seconds
FROM first_sessions fs
JOIN EVENTS e ON fs.USER_ID = e.USER_ID
AND fs.session_id = e.EVENT_JSON:sessionID::STRING
WHERE e.EVENT_DATE > CURRENT_DATE-360
AND fs.version IS NOT NULL
GROUP BY fs.USER_ID, fs.session_id, fs.version, fs.first_session_start
)
SELECT
version,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.1), 2) as p10_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.5), 2) as median_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.9), 2) as p90_duration_seconds,
ROUND(AVG(duration_seconds), 2) as average_duration_seconds,
COUNT(*) as total_sessions
FROM session_durations
WHERE version IS NOT NULL
GROUP BY version
ORDER BY version DESC;
Пример например
На примере веб-игры Ферма на острове (https://yandex.ru/games/app/336608), я собрал следующий запрос (ниже опишу разницу):
WITH first_sessions AS (
SELECT
USER_ID,
EVENT_JSON:sessionID::STRING as session_id,
MIN(EVENT_TIMESTAMP) as first_session_start,
EVENT_JSON:clientVersion::STRING as version
FROM EVENTS
WHERE EVENT_JSON:eventName::STRING = 'newPlayer'
GROUP BY USER_ID, EVENT_JSON:sessionID::STRING, EVENT_JSON:clientVersion::STRING
),
session_durations AS (
SELECT
fs.USER_ID,
fs.session_id,
fs.version,
fs.first_session_start,
MIN(e.EVENT_TIMESTAMP) as session_start,
MAX(e.EVENT_TIMESTAMP) as session_end,
DATEDIFF(second, MIN(e.EVENT_TIMESTAMP), MAX(e.EVENT_TIMESTAMP)) as duration_seconds
FROM first_sessions fs
JOIN EVENTS e ON fs.USER_ID = e.USER_ID
AND fs.session_id = e.EVENT_JSON:sessionID::STRING
WHERE fs.version IS NOT NULL
GROUP BY fs.USER_ID, fs.session_id, fs.version, fs.first_session_start
)
SELECT
version,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.1), 2) as p10_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.5), 2) as median_duration_seconds,
ROUND(APPROX_PERCENTILE(duration_seconds, 0.9), 2) as p90_duration_seconds,
ROUND(AVG(duration_seconds), 2) as average_duration_seconds,
COUNT(*) as total_sessions
FROM session_durations
WHERE version IS NOT NULL
GROUP BY version
HAVING total_sessions > 100
ORDER BY version DESC;
Номер версии генерируется автоматически при сборке -- могу рассказать другим постом, если интересно...
1. Я исключил данные по версиям, в которых было менее 100 первых сессий, чтобы убрать всплески:
2. Выгребаю данные за всё время, чтобы вот вообще всё увидеть :D
Вот, что получилось:
Релиз 1.1.2 от 30 октября 2024 года каким-то образом сломал первое впечатление об игре.
Тогда я об этом узнал не сразу, а только через неделю, потому что только-только настраивал аналитику. Это было мега печально, потому что эти игроки были привлечены рекламой за кровные.
Протестировав игру через инкогнито самостоятельно и привлекая друзей, удалось определить причину отвала: аналитика собиралась некорректно.
В браузерных играх, Unity Analytics складывает идентификатор пользователя в куки, а как только они протухают, то обновляет его. Получалось, что каждый раз, игра идентифицировала игрока по новой и считала, что это его первая сессия.
Решение было простым: генерировать случайный идентификатор и сохранять его в данных игрока, а затем использовать в качестве UnityServices.ExternalUserId.
20 октября была выпущена версия 1.2.0, в которой игра начала идентифицировать игроков корректно:
Это позволило адекватно оценить продолжительность первой игровой сессии и составить ряд гипотез. Первое, что бросилось в глаза после плейтестов: игра дарит игроку снаряжение, но не объясняет, как им пользоваться.
Спустя еще неделю игра начала обучать игрока экипировать и прокачивать снаряжение, и вот результат:
Это дало прирост в две минуты или ~28%!
После нескольких релизов летом, начиная с реализации защиты от накруток при переводе времени на устройстве, медиана начала падать:
Возможно, механики просто наскучили и игроки хотят чего-то нового.
Гипотезы закончились, проект наскучил и я переключился на новый.
Заключение
Надеюсь, что мне удалось рассказать что-то интересное и поделиться опытом аналитики.
Спасибо за внимание! Жду вопросы в комментариях.




















