В сфере разработки программного обеспечения время, проведённое пользователем в приложении, является одним из ключевых показателей его эффективности и востребованности. Отслеживание этой метрики позволяет разработчикам понять, насколько приложение полезно и удобно, и в дальнейшем ориентировать усилия на улучшение пользовательского опыта. Однако не всегда можно использовать готовые решения для сбора телеметрических данных, особенно когда речь идёт о приложениях, написанных на востребованных, но относительно молодых языках программирования, таких как Rust. В этой статье мы расскажем о нашем пути к созданию собственного алгоритма трекинга времени для приложения на Rust, о вызовах, с которыми столкнулись, и о том, как удалось эффективно решить эту задачу. Когда перед нами в 2021 году в стартапе, разрабатывающем приложение для рабочего стола, встал вопрос — сколько времени пользователи проводят в приложении ежедневно — мы столкнулись с ограничениями существующих инструментов.
Наши телеметрические данные не могли дать ответа на этот вопрос, так как текущая система не собирала необходимую информацию о продолжительности сессий. Несмотря на стремление использовать готовые SDK и сторонние сервисы, пришлось отказаться от этого варианта из-за отсутствия подходящих решений в экосистеме Rust и потенциальных сложностей с переносом кода с других платформ, таких как Swift для iOS. Первоначальная идея заключалась в использовании механизма «heartbeat» — периодической отправки сигналов на сервер, подтверждающих, что пользователь всё ещё активен в приложении. Однако при глубоком анализе мы поняли, что такой подход создаёт слишком большой объём событий, что ведёт к значительным затратам у сервиса аналитики, поскольку их тарифы базируются на объёме поступающих данных. Кроме того, увеличение интервала отправки таких сигналов для экономии ресурсов снижает точность измерений, что делает эту стратегию недостаточно надёжной.
Другой вариант — логировать событие при фокусировке приложения и соответствующее событие при потере фокуса. Казалось бы, таким образом можно примерно оценить время взаимодействия пользователя с приложением. Но на практике оказалось, что подобный способ склонен к переоценке времени — пользователь может оставить приложение открытым и сфокусированным на долгое время, при этом фактически не используя его активно. Более того, подобный метод уязвим к потере данных при аварийном завершении приложения, когда событие выхода из фокуса не успевает быть зафиксировано. Именно этот момент заставил нас искать более гибкое и устойчивое решение, которое не требовало бы постоянного мониторинга или событий, сильно нагружающих систему.
Мы пришли к идее сессий — представить пользовательскую активность как последовательность событий, сгруппированных по временным окнам, и определять сессию как серию событий, происходящих с перерывом не более 5 минут. Если между двумя событиями проходит больше 5 минут, то считаем, что пользователь закончил одну сессию и начал другую. Подобный подход позволял удобно объединять события, которые отражают активность пользователя, без необходимости непрерывно отправлять информацию на сервер, что значительно снижало количество данных для обработки. Мы отметили, что если пользователь переключается между приложениями, но возвращается в наше не реже, чем через 5 минут, то время его активности в нашей программе немного завышается. Однако, по нашему мнению, такой пользователь был заинтересован в работе с приложением и это не является проблемой для оценки вовлечённости.
В свою очередь, если пользователь задумался и сделал паузу более 6 минут перед следующим действием, такая активность разделится на две сессии, а промежуток между ними не будет считаться за время использования. В реальной практике таких случаев оказалось очень мало, поэтому мы приняли эту невеликая неточность как приемлемую погрешность. Первичная реализация алгоритма включала в себя хранение информации о начале текущей сессии и времени последнего события. При фиксировании нового события происходила проверка, не превышен ли порог в 5 минут с момента предыдущей активности: если превышен, то создавалась новая сессия с обновлённой меткой времени; если нет — событие относилось к текущей сессии. При этом для отправки телеметрических данных формировался пакет, включающий имя события, метки начала сессии и времени самого события.
Таким образом была организована компактная и адекватная фиксация пользовательской активности в приложении. Однако на следующем этапе мы столкнулись с тем, что такой метод не учитывает весь спектр действий пользователя — например, регулярные взаимодействия с интерфейсом, такие как набор текста, переключение между окнами и другое, которые не всегда были представлены в виде ключевых событий. Отказаться от их учёта означало занижение времени активности, что сказывалось на точности анализа. Мы рассматривали идею отправлять события для каждого отдельного нажатия клавиш или взаимодействия с окном, но этот подход грозил создавать чрезмерно большой поток данных и поднять стоимость хранения и обработки телеметрии. Поэтому решено было добавить механизм выборочного сэмплирования таких «фоново активных» событий.
Например, если пользователь доходит до такого действия хотя бы один раз в минуту, мы формируем специальное событие «App Active» каждые 60 секунд. Это позволило покрыть активность пользователя в целом, не создавая чрезмерной нагрузки на систему. Для оптимизации также внедрили логику объединения событий: несколько последовательных событий «App Active» в одной сессии сокращаются до одного, а если после такого события было совершено ключевое действие, то оно заменяло событие активности. Это позволило избежать избыточной информации и повысило точность определения сессий. В исходном коде на Rust появилась конструкция с очередью ограниченного размера, где хранились события с присвоением уникальных идентификаторов сессий.
При каждом новом действии происходила проверка «устаревания» сессии по временным меткам, и в случае необходимости начиналась новая сессия. Эта система поддерживала стабильность данных и позволяла эффективно работать с ограниченными ресурсами хранения телеметрии. После внедрения и реализации алгоритма мы получили возможность визуализировать среднее время, которое пользователи проводят в нашем приложении. Анализ данных показал, что большинство пользователей активно использовали продукт в течение значительной части рабочего дня. Это дало нам уверенность в правильности выбранного направления развития приложения и подтвердило его ценность для аудитории.
Стоит отметить, что подобный подход можно сделать более устойчивым к внезапному завершению работы приложения, написав обработчики системных сигналов, перехватывающих закрытие программы и корректно выгружающих накопленные данные на сервер. Однако полное решение проблемы неустойчивости при аварийных крашах, когда процесс завершается немедленно и без сохранения состояния, остаётся вызовом, который требует дополнительных технических решений. Подытоживая, создание собственного алгоритма учёта времени использования для приложения на Rust позволило нам избежать зависимости от сторонних сервисов, оптимизировать расходы на хранение и обработку данных, а также получить актуальную и полезную информацию о вовлечённости пользователей. Выбор сочетания сессий и выборочной отправки событий активности оказался практичным и даёт перспективы для дальнейшего усложнения и улучшения аналитики. Этот опыт демонстрирует, что гибкость и понимание внутренней логики продукта помогают в разработке эффективных и экономичных решений даже в условиях ограниченного технологического стека и необходимости оперировать с минимальным объёмом данных.
Для разработчиков на Rust и других языках полезно помнить, что иногда наиболее достойным ответом на сложные задачи становится разработка собственного алгоритма, адаптированного к конкретным требованиям и условиям проекта.