Введение
В заключительной статье цикла и всего расширенного плана статей разберём, как собрать минимально необходимые данные о процессе установки и обновления приложения — какая доля пользователей успешно завершает установку, какая часть переходит на новые версии и с какой скоростью, какие ошибки чаще всего возникают на этапе установки — чтобы принимать обоснованные решения об улучшении этого процесса, а не полагаться на догадки и единичные отзывы пользователей.
Концепция
Телеметрия установки и обновлений должна собирать минимально необходимый набор анонимизированных событий: успешное завершение установки (с указанием версии и платформы), ошибки установки (с типом ошибки, без персональных данных), факт и время перехода на новую версию относительно даты выпуска обновления (для построения «воронки» — какая доля аудитории обновилась через день, неделю, месяц после релиза). Эти данные обычно отправляются на простой HTTP-эндпоинт сбора событий — никакой специфичной для QtIFW инфраструктуры не требуется, достаточно той же модели событийного логирования, что и в статье 176 про аналитику использования справки, применённой к другому набору событий.
Пример кода: Телеметрия установки и
// installscript.qs — отправка события об успешной установке прямо из установщика
function Component()
{
installer.installationFinished.connect(this, Component.prototype.onInstallationFinished);
}
Component.prototype.onInstallationFinished = function()
{
if (installer.status === QInstaller.Success) {
var version = installer.value("ProductVersion");
var platform = systemInfo.productType;
// QtIFW поддерживает выполнение системных команд из скрипта установки —
// используем это для отправки минимального HTTP-запроса о завершении установки
var process = new QProcess();
var curlArgs = ["-X", "POST",
"https://telemetry.mycompany.com/events/install_completed",
"-d", JSON.stringify({version: version, platform: platform, timestamp: new Date().toISOString()})];
process.start("curl", curlArgs);
process.waitForFinished(5000);
}
};
// installtelemetry.h — отправка событий об обновлении из самого приложения
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QJsonObject>
#include <QJsonDocument>
#include <QSettings>
#include <QCoreApplication>
#include <QDateTime>
class InstallTelemetry : public QObject
{
Q_OBJECT
public:
explicit InstallTelemetry(QObject *parent = nullptr) : QObject(parent) {}
void reportIfVersionChanged()
{
QSettings settings;
const QString previousVersion = settings.value("telemetry/lastReportedVersion").toString();
const QString currentVersion = QCoreApplication::applicationVersion();
if (previousVersion == currentVersion) {
return; // версия не изменилась с момента последнего запуска — ничего не сообщаем
}
const bool isFirstRun = previousVersion.isEmpty();
sendEvent(isFirstRun ? "install_first_run" : "update_completed",
{{"previousVersion", previousVersion}, {"newVersion", currentVersion}});
settings.setValue("telemetry/lastReportedVersion", currentVersion);
}
private:
void sendEvent(const QString &eventType, const QJsonObject &extraData)
{
QJsonObject payload = extraData;
payload["event"] = eventType;
payload["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
payload["anonymousInstallId"] = anonymousInstallId();
QNetworkRequest request(QUrl("https://telemetry.mycompany.com/events"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
m_manager.post(request, QJsonDocument(payload).toJson());
}
QString anonymousInstallId()
{
// Случайный, не привязанный к личности пользователя идентификатор
// конкретной установки — НЕ персональные данные, генерируется один раз
QSettings settings;
QString id = settings.value("telemetry/anonymousId").toString();
if (id.isEmpty()) {
id = QUuid::createUuid().toString(QUuid::WithoutBraces);
settings.setValue("telemetry/anonymousId", id);
}
return id;
}
QNetworkAccessManager m_manager;
};
Пояснения к коду
JavaScript-скрипт установщика отправляет событие install_completed непосредственно из самого процесса установки через вызов внешней утилиты curl — поскольку QtIFW не предоставляет встроенного HTTP-клиента в своей скриптовой среде, делегирование этой задачи системному инструменту командной строки — практичное решение, хотя и зависящее от наличия curl в системе (что на современных Windows 10+/macOS/большинстве Linux дистрибутивов по умолчанию доступно, но не гарантировано универсально).
InstallTelemetry::reportIfVersionChanged() вызывается при каждом запуске приложения, но реально отправляет событие только когда обнаруживает изменение версии относительно последнего зафиксированного значения — различая install_first_run (совсем новая установка, previousVersion пуста) и update_completed (переход с одной версии на другую) даёт возможность строить отдельную воронку для новых установок и для процесса принятия обновлений существующими пользователями. anonymousInstallId() генерирует случайный, не связанный с личностью пользователя идентификатор конкретной установки — это позволяет отличать уникальные установки в агрегированной статистике без сбора каких-либо персональных данных.
Подводные камни
- Сбор телеметрии без явного согласия пользователя или прозрачного объяснения, какие данные собираются и зачем — даже полностью анонимизированные данные об установке должны собираться с уважением к ожиданиям приватности пользователя; для многих юрисдикций и категорий продуктов нужно явное согласие (опт-ин) или, как минимум, чёткое раскрытие в политике конфиденциальности, доступной перед самой установкой, а не просто молчаливая отправка данных.
- Отправка телеметрии непосредственно из скрипта установщика через внешний
curl-процесс блокирует завершение процесса установки до истечения таймаута сетевого запроса (или его успешного завершения) —process.waitForFinished(5000)в примере ограничивает это пятью секундами, но даже такая задержка добавляет заметное, хотя и небольшое замедление к восприятию пользователем скорости завершения установки; для минимизации влияния стоит рассмотреть полностью неблокирующий, «fire-and-forget» вызов без ожидания завершения вовсе, принимая риск того, что часть таких событий может не дойти до сервера при быстром завершении установщика. - Отсутствие устойчивости к временной недоступности сервера телеметрии. Если
telemetry.mycompany.comвременно недоступен в момент попытки отправки события (а не только в момент установки, но и при обычном запуске приложения после обновления), событие просто теряется без какой-либо локальной очереди для повторной попытки отправки позже — для более полной картины (особенно важной именно для построения точной «воронки» обновлений) стоит реализовать локальное буферизирование неотправленных событий с повторной попыткой при следующем успешном сетевом соединении, а не безусловно терять каждое неудачно отправленное событие. - Интерпретация агрегированных метрик телеметрии без учёта смещения выборки (selection bias). Пользователи, которые в принципе не соглашаются на отправку телеметрии (если это опционально, как и должно быть согласно предыдущему пункту), систематически отсутствуют в собранных данных — выводы о «доле пользователей, перешедших на новую версию за неделю», основанные только на телеметрии от согласившихся пользователей, могут не отражать реальную картину для всей аудитории продукта в целом, и это ограничение стоит явно учитывать при принятии решений на основе таких агрегированных метрик.