mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[CrashReporting] Sentry send path with consent gating (#7522)
* [CrashReporting] Real send path with consent gating (DSN still placeholder)
require_user_consent + give/revoke around the Ask dialog, message event
with in-memory crash-log.txt bytes attachment, before_send scrubs
message/logentry/breadcrumbs/data of cockatrice:// secrets. Failure box
offers Save (shows path) / Discard (revokes + rejects) / Cancel (back
to dialog); reject() override so X/Escape cannot bypass revocation;
silent AlwaysSend path falls back to a local file. Empty DSN keeps the
previous mock behavior byte-identical.
Tests: 528/528 ctest green, format clean, code + UI gates PASS.
* [CrashReporting] Attach crash log and user note to the pending crash event
The redacted log used to ride a separate message event from the native
crash (minidump + stack), forcing manual correlation in Sentry. Fold it
into the pending crash envelope(s) instead by appending well-formed
attachment items before consent releases them: sentry-native replays
cached envelopes byte-for-byte (raw read, raw serialize in 0.17.3), so
the appended crash-log.txt and user-note.txt items upload with the very
event that already carries the dump and stack.
Covers both storage locations: Ask mode's consent-held files in
<database>/cache and AlwaysSend's transport-dumped files in
<database>/<run>.run, which must be attached before sentry_init replays
them. When no pending crash event exists (unclean exit without a native
crash) the standalone message event remains as fallback, now also
carrying the optional note.
The dialog gains an optional free-text field ('Here's what I was
doing...') that travels as user-note.txt; the mock JSON records it too.
* [CrashReporting] Send PROJECT_VERSION as the Sentry release
main() reported VERSION_STRING (PROJECT_VERSION_FRIENDLY) as the Sentry
release — that carries a build date, so every build of the same version
landed in its own release, and it never matched the installer, which
reports CPACK/PROJECT_VERSION. Bake COCKATRICE_SENTRY_RELEASE=
PROJECT_VERSION instead: date-free, identical between client and
installer, and derived from the git tag on tag builds, so events,
symbol uploads and the installer log all group under one release.
* [CrashReporting] Friendlier crash dialog copy
Reword the report dialog along a single friendly arc: headline ('Oops,
we crashed — and now we need your help to make Cockatrice better!'),
why helping is easy, exactly what gets collected (crash dump with stack
trace, app version, OS info, redacted log excerpt), the value of each
report ('better and less crashy for everyone'), and the consent choice
('Send and remember makes this automatic without interruption in the
future'). The checkbox itself stays direction-neutral ('Remember my
choice') because it also persists a 'Don't send' decision.
* [CrashReporting] Log where crash reports go and warn when they cannot
The client resolved COCKATRICE_SENTRY_DSN (environment over the baked-in
value) without ever saying which one won, and sentry-native refuses to build
a request for a DSN it considers invalid without reporting anything, so a
tester pointing the client at a scratch project had nothing to go on when a
crash never appeared.
Say which DSN source won, and log the destination (host and project id, never
the key) once the SDK is armed. Warn instead of staying quiet when no DSN is
configured at all, when a DSN is malformed - disabling reporting rather than
silently falling back to the project baked into the build, which is the rule
the installer already follows in cmake/CrashReporting.cmake - and when the
build was compiled without Sentry support. A deliberate Never-send stays at
info level: that is a choice, not a misconfiguration.
Also warn when a pending crash envelope was captured for a different
destination than the session that will upload it. sentry-native posts every
envelope with the DSN held in the transport state of the session doing the
sending, and envelopes restored from disk are raw, so the "dsn" header of a
cached envelope is never read: a crash captured for one project can be
uploaded to another with nothing in the log to show it.
Tests cover the DSN shape check (same unanchored pattern the build matches, so
a DSN this build accepted is never rejected here), destination description
without the key, and the mismatch detection, which stays quiet for a different
key on the same project, unreadable files, and headers without a dsn.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
6385f3fab3
commit
ad0fd2d4b0
7 changed files with 965 additions and 47 deletions
|
|
@ -15,12 +15,14 @@
|
|||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QList>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
DlgCrashReport::DlgCrashReport(QWidget *parent) : QDialog(parent)
|
||||
DlgCrashReport::DlgCrashReport(QWidget *parent, bool _realSending, CrashReporter *_reporter)
|
||||
: QDialog(parent), realSending(_realSending), reporter(_reporter)
|
||||
{
|
||||
infoLabel = new QLabel(this);
|
||||
infoLabel->setWordWrap(true);
|
||||
|
|
@ -28,6 +30,13 @@ DlgCrashReport::DlgCrashReport(QWidget *parent) : QDialog(parent)
|
|||
accountLabel = new QLabel(this);
|
||||
accountLabel->setWordWrap(true);
|
||||
|
||||
// Constructed before previewArea: Qt derives the default Tab chain from construction
|
||||
// order, and note sits visually above the log excerpt, so this keeps Tab moving downward.
|
||||
noteArea = new QPlainTextEdit(this);
|
||||
// Optional free text ("what I was doing when it crashed") — a few lines are
|
||||
// enough for a description, leave the rest of the space to the log excerpt.
|
||||
noteArea->setMaximumHeight(70);
|
||||
|
||||
previewArea = new QPlainTextEdit(this);
|
||||
previewArea->setReadOnly(true);
|
||||
previewArea->setPlainText(buildRedactedPreview());
|
||||
|
|
@ -51,6 +60,7 @@ DlgCrashReport::DlgCrashReport(QWidget *parent) : QDialog(parent)
|
|||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(infoLabel);
|
||||
mainLayout->addWidget(accountLabel);
|
||||
mainLayout->addWidget(noteArea);
|
||||
mainLayout->addWidget(previewArea);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
setLayout(mainLayout);
|
||||
|
|
@ -70,9 +80,34 @@ void DlgCrashReport::changeEvent(QEvent *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."));
|
||||
if (realSending) {
|
||||
// The preview below prefers the crashed session's persisted log tail
|
||||
// (Logger::getPreviousSessionTail), falling back to this restart's in-memory buffer only
|
||||
// when no previous session was saved. On send it is attached as crash-log.txt to the
|
||||
// pending native crash event (dump + stack + log + note in one report); when no native
|
||||
// crash event exists, it rides the fallback message event instead.
|
||||
infoLabel->setText(tr("Oops, we crashed — and now we need your help to make Cockatrice better!\n\n"
|
||||
"The good thing is, helping is easy and convenient. Sending uploads what the "
|
||||
"previous run left behind when it crashed: the crash dump with the stack trace, "
|
||||
"the app version and operating system info, the redacted log excerpt shown below, "
|
||||
"and anything you write in the note box. If no log from the crashed session was "
|
||||
"saved, this restart's early logs are shown instead.\n\n"
|
||||
"Reports are what let us fix crashes like this one. Check \u201cRemember my choice\u201d "
|
||||
"and Send, and future crash reports are sent automatically without interruption."));
|
||||
} else {
|
||||
infoLabel->setText(tr("(Mock preview — nothing will be uploaded.)\n\n"
|
||||
"Cockatrice closed unexpectedly. A real report would contain what the previous run "
|
||||
"left behind when it crashed: the crash dump with the stack trace, the app version "
|
||||
"and operating system info, the redacted log excerpt shown below, and anything you "
|
||||
"write in the note box. If no log from the crashed session was saved, this restart's "
|
||||
"early logs are shown instead.\n\n"
|
||||
"A mock file is written locally for inspection."));
|
||||
}
|
||||
accountLabel->setText(tr("No account needed, and no contact info is collected. Login links "
|
||||
"(cockatrice://...) are scrubbed before sending."));
|
||||
noteArea->setPlaceholderText(tr("(Optional) Describe what you were doing when it crashed"));
|
||||
// Direction-neutral on purpose: the same box also remembers a "Don't send" decision
|
||||
// (reject() persists NeverSend when it is checked), so it must not promise sending.
|
||||
rememberBox->setText(tr("Remember my choice"));
|
||||
sendButton->setText(tr("Send"));
|
||||
dontSendButton->setText(tr("Don't send"));
|
||||
|
|
@ -103,7 +138,7 @@ QString DlgCrashReport::buildRedactedPreview()
|
|||
return lines.join(QStringLiteral("\n"));
|
||||
}
|
||||
|
||||
QString DlgCrashReport::writeMockReport(const QString &previewText)
|
||||
QString DlgCrashReport::writeMockReport(const QString &previewText, const QString &userNote)
|
||||
{
|
||||
Logger &logger = Logger::getInstance();
|
||||
|
||||
|
|
@ -115,6 +150,7 @@ QString DlgCrashReport::writeMockReport(const QString &previewText)
|
|||
report[QStringLiteral("systemLocale")] = logger.getSystemLocale();
|
||||
report[QStringLiteral("installInfo")] = logger.getClientInstallInfo();
|
||||
report[QStringLiteral("logPreview")] = previewText;
|
||||
report[QStringLiteral("userNote")] = userNote;
|
||||
|
||||
QString fileName = QStringLiteral("crash-report-mock-%1.json")
|
||||
.arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss")));
|
||||
|
|
@ -134,7 +170,56 @@ void DlgCrashReport::actSend()
|
|||
if (rememberBox->isChecked()) {
|
||||
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend);
|
||||
}
|
||||
const QString savedPath = writeMockReport(previewArea->toPlainText());
|
||||
const QString preview = previewArea->toPlainText();
|
||||
const QString userNote = noteArea->toPlainText().trimmed();
|
||||
if (realSending && reporter != nullptr) {
|
||||
// Fold log and note into the pending native crash event(s) BEFORE consent:
|
||||
// giveConsent() releases the held envelopes for upload, so dump, stack,
|
||||
// log and note arrive as one Sentry event. Without a pending crash event
|
||||
// (unclean exit that produced no native crash) this returns 0 and
|
||||
// sendReport() falls back to a standalone message event with the same
|
||||
// attachments.
|
||||
const int attached = reporter->attachToPendingCrashEnvelopes(preview, userNote);
|
||||
reporter->giveConsent();
|
||||
if (attached > 0 || reporter->sendReport(preview, userNote)) {
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
qCWarning(CrashReportLog) << "Failed to send crash report";
|
||||
// Outcome mapping for the failure box: Save = store locally and close
|
||||
// as accepted (the follow-up info box shows the path, so the user
|
||||
// knows it was NOT sent); Discard = revoke consent and close as
|
||||
// rejected (dropped); Cancel/X = back to the dialog, neither sent nor
|
||||
// dropped. The Remember checkbox governs only the original
|
||||
// Send/Don't-send decision, so the Discard path below revokes and
|
||||
// closes via QDialog::reject() without touching the setting (calling
|
||||
// our reject() override here would wrongly persist NeverSend over the
|
||||
// AlwaysSend stored above when Remember was checked).
|
||||
QMessageBox failureBox(QMessageBox::Warning, tr("Send crash report"),
|
||||
tr("Sending the crash report failed. Save the report locally instead?"),
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, this);
|
||||
failureBox.setDefaultButton(QMessageBox::Save);
|
||||
failureBox.setEscapeButton(QMessageBox::Cancel);
|
||||
const QMessageBox::StandardButton choice = static_cast<QMessageBox::StandardButton>(failureBox.exec());
|
||||
if (choice == QMessageBox::Save) {
|
||||
const QString savedPath = writeMockReport(preview, userNote);
|
||||
if (savedPath.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Send crash report"), tr("Could not save the crash report locally."));
|
||||
} else {
|
||||
QMessageBox::information(this, tr("Send crash report"),
|
||||
tr("Crash report saved locally to:\n%1").arg(savedPath));
|
||||
}
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
if (choice == QMessageBox::Discard) {
|
||||
reporter->revokeConsent();
|
||||
QDialog::reject();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QString savedPath = writeMockReport(preview, userNote);
|
||||
if (savedPath.isEmpty()) {
|
||||
qCWarning(CrashReportLog) << "Could not save the mock crash report locally";
|
||||
} else {
|
||||
|
|
@ -144,9 +229,17 @@ void DlgCrashReport::actSend()
|
|||
}
|
||||
|
||||
void DlgCrashReport::actDontSend()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
|
||||
void DlgCrashReport::reject()
|
||||
{
|
||||
if (rememberBox->isChecked()) {
|
||||
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend);
|
||||
}
|
||||
reject();
|
||||
if (realSending && reporter != nullptr) {
|
||||
reporter->revokeConsent();
|
||||
}
|
||||
QDialog::reject();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,20 +15,29 @@ class QLabel;
|
|||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
|
||||
class CrashReporter;
|
||||
|
||||
/**
|
||||
* @brief MOCK crash-report dialog (UI + settings only, no network).
|
||||
* @brief Crash-report dialog: mock fallback or real Sentry send.
|
||||
*
|
||||
* The Send path writes a redacted JSON file to the temp dir instead of
|
||||
* uploading anything. Real Sentry upload comes in a later phase.
|
||||
* Mock mode (default): the Send path writes a redacted JSON file to the temp
|
||||
* dir instead of uploading anything. Real mode (realSending, with a live
|
||||
* CrashReporter): Send first folds the redacted log and the optional user note
|
||||
* into the pending native crash event(s), then gives consent so dump, stack,
|
||||
* log and note upload as one Sentry event; when no pending crash event exists
|
||||
* it falls back to a standalone message event with the same attachments.
|
||||
* Failure offers a local save instead.
|
||||
*/
|
||||
class DlgCrashReport : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgCrashReport(QWidget *parent = nullptr);
|
||||
explicit DlgCrashReport(QWidget *parent = nullptr, bool realSending = false, CrashReporter *_reporter = nullptr);
|
||||
|
||||
static QString buildRedactedPreview();
|
||||
static QString writeMockReport(const QString &previewText);
|
||||
static QString writeMockReport(const QString &previewText, const QString &userNote = QString());
|
||||
|
||||
void reject() override;
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
|
@ -37,9 +46,12 @@ private:
|
|||
QLabel *infoLabel;
|
||||
QLabel *accountLabel;
|
||||
QPlainTextEdit *previewArea;
|
||||
QPlainTextEdit *noteArea;
|
||||
QCheckBox *rememberBox;
|
||||
QPushButton *sendButton;
|
||||
QPushButton *dontSendButton;
|
||||
bool realSending = false;
|
||||
CrashReporter *reporter = nullptr;
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue