Введение
Без обратной связи о том, какие страницы справки реально читают, что ищут через поиск и какие запросы не дают результатов, авторы контента работают «вслепую» — пишут то, что кажется важным, а не то, что действительно нужно пользователям. В статье разберём, как добавить минимальную, уважающую приватность аналитику использования встроенной справки, и как её результаты должны влиять на приоритеты последующего обновления контента.
Концепция
Базовая аналитика справки включает три типа событий: открытие конкретной страницы (через какой механизм — 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), долго работающее приложение без ротации лога аналитики может неограниченно накапливать файл, потребляя дисковое пространство без видимой пользы для самого пользователя (поскольку аналитика обычно нужна разработчикам, а не ему).
- Интерпретация «поиска без результатов» без учёта опечаток и синонимов. Не каждый запрос без результатов означает реальный пробел в контенте — часть из них может быть опечатками или использованием совершенно нерелевантных терминов; качественный анализ должен включать ручной просмотр топа таких запросов человеком, а не автоматическое создание новых статей по каждому формальному «запросу без результата».