1
0
forked from BRT/arc

Add bank profile and phone metadata for improved task tracking and error reporting. Update TaskDumpMeta structure and associated scripts to include bankProfileId and profilePhone. Enhance LogArchiveSender and monitoring outputs for better operator identification and debugging.

This commit is contained in:
slava 2026-05-30 14:54:32 +05:00
parent 65d3b00bb9
commit 8d86d5f835
20 changed files with 102 additions and 50 deletions

View File

@ -37,7 +37,7 @@ void GetCardInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран

View File

@ -35,7 +35,7 @@ void GetProfileInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран

View File

@ -36,7 +36,7 @@ void LoginAndCheckAccountsScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -166,22 +166,30 @@ TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind,
}
QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
const QString apiToken = SettingsDAO::get("api_key");
const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
// api-токен в caption больше не пишем: он общий на нескольких операторов и
// не позволяет отличить, чей профиль упал. Идентифицируем по UUID профиля,
// материалу и телефону профиля (см. m_meta).
QStringList head{QStringLiteral("[TASK_ERROR]")};
if (!apiToken.isEmpty())
head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped());
head << QStringLiteral("[%1]").arg(m_bank.toHtmlEscaped());
head << QStringLiteral("[%1]").arg(m_taskKind.toHtmlEscaped());
QString body = QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped());
const QString desktopId = SettingsDAO::get("desktop_id");
QString body;
if (!desktopId.isEmpty())
body += QStringLiteral("Desktop: %1\n").arg(desktopId.toHtmlEscaped());
body += QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped());
if (!m_meta.bankProfileId.isEmpty())
body += QStringLiteral("\nProfile: %1").arg(m_meta.bankProfileId.toHtmlEscaped());
if (!m_meta.materialId.isEmpty())
body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
if (m_meta.amount != 0)
body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.phone.toHtmlEscaped());
body += QStringLiteral("\nRecipient: %1").arg(m_meta.phone.toHtmlEscaped());
if (!m_meta.card.isEmpty())
body += QStringLiteral("\nCard: %1").arg(m_meta.card.toHtmlEscaped());
body += QStringLiteral("\nError: %1").arg(errorMessage.left(800).toHtmlEscaped());
@ -238,9 +246,13 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream ts(&ef);
ts.setEncoding(QStringConverter::Utf8);
const QString desktopId = SettingsDAO::get("desktop_id");
if (!desktopId.isEmpty()) ts << "desktop_id: " << desktopId << "\n";
ts << "bank: " << m_bank << "\n"
<< "task: " << m_taskKind << "\n"
<< "device: " << m_adbDeviceIdRaw << "\n";
if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n";
if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n";
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";
@ -255,6 +267,17 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
const QString archivePath = m_folder + ".zip";
const QString text = buildCaption(errorMessage);
// Тот же идентификатор профиля/материала/телефона добавляем в caption архива
// логов ([LOGS]), чтобы по общему api-токену можно было разделить операторов.
QStringList logsIdLines;
if (!m_meta.bankProfileId.isEmpty())
logsIdLines << QStringLiteral("Profile: %1").arg(m_meta.bankProfileId.toHtmlEscaped());
if (!m_meta.materialId.isEmpty())
logsIdLines << QStringLiteral("Material: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty())
logsIdLines << QStringLiteral("Phone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
const QString logsIdentity = logsIdLines.join(QStringLiteral("\n"));
// Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем
// его в память, пока папка ещё цела.
QByteArray finalScreenshot;
@ -265,7 +288,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
}
}
QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot]() {
QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot, logsIdentity]() {
QByteArray archiveBytes;
if (zipFolder(folder, archivePath)) {
archiveBytes = readFileBytes(archivePath);
@ -301,7 +324,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
// Следом — application.log + БД (+ WAL/SHM) отдельным архивом.
// Запускаем уже ПОСЛЕ того, как task-дамп поставлен в очередь
// отправки, чтобы порядок прихода на relay был "дамп → логи".
LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR"));
LogArchiveSender::dispatch(QStringLiteral("TASK_ERROR"), -1, logsIdentity);
});
QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater);
bg->start();

View File

@ -9,8 +9,10 @@ class NetworkService;
struct TaskDumpMeta {
QString materialId;
double amount = 0; // 0 = нет
QString phone; // пусто = нет
QString phone; // пусто = нет (телефон получателя в транзакции)
QString card; // пусто = нет
QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах)
QString profilePhone; // телефон банковского профиля (не получателя)
};
class TaskDumpRecorder {

View File

@ -36,7 +36,7 @@ void GetCardInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -276,7 +276,7 @@ void GetLastTransactionsScript::doStart() {
}
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}};
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -39,7 +39,7 @@ void GetProfileInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -41,7 +41,7 @@ void LoginAndCheckAccountsScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -123,7 +123,7 @@ void PayByCardScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber};
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -120,7 +120,7 @@ void PayByPhoneScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}};
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -42,7 +42,7 @@ void GetCardInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран

View File

@ -109,7 +109,7 @@ void GetLastTransactionsScript::doStart() {
}
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}};
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
if (!navigateToTransactionList(device, app)) {

View File

@ -37,7 +37,7 @@ void GetProfileInfoScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран

View File

@ -40,7 +40,7 @@ void LoginAndCheckAccountsScript::doStart() {
return;
}
const TaskDumpMeta dumpMeta{};
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -91,7 +91,7 @@ void PayByCardScript::doStart() {
}
const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber};
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -100,7 +100,7 @@ void PayByPhoneScript::doStart() {
}
const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}};
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -34,7 +34,7 @@
#include "dushanbe/PayByPhoneScript.h"
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonValue &data, int eventId = -1, bool updateStatus = true);
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1, const QString &transactionId = {});
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName, const QString &description, const QByteArray &screenshot = {}, int eventId = -1, const QString &transactionId = {}, int accountId = -1);
static void sendMonitoringLog(const QString &type, const QString &event, const QString &message, const QByteArray &image = {}, int eventId = -1, bool toApiBase = false);
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
@ -78,7 +78,7 @@ void EventHandler::start() {
if (e.sentToServer) continue;
EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted");
EventDAO::markSentToServer(e.id);
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, -1, e.transactionId);
sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, -1, e.transactionId, e.accountId);
qDebug() << "[EventHandler] Closed stale event id:" << e.id;
}
}
@ -127,7 +127,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
if (eventType == EventType::FetchProfile) {
const QList<EventInfo> cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile);
for (const EventInfo &c : cancelled) {
sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, -1, c.transactionId);
sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, -1, c.transactionId, c.accountId);
EventDAO::markSentToServer(c.id);
}
}
@ -168,7 +168,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) {
if (!onlineDevices.contains(account.deviceId)) {
qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId;
EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline");
sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, -1, incomingTransactionId);
sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, -1, incomingTransactionId, account.id);
}
}
@ -215,7 +215,7 @@ static void sendTaskResult(const QString &type, const QString &materialId, const
static void sendTaskError(const QString &type, const QString &materialId, const QString &bankName,
const QString &description, const QByteArray &screenshot, int eventId,
const QString &transactionId) {
const QString &transactionId, int accountId) {
QJsonObject errorData;
errorData["status"] = QStringLiteral("error");
errorData["event_type"] = type;
@ -224,18 +224,36 @@ static void sendTaskError(const QString &type, const QString &materialId, const
errorData["transaction_id"] = transactionId;
sendTaskResult(QStringLiteral("ERROR"), materialId, bankName, errorData, eventId);
if (!screenshot.isEmpty()) {
const QString apiToken = SettingsDAO::get("api_key");
// Шлём в мониторинг всегда — даже без скриншота (offline / expired /
// disabled / account not found и т.п.), чтобы такие ошибки не «молчали» на
// релее. Скриншот прикладываем, если он есть.
// api-токен общий на нескольких операторов — по нему не отличить, чей
// профиль упал. Резолвим банковский профиль по accountId и пишем его
// серверный UUID + материал + телефон профиля.
QString profileUuid, profilePhone;
if (accountId > 0) {
const MaterialInfo acc = MaterialDAO::getAccountById(accountId);
if (acc.id != -1) {
const BankProfileInfo app = BankProfileDAO::getApplication(acc.deviceId, acc.appCode);
profileUuid = app.bankProfileId;
profilePhone = app.phone;
}
}
const QString desktopId = SettingsDAO::get("desktop_id");
QStringList lines;
lines << QStringLiteral("<b>[TASK_ERROR] [%1] [%2]</b>")
.arg(type.toHtmlEscaped(), bankName.toUpper().toHtmlEscaped());
if (!apiToken.isEmpty())
lines << QStringLiteral("<b>[%1]</b>").arg(apiToken.toHtmlEscaped());
lines << QStringLiteral("<b>[%1]</b>").arg(materialId.toHtmlEscaped());
lines << description.toHtmlEscaped();
if (!desktopId.isEmpty())
lines << QStringLiteral("Desktop: %1").arg(desktopId.toHtmlEscaped());
if (!profileUuid.isEmpty())
lines << QStringLiteral("Profile: %1").arg(profileUuid.toHtmlEscaped());
if (!materialId.isEmpty())
lines << QStringLiteral("Material: %1").arg(materialId.toHtmlEscaped());
if (!profilePhone.isEmpty())
lines << QStringLiteral("Phone: %1").arg(profilePhone.toHtmlEscaped());
lines << QStringLiteral("Error: %1").arg(description.toHtmlEscaped());
const QString message = lines.join(QStringLiteral("\n"));
sendMonitoringLog("error", "task_error", message, screenshot, eventId);
}
// application.log + БД отправляются из TaskDumpRecorder::finishInternal,
// чтобы покрывать также non-event-driven задачи (login flow и т.п.).
@ -359,7 +377,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
.arg(eventTypeToString(event.type), deviceLabel, event.bankName, result);
GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId);
if (event.type == EventType::CreateTransaction) {
const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);
@ -750,7 +768,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn
const QString logMsg = QString("[%1] bank=%2 material=%3: Script timeout")
.arg(eventTypeToString(event.type), event.bankName, event.externalId);
GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Script timeout (5 min)", {}, -1, event.transactionId);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Script timeout (5 min)", {}, -1, event.transactionId, event.accountId);
EventDAO::markSentToServer(event.id);
// Убиваем банковское приложение на устройстве после таймаута
@ -787,7 +805,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
if (waitSecs > 5 * 60) {
qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id;
EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s");
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, -1, event.transactionId);
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, -1, event.transactionId, event.accountId);
continue;
}
@ -797,7 +815,7 @@ void EventHandler::processNextTask(const QString &deviceId) {
qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId;
EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled");
sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName,
"Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId);
"Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId, event.accountId);
continue;
}

View File

@ -90,16 +90,22 @@ QByteArray buildArchive() {
} // namespace
bool LogArchiveSender::dispatch(const QString &label, int eventId) {
bool LogArchiveSender::dispatch(const QString &label, int eventId, const QString &extraCaptionHtml) {
const QString apiToken = SettingsDAO::get("api_key");
const QDateTime now = QDateTime::currentDateTime();
const QString timestamp = now.toString(Qt::ISODate);
// api-токен в caption больше не пишем (общий на нескольких операторов);
// идентификация идёт по Profile/Material/Phone из extraCaptionHtml.
const QString desktopId = SettingsDAO::get("desktop_id");
QStringList head{QStringLiteral("[LOGS]")};
if (!apiToken.isEmpty()) head << QStringLiteral("[%1]").arg(apiToken.toHtmlEscaped());
if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped());
const QString text = QStringLiteral("<b>%1</b>\n%2")
QString text = QStringLiteral("<b>%1</b>\n%2")
.arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped());
if (!desktopId.isEmpty())
text += QStringLiteral("\nDesktop: %1").arg(desktopId.toHtmlEscaped());
if (!extraCaptionHtml.isEmpty())
text += QStringLiteral("\n") + extraCaptionHtml;
QString tokenSafe = apiToken;
tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_");

View File

@ -10,7 +10,10 @@ namespace LogArchiveSender {
// в собственных background-потоках (worker для zip + thread для HTTP).
// `label` — короткий тег ("TASK_ERROR" и т.п.), попадает в caption и в имя файла.
// `eventId` (если > 0) тоже уходит в имя файла для трассировки.
// `extraCaptionHtml` (если не пусто) — дополнительные строки caption'а
// (Profile/Material/Phone), уже экранированные; нужны чтобы по общему
// api-токену различать операторов.
// Возвращает true если задача поставлена в очередь; реальный результат отправки
// не отслеживается (fire-and-forget, как у sendMonitoringLog).
bool dispatch(const QString &label, int eventId = -1);
bool dispatch(const QString &label, int eventId = -1, const QString &extraCaptionHtml = {});
}