Commit a0c91d85 authored by Harald Sitter's avatar Harald Sitter 🏳️‍🌈
Browse files

add sentry support

this adds sentry support for evaluation. limited to form-git builds and
explicit opt-ins. testing sentry was agreed upon a while ago on the
community mailing list. the hope is that it will improve our crash
handling experience enough to potentially replace bugzilla as crash
tracking system.
parent d2e15726
Pipeline #199763 passed with stage
in 1 minute and 27 seconds
......@@ -23,3 +23,4 @@ Dependencies:
'frameworks/kwindowsystem': '@latest'
'frameworks/solid': '@latest'
'frameworks/syntax-highlighting': '@latest'
'libraries/kuserfeedback': '@stable'
......@@ -26,6 +26,10 @@ include(KDEClangFormat)
include(KDEGitCommitHooks)
include(ECMDeprecationSettings)
include(ECMFindQmlModule)
include(ECMSourceVersionControl)
include(CMakeDependentOption)
option(WITH_SENTRY "Submit crashes to KDE's Sentry instance" ${ECM_SOURCE_UNDER_VERSION_CONTROL})
kde_enable_exceptions()
......@@ -53,6 +57,9 @@ find_package(
SyntaxHighlighting
)
find_package(KUserFeedback)
set_package_properties(KUserFeedback PROPERTIES TYPE REQUIRED PURPOSE "Checking whether feedback is enabled or not")
ecm_find_qmlmodule(org.kde.kirigami 2.19)
ecm_find_qmlmodule(org.kde.kitemmodels 1.0)
ecm_find_qmlmodule(org.kde.kcm 1.6)
......
......@@ -67,6 +67,7 @@ set(drkonqi_SRCS
bugzillaintegration/productmapping.cpp
bugzillaintegration/parsebugbacktraces.cpp
bugzillaintegration/duplicatefinderjob.cpp
bugzillaintegration/sentrybeacon.cpp
)
ecm_qt_declare_logging_category(
......@@ -102,6 +103,7 @@ target_link_libraries(
drkonqi_backtrace_parser
qbugzilla
KF5::Declarative
KUserFeedbackCore
)
if(Systemd_FOUND)
......
......@@ -3,13 +3,18 @@
*
* SPDX-FileCopyrightText: 2000-2003 Hans Petter Bieker <bieker@kde.org>
* SPDX-FileCopyrightText: 2009 George Kiagiadakis <gkiagia@users.sourceforge.net>
* SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
* SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*****************************************************************/
#include "backtracegenerator.h"
#include "config-drkonqi.h"
#include "drkonqi.h"
#include "drkonqi_debug.h"
#include <QTemporaryDir>
#include <KProcess>
#include <KShell>
......@@ -176,6 +181,23 @@ void BacktraceGenerator::setBackendPrepared()
m_proc = new KProcess;
m_proc->setEnv(QStringLiteral("LC_ALL"), QStringLiteral("C")); // force C locale
// Temporary directory for the preeamble.py to write data into, we can then conveniently pick it up from there.
// Only useful for data that is not meant to appear in the trace (e.g. sentry payloads).
if (!m_tempDirectory) {
m_tempDirectory = std::make_unique<QTemporaryDir>();
}
if (!m_tempDirectory->isValid()) {
qCWarning(DRKONQI_LOG) << "Failed to create temporary directory for generator!";
} else {
#ifdef WITH_SENTRY
m_proc->setEnv(QStringLiteral("DRKONQI_WITH_SENTRY"), QStringLiteral("1"));
#endif
m_proc->setEnv(QStringLiteral("DRKONQI_TMP_DIR"), m_tempDirectory->path());
m_proc->setEnv(QStringLiteral("DRKONQI_VERSION"), QStringLiteral(PROJECT_VERSION));
m_proc->setEnv(QStringLiteral("DRKONQI_APP_VERSION"), DrKonqi::appVersion());
m_proc->setEnv(QStringLiteral("DRKONQI_SIGNAL"), QString::number(DrKonqi::signal()));
}
m_temp = new QTemporaryFile;
m_temp->open();
m_temp->write(m_debugger.backtraceBatchCommands().toLatin1());
......@@ -218,3 +240,14 @@ QString BacktraceGenerator::debuggerName() const
{
return m_debugger.displayName();
}
QByteArray BacktraceGenerator::sentryPayload() const
{
const QString sentryPayloadFile = m_tempDirectory->path() + QLatin1String("/sentry_payload.json");
QFile file(sentryPayloadFile);
if (!file.open(QFile::ReadOnly)) {
qCWarning(DRKONQI_LOG) << "Could not open sentry payload file" << sentryPayloadFile;
return {};
}
return file.readAll();
};
......@@ -2,7 +2,7 @@
* drkonqi - The KDE Crash Handler
*
* SPDX-FileCopyrightText: 2000-2003 Hans Petter Bieker <bieker@kde.org>
* SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
* SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*****************************************************************/
......@@ -10,6 +10,8 @@
#ifndef BACKTRACEGENERATOR_H
#define BACKTRACEGENERATOR_H
#include <memory>
#include <QProcess>
#include <QTemporaryFile>
......@@ -17,6 +19,7 @@
class KProcess;
class BacktraceParser;
class QTemporaryDir;
class BacktraceGenerator : public QObject
{
......@@ -56,6 +59,7 @@ public:
Q_INVOKABLE bool debuggerIsGDB() const;
Q_INVOKABLE QString debuggerName() const;
QByteArray sentryPayload() const;
public Q_SLOTS:
void start();
......@@ -82,6 +86,7 @@ private:
State m_state = NotLoaded;
BacktraceParser *m_parser = nullptr;
QString m_parsedBacktrace;
std::unique_ptr<QTemporaryDir> m_tempDirectory;
#ifdef BACKTRACE_PARSER_DEBUG
BacktraceParser *m_debugParser = nullptr;
......
......@@ -10,7 +10,11 @@
#include "reportinterface.h"
#include <chrono>
#include <KIO/TransferJob>
#include <KLocalizedString>
#include <KUserFeedback/Provider>
#include "backtracegenerator.h"
#include "bugzillalib.h"
......@@ -18,8 +22,10 @@
#include "crashedapplication.h"
#include "debuggermanager.h"
#include "drkonqi.h"
#include "drkonqi_debug.h"
#include "parser/backtraceparser.h"
#include "productmapping.h"
#include "sentrybeacon.h"
#include "systeminformation.h"
// Max size a report may have. This is enforced in bugzilla, hardcoded, and
......@@ -44,6 +50,21 @@ ReportInterface::ReportInterface(QObject *parent)
// Do not attach the bug report to any other existent report (create a new one)
m_attachToBugNumber = 0;
connect(&m_sentryBeacon, &SentryBeacon::eventSent, this, [this] {
m_sentryEventSent = true;
maybeDone();
});
connect(&m_sentryBeacon, &SentryBeacon::userFeedbackSent, this, [this] {
m_sentryUserFeedbackSent = true;
maybeDone();
});
if (KUserFeedback::Provider provider; provider.isEnabled()) {
metaObject()->invokeMethod(this, [this] {
// Send crash event ASAP, if applicable. Trace quality doesn't matter for it.
sendCrashEvent();
});
}
}
void ReportInterface::setBugAwarenessPageData(bool rememberSituation, Reproducible reproducible, bool actions, bool unusual, bool configuration)
......@@ -307,8 +328,38 @@ Bugzilla::NewBug ReportInterface::newBugReportTemplate() const
return bug;
}
void ReportInterface::sendCrashEvent()
{
#ifdef WITH_SENTRY
if (DrKonqi::debuggerManager()->backtraceGenerator()->state() == BacktraceGenerator::Loaded) {
m_sentryBeacon.sendEvent();
return;
}
static bool connected = false;
if (!connected) {
connected = true;
connect(DrKonqi::debuggerManager()->backtraceGenerator(), &BacktraceGenerator::done, this, [this] {
m_sentryBeacon.sendEvent();
});
}
if (DrKonqi::debuggerManager()->backtraceGenerator()->state() != BacktraceGenerator::Loading) {
DrKonqi::debuggerManager()->backtraceGenerator()->start();
}
#endif
}
void ReportInterface::sendCrashComment()
{
#ifdef WITH_SENTRY
m_sentryBeacon.sendUserFeedback(m_reportTitle + QLatin1Char('\n') + m_reportDetailText + QLatin1Char('\n') + DrKonqi::kdeBugzillaURL()
+ QLatin1String("show_bug.cgi?id=%1").arg(QString::number(m_sentReport)));
#endif
}
void ReportInterface::sendBugReport()
{
sendCrashEvent();
if (m_attachToBugNumber > 0) {
// We are going to attach the report to an existent one
connect(m_bugzillaManager, &BugzillaManager::addMeToCCFinished, this, &ReportInterface::attachBacktraceWithReport);
......@@ -345,7 +396,9 @@ void ReportInterface::sendBugReport()
m_attachToBugNumber = bugId;
attachBacktrace(QStringLiteral("DrKonqi auto-attaching complete backtrace."));
} else {
Q_EMIT reportSent(bugId);
m_sentReport = bugId;
sendCrashComment();
maybeDone();
}
});
connect(m_bugzillaManager, &BugzillaManager::sendReportError, this, &ReportInterface::sendReportError);
......@@ -389,7 +442,9 @@ void ReportInterface::attachSent(int attachId)
Q_UNUSED(attachId);
// The bug was attached, consider it "sent"
Q_EMIT reportSent(m_attachToBugNumber);
m_sentReport = attachId;
sendCrashComment();
maybeDone();
}
QStringList ReportInterface::relatedBugzillaProducts() const
......@@ -476,3 +531,10 @@ ProductMapping *ReportInterface::productMapping() const
{
return m_productMapping;
}
void ReportInterface::maybeDone()
{
if (m_sentReport != 0 && m_sentryEventSent && m_sentryUserFeedbackSent) {
Q_EMIT done();
}
};
......@@ -14,6 +14,8 @@
#include <QObject>
#include <QStringList>
#include "sentrybeacon.h"
namespace Bugzilla
{
class NewBug;
......@@ -40,6 +42,8 @@ class ReportInterface : public QObject
Q_PROPERTY(uint attachToBugNumber READ attachToBugNumber WRITE setAttachToBugNumber NOTIFY attachToBugNumberChanged)
Q_PROPERTY(uint duplicateId READ duplicateId WRITE setDuplicateId NOTIFY duplicateIdChanged)
Q_PROPERTY(uint sentReport MEMBER m_sentReport NOTIFY done)
public:
enum Reproducible {
ReproducibleUnsure,
......@@ -124,6 +128,8 @@ public:
}
public Q_SLOTS:
void sendCrashEvent();
void sendCrashComment();
void sendBugReport();
private Q_SLOTS:
......@@ -133,7 +139,7 @@ private Q_SLOTS:
void attachSent(int);
Q_SIGNALS:
void reportSent(int);
void done();
void sendReportError(const QString &);
void provideUnusualBehaviorChanged();
......@@ -141,6 +147,8 @@ private:
// Attach backtrace to bug. Only used internally when the comment isn't
// meant to be the full report.
void attachBacktrace(const QString &comment);
void sendToSentry();
void maybeDone();
QString generateAttachmentComment() const;
......@@ -165,6 +173,11 @@ private:
ProductMapping *m_productMapping = nullptr;
BugzillaManager *m_bugzillaManager = nullptr;
SentryBeacon m_sentryBeacon;
bool m_sentryEventSent = false;
bool m_sentryUserFeedbackSent = false;
uint m_sentReport = 0;
};
#endif
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#include "sentrybeacon.h"
#include "backtracegenerator.h"
#include "crashedapplication.h"
#include "debuggermanager.h"
#include "drkonqi.h"
#include "drkonqi_debug.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QTemporaryDir>
void SentryBeacon::sendEvent()
{
if (m_started) {
return;
}
m_started = true;
// We grab the payload here to prevent data races, it could change between now and when the actual submission happens.
m_eventPayload = DrKonqi::debuggerManager()->backtraceGenerator()->sentryPayload();
m_manager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
getDSNs();
}
void SentryBeacon::sendUserFeedback(const QString &feedback)
{
m_userFeedback = feedback;
if (!m_eventID.isNull() && !m_userFeedback.isEmpty()) {
postUserFeedback();
} else {
Q_EMIT userFeedbackSent();
}
}
void SentryBeacon::getDSNs()
{
QNetworkRequest request(QUrl::fromUserInput(QStringLiteral("https://errors-eval.kde.org/_drkonqi_static/0/dsns.json")));
auto reply = m_manager->get(request);
connect(reply, &QNetworkReply::finished, this, &SentryBeacon::onDSNsReceived);
}
void SentryBeacon::onDSNsReceived()
{
auto reply = qobject_cast<QNetworkReply *>(sender());
reply->deleteLater();
const auto application = DrKonqi::crashedApplication()->fakeExecutableBaseName();
const auto document = QJsonDocument::fromJson(reply->readAll());
const auto object = document.object();
qDebug() << document << document.isObject() << object;
if (maybePostStore(object.value(application))) {
return;
}
qCWarning(DRKONQI_LOG) << "Failed to post to application" << application;
if (maybePostStore(object.value(QStringLiteral("fallthrough")))) {
return;
}
qCWarning(DRKONQI_LOG) << "Failed to post to dynamic fallthrough";
// final fallback is a hardcoded fallthrough, this isn't ideal because we can't change this after releases
postStore({QStringLiteral("fallthrough"), QStringLiteral("456f53a71a074438bbb786d6add63241"), QStringLiteral("11")});
}
bool SentryBeacon::maybePostStore(const QJsonValue &value)
{
if (value.isObject()) {
const auto object = value.toObject();
postStore({
object.value(QStringLiteral("project")).toString(),
object.value(QStringLiteral("key")).toString(),
object.value(QStringLiteral("index")).toString(),
});
return true;
}
return false;
}
void SentryBeacon::postStore(const SentryDSNContext &context)
{
m_context = context;
// https://develop.sentry.dev/sdk/store/
QNetworkRequest request(QUrl::fromUserInput(QStringLiteral("https://%1@errors-eval.kde.org/api/%2/store/").arg(context.key, context.index)));
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("DrKonqi"));
request.setRawHeader(QByteArrayLiteral("X-Sentry-Auth"),
QStringLiteral("Sentry sentry_version=7,sentry_timestamp=%1,sentry_client=sentry-curl/1.0,sentry_key=%2")
.arg(QString::number(std::time(nullptr)), context.key)
.toUtf8());
auto reply = m_manager->post(request, m_eventPayload);
connect(reply, &QNetworkReply::finished, this, &SentryBeacon::onStoreSent);
}
void SentryBeacon::onStoreSent()
{
auto reply = qobject_cast<QNetworkReply *>(sender());
reply->deleteLater();
const auto replyBlob = QJsonDocument::fromJson(reply->readAll());
m_eventID = replyBlob.object().value(QStringLiteral("id"));
Q_EMIT eventSent();
if (!m_userFeedback.isEmpty()) { // a feedback was set in the meantime, apply it; otherwise we wait for sendFeedback()
postUserFeedback();
} else {
Q_EMIT userFeedbackSent();
}
}
void SentryBeacon::postUserFeedback()
{
qDebug() << m_context.key << m_context.index;
const QJsonObject feedbackObject = {
{QStringLiteral("event_id"), QJsonValue::fromVariant(m_eventID)},
{QStringLiteral("name"), QStringLiteral("Anonymous")},
{QStringLiteral("email"), QStringLiteral("anonymous@kde.org")},
{QStringLiteral("comments"), m_userFeedback},
};
// TODO we could back reference the bug report, but that needs some reshuffling
// https://docs.sentry.io/api/projects/submit-user-feedback/
QNetworkRequest request(QUrl::fromUserInput(QStringLiteral("https://errors-eval.kde.org/api/0/projects/kde/%1/user-feedback/").arg(m_context.project)));
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("DrKonqi"));
request.setRawHeader(QByteArrayLiteral("Authorization"),
QStringLiteral("DSN https://%1@errors-eval.kde.org/%2").arg(m_context.key, m_context.index).toUtf8());
auto feedbackReply = m_manager->post(request, QJsonDocument(feedbackObject).toJson());
connect(feedbackReply, &QNetworkReply::finished, this, &SentryBeacon::onUserFeedbackSent);
}
void SentryBeacon::onUserFeedbackSent()
{
auto reply = qobject_cast<QNetworkReply *>(sender());
reply->deleteLater();
qDebug() << reply->readAll();
Q_EMIT userFeedbackSent();
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#pragma once
#include <QNetworkAccessManager>
#include <QObject>
#include <memory>
struct SentryDSNContext {
QString project;
QString key;
QString index;
};
class SentryBeacon : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
void sendEvent();
void sendUserFeedback(const QString &feedback);
Q_SIGNALS:
void eventSent();
void userFeedbackSent();
private Q_SLOTS:
void getDSNs();
void onDSNsReceived();
bool maybePostStore(const QJsonValue &value);
void postStore(const SentryDSNContext &context);
void onStoreSent();
void postUserFeedback();
void onUserFeedbackSent();
private:
QString m_userFeedback;
std::unique_ptr<QNetworkAccessManager> m_manager = std::make_unique<QNetworkAccessManager>();
SentryDSNContext m_context;
QByteArray m_eventPayload;
QVariant m_eventID;
bool m_started = false;
};
......@@ -4,3 +4,5 @@
#define DEBUG_PACKAGE_INSTALLER_NAME "@DEBUG_PACKAGE_INSTALLER_NAME@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
#cmakedefine01 WITH_SENTRY
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
# SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
from typing import Mapping
import gdb
from gdb.FrameDecorator import FrameDecorator
from datetime import datetime
import distro
import uuid
import os
import json
import subprocess
import signal
import re
import binascii
import platform
import psutil
import multiprocessing
if os.getenv('DRKONQI_WITH_SENTRY'):
try:
import sentry_sdk
sentry_sdk.init(
dsn="https://d6d53bb0121041dd97f59e29051a1781@errors-eval.kde.org/13",
traces_sample_rate=1.0,
release="drkonqi@" + os.getenv('DRKONQI_VERSION'),
)
except ImportError:
print("python sentry-sdk not installed :(")
class SentryQMLThread:
def __init__(self):
self.payload = None
# should we iterate the inferiors? Probably makes no diff for 99% of apps.
for thread in gdb.selected_inferior().threads():
if not thread.is_valid() :
continue
thread.switch()
if gdb.selected_thread() != thread:
continue # failed to switch :shrug:
try:
frame = gdb.newest_frame()
except gdb.error:
pass
while frame:
ret = qml_trace_frame(frame)
if ret:
self.payload = ret
break
try:
frame = frame.older()
except gdb.error:
pass
def to_sentry_frame(self, frame):
print("level={level} func={func} at={file}:{line}".format(**frame) )
return {
'platform': 'other', # always different from the cpp/native frames. alas, technically this frame isn't a javascript frame
'filename': frame['file'],
'function': frame['func'],
'lineno': int(frame['line']),
'in_app': True # qml is always in the app I should think
}
def to_sentry_frames(self, frames):
lst = []
for frame in frames:
data = self.to_sentry_frame(frame)
if not data:
continue
lst.append(data)
return lst
def to_dict(self):
if not self.payload:
return None
payload = self.payload
from pygdbmi import gdbmiparser
result = gdbmiparser.parse_response("*stopped," + payload)
frames = result['payload']['frame']
print(frames)
if type(frames) is dict: # single frames traces aren't arrays to make it more fun -.-
frames = [frames]
lst = self.to_sentry_frames(frames)
print(lst)
if lst:
return {
'id': 'QML', # docs say this is typically a number to there is indeed no enforcement it seems
'name': 'QML',
'crashed': True,
'stacktrace': {
'frames': self.to_sentry_frames(frames)
}
}
return None
def to_list(self):
data = self.to_dict()
if data:
return [data]
return []