[CrashReporting] Mock dialog, sentry skeleton and CI prerequisites

Mock: DlgCrashReport preview dialog (no account needed, redacted log
preview), CrashReportingMode Ask/Always/Never setting in DebugSettings,
--simulate-crash flag and Help-menu preview entry. Send writes a local
mock JSON only.

Vendor sentry-native 0.17.3 via FetchContent behind
WITH_CRASH_REPORTING (breakpad everywhere, backend none under
sanitizers) plus a CrashReporter facade with early init and post-loop
shutdown. DSN is a placeholder: empty CMake default with
COCKATRICE_SENTRY_DSN env override, so SDK init is skipped and mock
behavior is unchanged until a real DSN is configured.

CI prerequisites riding here so every layer below builds green:
libcurl dev plus CA bundles in the Linux images and CodeQL env, sentry
Qt integration off (its own find_package cannot see the macOS thin Qt
layout), and the StrFunc Using construction makensis requires.

Tests: ctest green, format clean, code plus UI gates PASS.
This commit is contained in:
Lukas Brübach 2026-10-14 08:53:41 +02:00 • committed by GitHub
parent ea6af0048c
commit 6411abfb57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 962 additions and 5 deletions

View file

@ -0,0 +1,152 @@
#include "dlg_crash_report.h"
#include "../../../client/crash/crash_reporter.h"
#include "../../../client/settings/cache_settings.h"
#include "../../logger.h"
#include <QCheckBox>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QEvent>
#include <QFile>
#include <QHBoxLayout>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QList>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <libcockatrice/settings/personal_settings.h>
DlgCrashReport::DlgCrashReport(QWidget *parent) : QDialog(parent)
{
infoLabel = new QLabel(this);
infoLabel->setWordWrap(true);
accountLabel = new QLabel(this);
accountLabel->setWordWrap(true);
previewArea = new QPlainTextEdit(this);
previewArea->setReadOnly(true);
previewArea->setPlainText(buildRedactedPreview());
rememberBox = new QCheckBox(this);
sendButton = new QPushButton(this);
sendButton->setDefault(true);
connect(sendButton, &QPushButton::clicked, this, &DlgCrashReport::actSend);
dontSendButton = new QPushButton(this);
dontSendButton->setAutoDefault(false);
connect(dontSendButton, &QPushButton::clicked, this, &DlgCrashReport::actDontSend);
auto *buttonLayout = new QHBoxLayout;
buttonLayout->addWidget(rememberBox);
buttonLayout->addStretch();
buttonLayout->addWidget(dontSendButton);
buttonLayout->addWidget(sendButton);
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(infoLabel);
mainLayout->addWidget(accountLabel);
mainLayout->addWidget(previewArea);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
resize(700, 500);
retranslateUi();
}
void DlgCrashReport::changeEvent(QEvent *event)
{
if (event->type() == QEvent::LanguageChange) {
retranslateUi();
}
QDialog::changeEvent(event);
}
void DlgCrashReport::retranslateUi()
{
setWindowTitle(tr("Send crash report"));
infoLabel->setText(
tr("Cockatrice closed unexpectedly. The preview below shows what a crash report would contain (mock)."));
accountLabel->setText(tr("No account needed to send a crash report."));
rememberBox->setText(tr("Remember my choice"));
sendButton->setText(tr("Send"));
dontSendButton->setText(tr("Don't send"));
}
QString DlgCrashReport::buildRedactedPreview()
{
Logger &logger = Logger::getInstance();
QStringList header;
header.append(logger.getClientVersion());
header.append(logger.getSystemArchitecture());
header.append(logger.getSystemLocale());
header.append(logger.getClientInstallInfo());
header.append(QStringLiteral("---"));
QList<QString> logLines = logger.getPreviousSessionTail();
if (logLines.isEmpty()) {
logLines = logger.getLogBuffer();
}
const int previewLines = 60;
int start = logLines.size() > previewLines ? logLines.size() - previewLines : 0;
QStringList lines = header;
for (int i = start; i < logLines.size(); ++i) {
lines.append(CrashReporter::redactCockatriceUrls(CrashReporter::sanitizeLogLine(logLines.at(i))));
}
return lines.join(QStringLiteral("\n"));
}
QString DlgCrashReport::writeMockReport(const QString &previewText)
{
Logger &logger = Logger::getInstance();
QJsonObject report;
report[QStringLiteral("mock")] = true;
report[QStringLiteral("timestamp")] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
report[QStringLiteral("clientVersion")] = logger.getClientVersion();
report[QStringLiteral("systemArchitecture")] = logger.getSystemArchitecture();
report[QStringLiteral("systemLocale")] = logger.getSystemLocale();
report[QStringLiteral("installInfo")] = logger.getClientInstallInfo();
report[QStringLiteral("logPreview")] = previewText;
QString fileName = QStringLiteral("crash-report-mock-%1.json")
.arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss")));
QString filePath = QDir::tempPath() + QStringLiteral("/") + fileName;
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return QString();
}
file.write(QJsonDocument(report).toJson());
file.close();
return filePath;
}
void DlgCrashReport::actSend()
{
if (rememberBox->isChecked()) {
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend);
}
const QString savedPath = writeMockReport(previewArea->toPlainText());
if (savedPath.isEmpty()) {
qWarning() << "Could not save the mock crash report locally";
} else {
qInfo() << "Mock crash report saved locally to" << savedPath;
}
accept();
}
void DlgCrashReport::actDontSend()
{
if (rememberBox->isChecked()) {
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend);
}
reject();
}

View file

@ -0,0 +1,51 @@
/**
* @file dlg_crash_report.h
* @ingroup Dialogs
*/
#ifndef DLG_CRASH_REPORT_H
#define DLG_CRASH_REPORT_H
#include <QDialog>
#include <QString>
#include <qtmetamacros.h>
class QCheckBox;
class QLabel;
class QPlainTextEdit;
class QPushButton;
/**
* @brief MOCK crash-report dialog (UI + settings only, no network).
*
* The Send path writes a redacted JSON file to the temp dir instead of
* uploading anything. Real Sentry upload comes in a later phase.
*/
class DlgCrashReport : public QDialog
{
Q_OBJECT
public:
explicit DlgCrashReport(QWidget *parent = nullptr);
static QString buildRedactedPreview();
static QString writeMockReport(const QString &previewText);
protected:
void changeEvent(QEvent *event) override;
private:
QLabel *infoLabel;
QLabel *accountLabel;
QPlainTextEdit *previewArea;
QCheckBox *rememberBox;
QPushButton *sendButton;
QPushButton *dontSendButton;
void retranslateUi();
private slots:
void actSend();
void actDontSend();
};
#endif

View file

@ -133,6 +133,28 @@ GeneralSettingsPage::GeneralSettingsPage()
versionGroupBox = new QGroupBox;
versionGroupBox->setLayout(versionGrid);
// crash reporting consent (lives in personal.ini, not debug.ini): Ask shows the crash dialog
// after an unclean exit, AlwaysSend uploads without asking, NeverSend stays silent.
crashReportingBox.addItem(""); // texts set in retranslateUi
crashReportingBox.addItem("");
crashReportingBox.addItem("");
crashReportingBox.setCurrentIndex(static_cast<int>(settings.personal().getCrashReportingMode()));
connect(&crashReportingBox, qOverload<int>(&QComboBox::currentIndexChanged), this, [](int index) {
if (index < 0 || index > static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend)) {
return;
}
SettingsCache::instance().personal().setCrashReportingMode(
static_cast<PersonalSettings::CrashReportingMode>(index));
});
auto *crashReportingGrid = new QGridLayout;
crashReportingGrid->addWidget(&crashReportingLabel, 0, 0);
crashReportingGrid->addWidget(&crashReportingBox, 0, 1);
crashReportingGroupBox = new QGroupBox;
crashReportingGroupBox->setLayout(crashReportingGrid);
// card database settings
startupCardUpdateCheckBehaviorSelector.addItem(""); // these will be set in retranslateUI
startupCardUpdateCheckBehaviorSelector.addItem("");
@ -336,6 +358,7 @@ GeneralSettingsPage::GeneralSettingsPage()
auto *mainLayout = new QVBoxLayout;
mainLayout->addWidget(languageGroupBox);
mainLayout->addWidget(versionGroupBox);
mainLayout->addWidget(crashReportingGroupBox);
mainLayout->addWidget(cardDatabaseGroupBox);
mainLayout->addWidget(startupGroupBox);
mainLayout->addWidget(pathsGroupBox);
@ -569,13 +592,19 @@ void GeneralSettingsPage::retranslateUi()
tr("English and selected card language"));
advertiseTranslationPageLabel.setText(
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
versionGroupBox->setTitle(tr("Version settings"));
updateReleaseChannelLabel.setText(tr("Update channel"));
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
crashReportingGroupBox->setTitle(tr("Crash reporting"));
crashReportingLabel.setText(tr("Send crash reports:"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::Ask), tr("Ask every time"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::AlwaysSend),
tr("Always send"));
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend), tr("Never send"));
// We can't change the strings after they're put into the QComboBox, so this is our workaround
int oldIndex = updateReleaseChannelBox.currentIndex();
updateReleaseChannelBox.clear();

View file

@ -49,6 +49,7 @@ private slots:
private:
QGroupBox *languageGroupBox;
QGroupBox *versionGroupBox;
QGroupBox *crashReportingGroupBox;
QGroupBox *cardDatabaseGroupBox;
QGroupBox *startupGroupBox;
QGroupBox *pathsGroupBox;
@ -70,6 +71,9 @@ private:
QCheckBox updateNotificationCheckBox;
QCheckBox newVersionOracleCheckBox;
QLabel crashReportingLabel;
QComboBox crashReportingBox;
QLabel startupCardUpdateCheckBehaviorLabel;
QComboBox startupCardUpdateCheckBehaviorSelector;
QLabel cardUpdateCheckIntervalLabel;