Различные статьи по Qt и c++

Аналитика использования встроенной справки: что ищут пользователи и зачем это считать

Введение

Без обратной связи о том, какие страницы справки реально читают, что ищут через поиск и какие запросы не дают результатов, авторы контента работают «вслепую» — пишут то, что кажется важным, а не то, что действительно нужно пользователям. В статье разберём, как добавить минимальную, уважающую приватность аналитику использования встроенной справки, и как её результаты должны влиять на приоритеты последующего обновления контента.

Концепция

Базовая аналитика справки включает три типа событий: открытие конкретной страницы (через какой механизм — TOC, поиск, F1, прямая ссылка), выполнение поискового запроса (сам текст запроса и число найденных результатов — нулевое число результатов особенно ценно: это явный сигнал о пробеле в контенте) и явная обратная связь «было полезно / не было полезно» на конкретной странице, если она реализована в интерфейсе справки. Все эти события можно логировать локально (для оффлайн-анализа поддержкой) либо отправлять на сервер аналитики, если у продукта уже есть инфраструктура для этого и пользователь дал согласие на телеметрию.

Пример кода: Аналитика использования встроенной справки

#pragma once
#include <QObject>
#include <QDateTime>
#include <QFile>
#include <QJsonObject>
#include <QJsonDocument>
#include <QStandardPaths>

class HelpAnalytics : public QObject
{
    Q_OBJECT
public:
    explicit HelpAnalytics(QObject *parent = nullptr) : QObject(parent)
    {
        const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
        m_logPath = dir + "/help_analytics.jsonl";
    }

    void logPageView(const QUrl &page, const QString &source)
    {
        QJsonObject event;
        event["type"] = "page_view";
        event["url"] = page.toString();
        event["source"] = source; // "toc", "search", "f1", "direct_link"
        event["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
        appendEvent(event);
    }

    void logSearchQuery(const QString &query, int resultCount)
    {
        QJsonObject event;
        event["type"] = "search_query";
        event["query"] = query;
        event["result_count"] = resultCount;
        event["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
        appendEvent(event);

        if (resultCount == 0) {
            qDebug() << "Поиск без результатов (потенциальный пробел в контенте):" << query;
        }
    }

    void logPageFeedback(const QUrl &page, bool wasHelpful)
    {
        QJsonObject event;
        event["type"] = "page_feedback";
        event["url"] = page.toString();
        event["was_helpful"] = wasHelpful;
        event["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
        appendEvent(event);
    }

private:
    void appendEvent(const QJsonObject &event)
    {
        QFile file(m_logPath);
        if (file.open(QIODevice::Append | QIODevice::Text)) {
            file.write(QJsonDocument(event).toJson(QJsonDocument::Compact));
            file.write("\n");
        }
    }

    QString m_logPath;
};
#!/usr/bin/env python3
# analyze_help_usage.py — офлайн-анализ собранных логов для авторов контента

import json
from collections import Counter

def analyze(log_path):
    zero_result_queries = Counter()
    page_views = Counter()
    negative_feedback_pages = Counter()

    with open(log_path, encoding="utf-8") as f:
        for line in f:
            event = json.loads(line)
            if event["type"] == "search_query" and event["result_count"] == 0:
                zero_result_queries[event["query"]] += 1
            elif event["type"] == "page_view":
                page_views[event["url"]] += 1
            elif event["type"] == "page_feedback" and not event["was_helpful"]:
                negative_feedback_pages[event["url"]] += 1

    print("Топ запросов без результатов поиска (приоритет для нового контента):")
    for query, count in zero_result_queries.most_common(10):
        print(f"  {count:4d}  {query}")

    print("\nСтраницы с наибольшим количеством негативной обратной связи:")
    for url, count in negative_feedback_pages.most_common(10):
        print(f"  {count:4d}  {url}")

if __name__ == "__main__":
    analyze("help_analytics.jsonl")

Пояснения к коду

HelpAnalytics пишет каждое событие как отдельную JSON-строку (формат NDJSON, уже разбиравшийся в статье про потоковый разбор больших JSON-файлов) — этот формат удобен именно для логов, поскольку позволяет дописывать новые события (QIODevice::Append) без необходимости перезаписывать весь файл и без риска повредить уже накопленные данные при сбое посередине записи. Три метода (logPageView, logSearchQuery, logPageFeedback) покрывают три типа событий, обсуждённых в концепции.

analyze_help_usage.py — простой офлайн-скрипт, который читает накопленный лог и агрегирует две самые практически ценные метрики: поисковые запросы без единого результата (прямой сигнал о недостающем контенте) и страницы с негативной обратной связью (сигнал о том, что существующий контент непонятен или неполон) — обе метрики напрямую указывают авторам документации, куда направить усилия по улучшению в первую очередь.

Подводные камни

  • Сбор аналитики без явного согласия пользователя или без анонимизации может нарушать законодательство о защите данных (GDPR и аналоги) — даже локальное логирование «безобидных» событий, как поисковые запросы, может непреднамеренно фиксировать персональные данные, если пользователь, например, ищет в справке термин, содержащий личную информацию (что не редкость для поддержки конкретных, специфичных пользовательских сценариев); явное согласие и/или возможность отключить аналитику — обязательны для продакшен-реализации.
  • Локальный лог, никогда не покидающий машину пользователя, бесполезен для авторов контента, если нет механизма агрегации с множества пользователей — для извлечения реальной пользы из аналитики (как в примере с analyze_help_usage.py) нужна либо явная (с согласия) отправка агрегированных, анонимизированных данных на сервер, либо ручной процесс сбора фидбека от пользователей поддержкой/опросами, что менее автоматизировано, но не требует телеметрии вовсе.
  • Неограниченный рост файла лога на машине пользователя без ротации или ограничения размера — аналогично проблеме, разобранной в статье про логирование (статья 72), долго работающее приложение без ротации лога аналитики может неограниченно накапливать файл, потребляя дисковое пространство без видимой пользы для самого пользователя (поскольку аналитика обычно нужна разработчикам, а не ему).
  • Интерпретация «поиска без результатов» без учёта опечаток и синонимов. Не каждый запрос без результатов означает реальный пробел в контенте — часть из них может быть опечатками или использованием совершенно нерелевантных терминов; качественный анализ должен включать ручной просмотр топа таких запросов человеком, а не автоматическое создание новых статей по каждому формальному «запросу без результата».