Как две минуты первой сессии предсказывают судьбу вашей игры (и 4 SQL-запроса для анализа)

Анализ первой сессии -- это "момент истины" для твоей игры.

Представь, что игрок только что установил твою игру. У него нет лояльности, нет эмоциональной связи с проектом. Его первая сессия -- это как первое свидание: либо возникает магия, либо вы больше не увидитесь.

Замылил картинку для интриги :D
Замылил картинку для интриги :D

Ниже я расскажу, как на основе длительности этого "свидания", можно давать оценку и строить гипотезы. А еще прикреплю SQL запросы и поделюсь аналитикой по своей веб-игре Ферма на острове.

Что показывает длительность первой игровой сессии

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;

Пример например

На примере веб-игры Ферма на острове, я собрал следующий запрос (ниже опишу разницу):

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 года каким-то образом сломал первое впечатление об игре.

50% игроков проводили 43 секунды и меньше в свою первую игровую сессию
50% игроков проводили 43 секунды и меньше в свою первую игровую сессию

Тогда я об этом узнал не сразу, а только через неделю, потому что только-только настраивал аналитику. Это было мега печально, потому что эти игроки были привлечены рекламой за кровные.

Протестировав игру через инкогнито самостоятельно и привлекая друзей, удалось определить причину отвала: аналитика собиралась некорректно.

В браузерных играх, Unity Analytics складывает идентификатор пользователя в куки, а как только они протухают, то обновляет его. Получалось, что каждый раз, игра идентифицировала игрока по новой и считала, что это его первая сессия.

Решение было простым: генерировать случайный идентификатор и сохранять его в данных игрока, а затем использовать в качестве UnityServices.ExternalUserId.

20 октября была выпущена версия 1.2.0, в которой игра начала идентифицировать игроков корректно:

Как две минуты первой сессии предсказывают судьбу вашей игры (и 4 SQL-запроса для анализа)

Это позволило адекватно оценить продолжительность первой игровой сессии и составить ряд гипотез. Первое, что бросилось в глаза после плейтестов: игра дарит игроку снаряжение, но не объясняет, как им пользоваться.

Спустя еще неделю игра начала обучать игрока экипировать и прокачивать снаряжение, и вот результат:

Как две минуты первой сессии предсказывают судьбу вашей игры (и 4 SQL-запроса для анализа)

Это дало прирост в две минуты или ~28%!

После нескольких релизов летом, начиная с реализации защиты от накруток при переводе времени на устройстве, медиана начала падать:

Как две минуты первой сессии предсказывают судьбу вашей игры (и 4 SQL-запроса для анализа)

Возможно, механики просто наскучили и игроки хотят чего-то нового.

Гипотезы закончились, проект наскучил и я переключился на новый.

Заключение

Надеюсь, что мне удалось рассказать что-то интересное и поделиться опытом аналитики.

Спасибо за внимание! Жду вопросы в комментариях.

2