From 529b7cf5296f95b0854153c8f94fe15152c95490 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Thu, 23 Nov 2023 13:22:37 -0500
Subject: [PATCH] troubleshooting: add configurable crash reporting with
 crashpad

This commit adds a basic crash-report system that can be optionally
configured to automatically send minidump crash-reports in addition
to product versions and a platform description including the OS
name and CPU architecture. Reports can be received at a configured
REST endpoint(POST). This endpoint URL can be configured using
a CMake variable `CRASH_REPORT_URL` which defaults to
"http://localhost:8080/submit".

- Introduces a new CMake option `ENABLE_CRASHREPORTS`, defaulting
  to OFF. This allows developers to enable crash reporting features
  at build time selectively. We also define a new macro with the
  same name to expose the state to QML in order to hide the UI
  components if needed.

- Implemented conditional inclusion of crashpad dependencies using
  `ENABLE_CRASHREPORTS`. If set, `ENABLE_CRASHPAD` is also enabled
  (other crash reporters exist and we may want to use them).

- 2 new application settings are added: `EnableCrashReporting` and
  `EnableAutomaticCrashReporting`. Default settings make it so
  crash-reports are generated but not automatically sent. With this
  default configuration, users will be prompted upon application
  start to confirm the report upload. Additionally, users may
  opt-in in order to have reports sent automatically at crash-time.

Gitlab: #1454
Change-Id: I53edab2dae210240a99272479381695fce1e221b
---
 CMakeLists.txt                                |  86 ++++-
 .../example-submission-servers/.gitignore     |   9 +
 .../example-submission-servers/README.md      |  36 ++
 .../example-submission-servers/crashpad.py    |  51 +++
 .../requirements.txt                          |   5 +
 src/app/MainApplicationWindow.qml             |  16 +
 src/app/appsettingsmanager.cpp                |   3 +-
 src/app/appsettingsmanager.h                  |   5 +-
 src/app/commoncomponents/ConfirmDialog.qml    |  16 +-
 src/app/crashreportclient.h                   | 131 ++++++++
 src/app/crashreportclients/crashpad.cpp       | 311 ++++++++++++++++++
 src/app/crashreportclients/crashpad.h         |  47 +++
 src/app/crashreporter.h                       |  63 ++++
 src/app/mainapplication.cpp                   |  16 +-
 src/app/mainapplication.h                     |   7 +-
 src/app/net/jami/Constants/JamiStrings.qml    |   7 +-
 src/app/qmlregister.cpp                       |   1 +
 .../components/TroubleshootSettingsPage.qml   |  93 ++++--
 18 files changed, 840 insertions(+), 63 deletions(-)
 create mode 100644 extras/crash-reports/example-submission-servers/.gitignore
 create mode 100644 extras/crash-reports/example-submission-servers/README.md
 create mode 100644 extras/crash-reports/example-submission-servers/crashpad.py
 create mode 100644 extras/crash-reports/example-submission-servers/requirements.txt
 create mode 100644 src/app/crashreportclient.h
 create mode 100644 src/app/crashreportclients/crashpad.cpp
 create mode 100644 src/app/crashreportclients/crashpad.h
 create mode 100644 src/app/crashreporter.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 11f1603cb..c7342bca6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -50,9 +50,13 @@ if(ENABLE_ASAN AND NOT MSVC)
   set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
 endif()
 
+# Enable this option when building for production.
+option(ENABLE_CRASHREPORTS "Enable crash reports" OFF)
+
 # These values are exposed to QML and are better off being defined as values.
 define_macro_with_value(WITH_WEBENGINE)
 define_macro_with_value(APPSTORE)
+define_macro_with_value(ENABLE_CRASHREPORTS)
 
 # jami-core
 if(NOT WITH_DAEMON_SUBMODULE)
@@ -72,12 +76,6 @@ set(CLIENT_INCLUDE_DIRS, "")
 set(CLIENT_LINK_DIRS, "")
 set(CLIENT_LIBS, "")
 
-set(CMAKE_CXX_STANDARD 17)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-if(NOT MSVC)
-  set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
-endif()
-
 include(${PROJECT_SOURCE_DIR}/extras/build/cmake/contrib_tools.cmake)
 set(EXTRA_PATCHES_DIR ${PROJECT_SOURCE_DIR}/extras/patches)
 
@@ -87,6 +85,17 @@ list(APPEND QWINDOWKIT_OPTIONS
   QWINDOWKIT_BUILD_STATIC ON
 )
 
+if(WIN32)
+  # Beta config
+  if(BETA)
+    message(STATUS "Beta config enabled")
+    add_definitions(-DBETA)
+    set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Beta)
+  else()
+    set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Release)
+  endif()
+endif()
+
 if(WIN32)
   list(APPEND QWINDOWKIT_OPTIONS QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS OFF)
 endif()
@@ -110,6 +119,44 @@ add_fetch_content(
 list(APPEND CLIENT_INCLUDE_DIRS ${QWindowKit_BINARY_DIR}/include)
 list(APPEND CLIENT_LIBS QWindowKit::Quick)
 
+# If ENABLE_CRASHREPORTS is enabled, we will use crashpad_cmake for now.
+if(ENABLE_CRASHREPORTS)
+  set(ENABLE_CRASHPAD ON)
+  set(CRASH_REPORT_URL "http://localhost:8080/submit" CACHE STRING "URL for crash handler uploads")
+endif()
+add_definitions(-DCRASH_REPORT_URL="${CRASH_REPORT_URL}")
+
+# Crash-report client: crashpad
+if(ENABLE_CRASHPAD)
+  message(STATUS "Crashpad enabled for client")
+  if(WIN32)
+    set(CMAKE_OBJECT_PATH_MAX 256)
+    add_definitions(-DNOMINMAX)
+  endif()
+  add_fetch_content(
+    TARGET crashpad_cmake
+    URL https://github.com/TheAssemblyArmada/crashpad-cmake.git
+    BRANCH 80573adcc845071401c73c99eaec7fd9847d45fb
+  )
+  add_definitions(-DENABLE_CRASHPAD)
+  if (WIN32)
+    # This makes sure the console window doesn't show up when running the
+    # crashpad_handler executable.
+    set_target_properties(crashpad_handler PROPERTIES LINK_FLAGS "/SUBSYSTEM:WINDOWS")
+    # Set the output directory for the crashpad_handler executable. On Windows,
+    # we use either the Release or Beta directory depending on the BETA option
+    # which is set above.
+    set_target_properties(crashpad_handler PROPERTIES
+        RUNTIME_OUTPUT_DIRECTORY_RELEASE "${JAMI_OUTPUT_DIRECTORY_RELEASE}")
+  endif()
+endif()
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+if(NOT MSVC)
+  set(CMAKE_CXX_FLAGS_DEBUG "-Og -ggdb")
+endif()
+
 set(CMAKE_AUTOMOC ON)
 set(CMAKE_AUTORCC ON)
 set(CMAKE_AUTOUIC ON)
@@ -323,7 +370,8 @@ set(COMMON_SOURCES
   ${APP_SRC_DIR}/imagedownloader.cpp
   ${APP_SRC_DIR}/pluginversionmanager.cpp
   ${APP_SRC_DIR}/connectioninfolistmodel.cpp
-  ${APP_SRC_DIR}/pluginversionmanager.cpp)
+  ${APP_SRC_DIR}/pluginversionmanager.cpp
+)
 
 set(COMMON_HEADERS
   ${APP_SRC_DIR}/global.h
@@ -392,7 +440,10 @@ set(COMMON_HEADERS
   ${APP_SRC_DIR}/imagedownloader.h
   ${APP_SRC_DIR}/pluginversionmanager.h
   ${APP_SRC_DIR}/connectioninfolistmodel.h
-  ${APP_SRC_DIR}/pttlistener.h)
+  ${APP_SRC_DIR}/pttlistener.h
+  ${APP_SRC_DIR}/crashreportclient.h
+  ${APP_SRC_DIR}/crashreporter.h
+)
 
 # For libavutil/avframe.
 set(LIBJAMI_CONTRIB_DIR "${DAEMON_DIR}/contrib")
@@ -411,6 +462,15 @@ endif()
 # Define PREFER_VULKAN to prefer Vulkan over the default API
 # on GNU/Linux and Windows. Metal is always preferred on macOS.
 
+if(ENABLE_CRASHREPORTS)
+  set(CRASHREPORT_CLIENT_DIR ${APP_SRC_DIR}/crashreportclients)
+  if(ENABLE_CRASHPAD)
+    list(APPEND CLIENT_LIBS crashpad_client)
+    list(APPEND COMMON_SOURCES ${CRASHREPORT_CLIENT_DIR}/crashpad.cpp)
+    list(APPEND COMMON_HEADERS ${CRASHREPORT_CLIENT_DIR}/crashpad.h)
+  endif()
+endif()
+
 if(MSVC)
   set(WINDOWS_SYS_LIBS
     windowsapp.lib
@@ -456,16 +516,6 @@ if(MSVC)
   set(JAMID_SRC_PATH ${DAEMON_DIR}/contrib/msvc/include)
   set(GNUTLS_LIB ${DAEMON_DIR}/contrib/msvc/lib/x64/libgnutls.lib)
 
-  # Beta config
-  if(BETA)
-    message(STATUS "Beta config enabled")
-    add_definitions(-DBETA)
-    set(JAMI_OUTPUT_DIRECTORY_RELEASE ${PROJECT_SOURCE_DIR}/x64/Beta)
-  else()
-    set(JAMI_OUTPUT_DIRECTORY_RELEASE
-      ${PROJECT_SOURCE_DIR}/x64/Release)
-  endif()
-
   include_directories(
     ${JAMID_SRC_PATH}
     ${LIBCLIENT_SRC_DIR}
diff --git a/extras/crash-reports/example-submission-servers/.gitignore b/extras/crash-reports/example-submission-servers/.gitignore
new file mode 100644
index 000000000..e5928400b
--- /dev/null
+++ b/extras/crash-reports/example-submission-servers/.gitignore
@@ -0,0 +1,9 @@
+# python virtual environment
+venv/
+# python compiled files
+*.pyc
+# python cache
+__pycache__
+
+# example output
+crash_reports/
diff --git a/extras/crash-reports/example-submission-servers/README.md b/extras/crash-reports/example-submission-servers/README.md
new file mode 100644
index 000000000..407c577b7
--- /dev/null
+++ b/extras/crash-reports/example-submission-servers/README.md
@@ -0,0 +1,36 @@
+# Crash report submission server examples
+
+## Overview
+
+This directory contains examples of crash report submission servers. These servers are responsible for receiving crash reports from clients and storing them. The examples are written in Python and use the Flask web framework.
+
+## Running the examples
+
+To run the examples, you need to have Python 3 installed. You can just use the virtual environment provided in this directory. To activate the virtual environment, run the following commands:
+
+```
+python3 -m venv venv
+source venv/bin/activate
+pip install -r requirements.txt
+```
+
+After activating the virtual environment, you can should be able to execute the example submission servers. To run the example submission server that uses the Crashpad format, run the following command:
+
+```
+python crashpad.py
+```
+
+## Metadata
+
+The crash report submission servers expect the crash reports to contain a JSON object. The JSON object should contain the following basic metadata:
+```
+{
+    "build_id": "202410021437",
+    "client_sha": "77149ebd62",
+    "guid": "50c4218a-bcb9-48a9-8093-a06e6435cd61",
+    "jamicore_sha": "cbf8f0af6",
+    "platform": "Ubuntu 22.04.4 LTS_x86_64"
+}
+```
+
+The `build_id` field is the build identifier of the client application. The `client_sha` field is the SHA-1 hash of the client application. The `guid` field is a unique identifier for the crash report. The `jamicore_sha` field is the SHA-1 hash of the Jami core library. The `platform` field is the platform on which the client application is running.
\ No newline at end of file
diff --git a/extras/crash-reports/example-submission-servers/crashpad.py b/extras/crash-reports/example-submission-servers/crashpad.py
new file mode 100644
index 000000000..fed0c45a5
--- /dev/null
+++ b/extras/crash-reports/example-submission-servers/crashpad.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+import os
+from flask import Flask, request
+import json
+
+app = Flask(__name__)
+
+@app.route('/submit', methods=['POST'])
+def submit():
+    try:
+        print("Received a crash report GUID: %s" % request.form.get('guid', 'No GUID provided'))
+        file_storage = request.files.get('upload_file_minidump')
+        dump_id = ""
+        if file_storage:
+            dump_id = file_storage.filename
+
+            # Create a directory to store the crash reports if it doesn't exist
+            base_path = 'crash_reports'
+            if not os.path.exists(base_path):
+                os.makedirs(base_path)
+
+            filepath = os.path.join(base_path, dump_id)
+
+            # Attempt to write the file, fail gracefully if it already exists
+            if os.path.exists(filepath):
+                print(f"File {filepath} already exists.")
+                return 'File already exists', 409
+            with open(filepath, 'wb') as f:
+                f.write(file_storage.read())
+            print(f"File saved successfully at {filepath}")
+
+            # Now save the metadata in {request.form} as separate filename <UID>.info.
+            # We assume the data is a JSON string.
+            metadata_filepath = os.path.join(base_path, f"{dump_id}.info")
+            with open(metadata_filepath, 'w') as f:
+                f.write(str(json.dumps(dict(request.form), indent=4)))
+        else:
+            print("No file found for the key 'upload_file_minidump'")
+            return 'No file found', 400
+
+        return 'Crash report received', 200
+    except OSError as e:
+        print(f"Error creating directory or writing file: {e}")
+        return 'Internal Server Error', 500
+    except Exception as e:
+        print(f"An unexpected error occurred: {e}")
+        return 'Internal Server Error', 500
+
+if __name__ == '__main__':
+    app.run(port=8080, debug=True)
\ No newline at end of file
diff --git a/extras/crash-reports/example-submission-servers/requirements.txt b/extras/crash-reports/example-submission-servers/requirements.txt
new file mode 100644
index 000000000..dce4dce80
--- /dev/null
+++ b/extras/crash-reports/example-submission-servers/requirements.txt
@@ -0,0 +1,5 @@
+Flask==3.0.3
+requests==2.24.0
+markupsafe==2.1.1
+itsdangerous==2.1.2
+werkzeug==3.0.0
\ No newline at end of file
diff --git a/src/app/MainApplicationWindow.qml b/src/app/MainApplicationWindow.qml
index 18ce79e45..68f68a67e 100644
--- a/src/app/MainApplicationWindow.qml
+++ b/src/app/MainApplicationWindow.qml
@@ -210,6 +210,22 @@ ApplicationWindow {
         // Dbus error handler for Linux.
         if (Qt.platform.os.toString() !== "windows" && Qt.platform.os.toString() !== "osx")
             DBusErrorHandler.setActive(true);
+
+        // Handle potential crash recovery.
+        var crashedLastRun = crashReporter.getHasPendingReport();
+        if (crashedLastRun) {
+            // A crash was detected during the last session. We need to inform the user and offer to send a crash report.
+            var dlg = viewCoordinator.presentDialog(appWindow, "commoncomponents/ConfirmDialog.qml", {
+                    "title": JamiStrings.crashReportTitle,
+                    "textLabel": JamiStrings.crashReportMessage + "\n\n" + JamiStrings.crashReportMessageExtra,
+                    "confirmLabel": JamiStrings.send,
+                    "rejectLabel": JamiStrings.dontSend,
+                    "textHAlign": Text.AlignLeft,
+                    "textMaxWidth": 400,
+                });
+            dlg.accepted.connect(function () { crashReporter.uploadLastReport(); });
+            dlg.rejected.connect(function () { crashReporter.clearReports(); });
+        }
     }
 
     Loader {
diff --git a/src/app/appsettingsmanager.cpp b/src/app/appsettingsmanager.cpp
index c7330b3f5..1c583bb32 100644
--- a/src/app/appsettingsmanager.cpp
+++ b/src/app/appsettingsmanager.cpp
@@ -24,8 +24,7 @@
 
 #include <QCoreApplication>
 #include <QLibraryInfo>
-
-#include <locale.h>
+#include <QDir>
 
 const QString defaultDownloadPath = QStandardPaths::writableLocation(
     QStandardPaths::DownloadLocation);
diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h
index c2b61d79e..bbb61d119 100644
--- a/src/app/appsettingsmanager.h
+++ b/src/app/appsettingsmanager.h
@@ -26,7 +26,6 @@
 #include <QStandardPaths>
 #include <QWindow> // for QWindow::AutomaticVisibility
 #include <QSettings>
-#include <QDir>
 
 #include <QTranslator>
 
@@ -74,7 +73,9 @@ extern const QString defaultDownloadPath;
     X(ShowSendOption, false) \
     X(EnablePtt, false) \
     X(PttKeys, 32) \
-    X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT)
+    X(UseFramelessWindow, USE_FRAMELESS_WINDOW_DEFAULT) \
+    X(EnableCrashReporting, true) \
+    X(EnableAutomaticCrashReporting, false)
 #if APPSTORE
 #define KEYS COMMON_KEYS
 #else
diff --git a/src/app/commoncomponents/ConfirmDialog.qml b/src/app/commoncomponents/ConfirmDialog.qml
index c1a9e6606..dbaa5bc38 100644
--- a/src/app/commoncomponents/ConfirmDialog.qml
+++ b/src/app/commoncomponents/ConfirmDialog.qml
@@ -26,10 +26,15 @@ BaseModalDialog {
     id: root
 
     signal accepted
+    signal rejected
 
     property string confirmLabel: ""
+    property string rejectLabel
     property string textLabel: ""
+    property int textHAlign: Text.AlignHCenter
+    property real textMaxWidth: width - JamiTheme.preferredMarginSize * 4
 
+    autoClose: false
     closeButtonVisible: false
     button1.text: confirmLabel
     button1.contentColorProvider: JamiTheme.redButtonColor
@@ -37,8 +42,11 @@ BaseModalDialog {
         close();
         accepted();
     }
-    button2.text: JamiStrings.optionCancel
-    button2.onClicked: close()
+    button2.text: rejectLabel ? rejectLabel : JamiStrings.optionCancel
+    button2.onClicked: {
+        close();
+        rejected();
+    }
 
     button1Role: DialogButtonBox.AcceptRole
     button2Role: DialogButtonBox.RejectRole
@@ -50,7 +58,7 @@ BaseModalDialog {
             id: labelAction
 
             Layout.alignment: Qt.AlignHCenter
-            Layout.maximumWidth: root.width - JamiTheme.preferredMarginSize * 4
+            Layout.maximumWidth: textMaxWidth
 
             color: JamiTheme.textColor
             text: root.textLabel
@@ -58,7 +66,7 @@ BaseModalDialog {
             font.pointSize: JamiTheme.textFontSize
             font.kerning: true
 
-            horizontalAlignment: Text.AlignHCenter
+            horizontalAlignment: textHAlign
             verticalAlignment: Text.AlignVCenter
             wrapMode: Text.Wrap
         }
diff --git a/src/app/crashreportclient.h b/src/app/crashreportclient.h
new file mode 100644
index 000000000..386c6d266
--- /dev/null
+++ b/src/app/crashreportclient.h
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "version.h"
+#include "version_info.h"
+
+#include <QVariantMap>
+
+class AppSettingsManager;
+
+/**
+ * In the context of Jami where we employ GnuTLS, OpenSSL, and other cryptographic
+ * components as part of secure communication protocols, it is essential to configure
+ * crash reports with security in mind to prevent sensitive data from being exposed in crash
+ * reports. We must assume that attackers may attempt to exploit vulnerabilities based on
+ * stack data including values of cryptographic keys, certificates, etc. that may be used
+ * in some way to compromise the security of the user's account.
+ *
+ * We attempt to mitigate this risk by configuring crash reports to avoid collecting stack
+ * data beyond the offending function that caused the crash. We make the assumption that
+ * cryptographically sensitive data is not stored on the stack by 3rd party libraries.
+ *
+ * We also take care to avoid sending crash reports automatically and instead require user
+ * consent before uploading the last report.
+ *
+ * IMPORTANT: The opt-in approach is crucial, and the potential implications of transmitting
+ * these reports must be communicated to the user. The user should be informed that we cannot
+ * guarantee the security of the data in the crash report, even if we take steps to avoid
+ * leaking any sensitive information.
+ *
+ * We offer the following configuration options to enhance security:
+ *
+ * - (Option) EnableCrashReporting (default - true):
+ *   An application settings allowing users to disable crash handling entirely.
+ *
+ * - (Option) EnableAutomaticCrashReporting (default - false):
+ *   This setting allows users to opt-in to automatic crash reporting, which should be disabled
+ *   by default. When the application crashes, the user should be prompted to upload the last
+ *   crash report. If the user agrees, the report will be uploaded to the server. If this
+ *   setting is enabled, no prompt will be shown, and the report will be uploaded automatically
+ *   when the application crashes.
+ *
+ * Further considerations:
+ *
+ * - **Annotations**:
+ *   Allows the inclusion of custom metadata in crash reports, such as the application version
+ *   and build number, without exposing sensitive information. We must include this information
+ *   to use the crash reports constructively.
+ */
+
+class CrashReportClient : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit CrashReportClient(AppSettingsManager* settingsManager, QObject* parent = nullptr)
+        : QObject(parent)
+        , settingsManager_(settingsManager)
+        , crashReportUrl_(CRASH_REPORT_URL)
+    {}
+    ~CrashReportClient() = default;
+
+    virtual void syncHandlerWithSettings() = 0;
+    virtual void uploadLastReport() = 0;
+    virtual void clearReports() = 0;
+
+    // Used by the QML interface to query whether the application crashed last run.
+    bool getHasPendingReport()
+    {
+        // In builds that do not support crashpad, this will always return false, and
+        // thus will never trigger the dialog asking the user to upload the last report.
+        return crashedLastRun_;
+    }
+
+protected:
+    // This function is used to toggle automatic crash reporting.
+    virtual void setUploadsEnabled(bool enabled) = 0;
+
+    // We will need to access the crash report related settings.
+    AppSettingsManager* settingsManager_;
+
+    // The endpoint URL that crash reports will be uploaded to.
+    QString crashReportUrl_;
+
+    // We store if the last run resulted in
+    bool crashedLastRun_ {false};
+
+    // This is the metadata that will be sent with each crash report.
+    // This data is required to correlate crash reports with the build so we can
+    // effectively load and analyze the mini-dumps.
+    QVariantMap metaData_ {
+        {"platform", QSysInfo::prettyProductName() + "_" + QSysInfo::currentCpuArchitecture()},
+        {"client_sha", APP_VERSION_STRING},
+        {"jamicore_sha", CORE_VERSION_STRING},
+        {"build_id", QString(VERSION_STRING)},
+    };
+};
+
+// Null implementation of the crash report client
+class NullCrashReportClient : public CrashReportClient
+{
+    Q_OBJECT
+
+public:
+    explicit NullCrashReportClient(AppSettingsManager* settingsManager, QObject* parent = nullptr)
+        : CrashReportClient(settingsManager, parent)
+    {}
+
+    void syncHandlerWithSettings() override {}
+    void uploadLastReport() override {}
+    void clearReports() override {}
+
+protected:
+    void setUploadsEnabled(bool enabled) override {}
+};
diff --git a/src/app/crashreportclients/crashpad.cpp b/src/app/crashreportclients/crashpad.cpp
new file mode 100644
index 000000000..c97888420
--- /dev/null
+++ b/src/app/crashreportclients/crashpad.cpp
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "crashpad.h"
+
+#include "appsettingsmanager.h"
+#include "global.h"
+
+#include <client/crash_report_database.h>
+#include <client/settings.h>
+#include <client/crashpad_info.h>
+
+#include <QDir>
+#include <QCoreApplication>
+#include <QStandardPaths>
+#include <QThreadPool>
+
+#include <thread>
+
+#if defined(OS_WIN)
+#define FILEPATHSTR(Qs)     Qs.toStdWString()
+#define STRFILEPATH(Qs)     QString::fromStdWString(Qs)
+#define CRASHPAD_EXECUTABLE "crashpad_handler.exe"
+#else
+#define FILEPATHSTR(Qs)     Qs.toStdString()
+#define STRFILEPATH(Qs)     QString::fromStdString(Qs)
+#define CRASHPAD_EXECUTABLE "crashpad_handler"
+#endif // OS_WIN
+
+// We need the number of reports in the database to determine if the application crashed last time.
+static int
+getReportCount(base::FilePath dbPath)
+{
+    auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath));
+    if (database == nullptr) {
+        return 0;
+    }
+    std::vector<crashpad::CrashReportDatabase::Report> completedReports;
+    database->GetCompletedReports(&completedReports);
+    return completedReports.size();
+}
+
+static void
+clearCompletedReports(crashpad::CrashReportDatabase* database)
+{
+    using namespace crashpad;
+    using OperationStatus = CrashReportDatabase::OperationStatus;
+
+    std::vector<CrashReportDatabase::Report> reports;
+    auto status = database->GetCompletedReports(&reports);
+    if (OperationStatus::kNoError != status) {
+        C_WARN << "Could not retrieve completed reports";
+        return;
+    }
+
+    for (const auto& report : reports) {
+        C_INFO.noquote() << QString("Deleting report: %1").arg(report.uuid.ToString().c_str());
+        status = database->DeleteReport(report.uuid);
+        if (OperationStatus::kNoError != status) {
+            C_WARN << "Failed to delete report";
+        }
+    }
+}
+
+CrashPadClient::CrashPadClient(AppSettingsManager* settingsManager, QObject* parent)
+    : CrashReportClient(settingsManager, parent)
+{
+    try {
+        C_INFO << "Crashpad crash reporting enabled";
+
+        // We store the crashpad database in the application's local data.
+        const auto dataPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
+        if (dataPath.isEmpty()) {
+            throw std::runtime_error("Failed to retrieve writable location for AppLocalData");
+        }
+        dbPath_ = base::FilePath(FILEPATHSTR(QDir(dataPath).absoluteFilePath("crash_db")));
+
+        // Make sure the database directory exists.
+        if (!QDir().mkpath(STRFILEPATH(dbPath_.value()))) {
+            throw std::runtime_error("Failed to create crash database directory");
+        }
+
+        // The crashpad_handler executable is in the same directory as this executable.
+        const auto appBinPath = QCoreApplication::applicationDirPath();
+        if (appBinPath.isEmpty()) {
+            throw std::runtime_error("Failed to retrieve application directory path");
+        }
+        handlerPath_ = base::FilePath(FILEPATHSTR(QDir(appBinPath).filePath(CRASHPAD_EXECUTABLE)));
+        C_DBG << "Handler runtime path: " << handlerPath_.value();
+
+        // Check if the application crashed last time it was run by checking the crashpad database
+        // report count. If there is at least one report, we set the crashedLastRun_ flag to true.
+        // The flag will be queried by the QML interface to display a dialog. If the user accepts,
+        // the uploadLastReport function will be called, otherwise the reports will be cleared
+        // to avoid a build up of crash reports on the user's system.
+        using key = Settings::Key;
+        auto automaticReporting = settingsManager_->getValue(key::EnableAutomaticCrashReporting)
+                                      .toBool();
+        if (getReportCount(dbPath_) > 0 && !automaticReporting) {
+            crashedLastRun_ = true;
+        }
+
+        // If we crashed last time and need to send off a report, then uploadLastReport will call
+        // startHandler, and considering the `restartable` option for `StartHandler` is unused,
+        // and that restarting the handler will cause an assertion failure when debugging Linux,
+        // we will just not start the handler here in that case. The handler will be started in
+        // the clearReports function after the reports are cleared.
+        if (!crashedLastRun_) {
+            startHandler();
+        }
+    } catch (const std::exception& e) {
+        C_ERR << "Error initializing CrashPadClient: " << e.what();
+    } catch (...) {
+        C_ERR << "Unknown error initializing CrashPadClient";
+    }
+}
+
+CrashPadClient::~CrashPadClient()
+{
+    // Remove any remaining stale crash reports.
+    // We use sleep to ensure that the reports are cleared after a forced upload,
+    // and it's possible that the reports are still being processed. This is a
+    // workaround for the lack of a synchronous `report-uploaded` signal/event.
+    if (auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_))) {
+        clearCompletedReports(database.get());
+    }
+}
+
+void
+CrashPadClient::startHandler()
+{
+    std::vector<std::string> arguments;
+
+    // We disable rate-limiting because we want to upload crash reports as soon as possible.
+    // Perhaps we should enable rate-limiting in the future to avoid spamming the server, but
+    // we will need to investigate how that works in crashpad's implementation.
+    arguments.push_back("--no-rate-limit");
+
+    // We disable gzip compression because we want to be able to read the reports easily.
+    arguments.push_back("--no-upload-gzip");
+
+    // Convert the client metadata to map-string-string.
+    std::map<std::string, std::string> annotations;
+    Q_FOREACH (auto key, metaData_.keys()) {
+        annotations[key.toStdString()] = metaData_[key].toString().toStdString();
+    }
+
+    C_INFO << "Starting crashpad handler";
+
+    bool success = client_.StartHandler(handlerPath_,                  // handler
+                                        dbPath_,                       // database_dir
+                                        {},                            // metrics_dir
+                                        crashReportUrl_.toStdString(), // url to upload reports
+                                        annotations,                   // Annotations
+                                        arguments,                     // Arguments
+                                        false, // restartable (this doesn't do anything)
+                                        false, // asynchronous_start (this doesn't do anything)
+                                        std::vector<base::FilePath>() // Attachments
+    );
+
+    if (!success) {
+        C_WARN << "Crashpad initialization failed";
+        return;
+    }
+
+    // Update the handler settings after starting the handler (we may have restarted it).
+    syncHandlerWithSettings();
+}
+
+void
+CrashPadClient::syncHandlerWithSettings()
+{
+    // Configure the crashpad handler with the settings from the settings manager.
+    using key = Settings::Key;
+    using namespace crashpad;
+    CrashpadInfo* crashpad_info = CrashpadInfo::GetCrashpadInfo();
+
+    // Optionally disable crashpad handler.
+    auto enableReportsAppSetting = settingsManager_->getValue(key::EnableCrashReporting).toBool();
+    crashpad_info->set_crashpad_handler_behavior(enableReportsAppSetting ? TriState::kEnabled
+                                                                         : TriState::kDisabled);
+
+    // Enable automatic crash reporting if the user has opted in.
+    auto automaticReporting = settingsManager_->getValue(key::EnableAutomaticCrashReporting).toBool();
+    setUploadsEnabled(automaticReporting);
+}
+
+void
+CrashPadClient::setUploadsEnabled(bool enabled)
+{
+    auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_));
+    if (database != nullptr && database->GetSettings() != nullptr) {
+        database->GetSettings()->SetUploadsEnabled(enabled);
+    }
+}
+
+void
+CrashPadClient::clearReports()
+{
+    auto database = crashpad::CrashReportDatabase::Initialize(base::FilePath(dbPath_));
+    if (database == nullptr) {
+        return;
+    }
+
+    C_DBG << "Clearing completed crash reports";
+
+    const time_t secondsToWaitForReportLocks = 1;
+    database->CleanDatabase(secondsToWaitForReportLocks);
+
+    ::clearCompletedReports(database.get());
+
+    // If the crashedLastRun_ flag is set, then we should follow up and start the handler.
+    // Refer to the comment in constructor for more information on why the handler wasn't
+    // started in the constructor if we crashed last time.
+    if (crashedLastRun_) {
+        startHandler();
+    }
+}
+
+void
+CrashPadClient::uploadLastReport()
+{
+    using namespace crashpad;
+
+    // Find the latest crash report.
+    auto database = CrashReportDatabase::Initialize(base::FilePath(dbPath_));
+    if (database == nullptr) {
+        C_WARN << "Crashpad database initialization failed";
+        return;
+    }
+
+    std::vector<CrashReportDatabase::Report> reports;
+    using OperationStatus = CrashReportDatabase::OperationStatus;
+    auto status = database->GetCompletedReports(&reports);
+    if (OperationStatus::kNoError != status) {
+        C_WARN << "Crashpad database GetCompletedReports failed";
+        return;
+    }
+
+    if (reports.empty()) {
+        C_WARN << "Crashpad database contains no completed reports";
+        return;
+    }
+
+    auto report = reports.back();
+
+    // Force the report to be uploaded (should change the report state to pending).
+    C_INFO << "Requesting report upload:" << report.uuid.ToString().c_str();
+    status = database->RequestUpload(report.uuid);
+
+    if (status != CrashReportDatabase::kNoError) {
+        // This may indicate that the report has already been removed from the database.
+        C_WARN << "Failed to request upload, status: " << status;
+        return;
+    }
+
+    // In this case, unless we restart the crashpad handler, the report won't
+    // be uploaded until the application is terminated.
+    startHandler();
+
+    // Let's wait for the report to be uploaded then clear all reports on a
+    // separate thread to avoid blocking the UI.
+    QThreadPool::globalInstance()->start([this, uuid = report.uuid]() {
+        auto database = CrashReportDatabase::Initialize(base::FilePath(dbPath_));
+        if (database == nullptr) {
+            C_WARN << "Crashpad database initialization failed";
+            return;
+        }
+
+        // Wait up to 3 seconds (~1.5s observed on Windows) for the report to be uploaded.
+        const int maxAttempts = 20;
+        int attempts = 0;
+        auto timeout = std::chrono::milliseconds(150);
+
+        C_INFO << "Waiting for report to be uploaded";
+        while (attempts++ < maxAttempts) {
+            CrashReportDatabase::Report report;
+            if (database->LookUpCrashReport(uuid, &report) == CrashReportDatabase::kNoError) {
+                if (report.uploaded
+                    && database->DeleteReport(uuid) == CrashReportDatabase::kNoError) {
+                    C_INFO << "Report uploaded and deleted successfully";
+                    return;
+                }
+            }
+            std::this_thread::sleep_for(timeout);
+        }
+
+        // Note: This usually indicates that the submission server is inaccessible.
+        C_WARN << "Failed to delete report. It may not have been uploaded successfully";
+
+        // If we failed to delete the report, we should still try to clear any dangling reports.
+        // This will prevent the database from growing indefinitely by removing at least the
+        // previous unsuccessfully removed report. In this case, we don't know if the report
+        // was successfully uploaded or not. This is a best-effort attempt.
+        ::clearCompletedReports(database.get());
+    });
+}
diff --git a/src/app/crashreportclients/crashpad.h b/src/app/crashreportclients/crashpad.h
new file mode 100644
index 000000000..6034675fc
--- /dev/null
+++ b/src/app/crashreportclients/crashpad.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "crashreportclient.h"
+
+#include <client/crashpad_client.h>
+
+class AppSettingsManager;
+
+class CrashPadClient final : public CrashReportClient
+{
+    Q_OBJECT
+
+public:
+    explicit CrashPadClient(AppSettingsManager* settingsManager, QObject* parent = nullptr);
+    ~CrashPadClient();
+
+    void syncHandlerWithSettings() override;
+    void uploadLastReport() override;
+    void clearReports() override;
+
+protected:
+    void setUploadsEnabled(bool enabled) override;
+
+private:
+    void startHandler();
+
+    crashpad::CrashpadClient client_;
+    base::FilePath dbPath_;
+    base::FilePath handlerPath_;
+};
diff --git a/src/app/crashreporter.h b/src/app/crashreporter.h
new file mode 100644
index 000000000..6f0087ffd
--- /dev/null
+++ b/src/app/crashreporter.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// Implementation choice
+#include "crashreportclient.h"
+#if not ENABLE_CRASHREPORTS
+using CrashReportClientImpl = NullCrashReportClient;
+#elif defined(ENABLE_CRASHPAD)
+#include "crashreportclients/crashpad.h"
+using CrashReportClientImpl = CrashPadClient;
+#else
+#pragma GCC error "No crash report client enabled, but reports are enabled."
+#endif
+
+#include <memory>
+
+class CrashReporter : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit CrashReporter(AppSettingsManager* settingsManager, QObject* parent = nullptr)
+        : QObject(parent)
+    {
+        client_ = std::make_unique<CrashReportClientImpl>(settingsManager, this);
+    }
+
+    Q_INVOKABLE void syncHandlerWithSettings()
+    {
+        client_->syncHandlerWithSettings();
+    }
+    Q_INVOKABLE void uploadLastReport()
+    {
+        client_->uploadLastReport();
+    }
+    Q_INVOKABLE void clearReports()
+    {
+        client_->clearReports();
+    }
+    Q_INVOKABLE bool getHasPendingReport()
+    {
+        return client_->getHasPendingReport();
+    }
+
+private:
+    std::unique_ptr<CrashReportClient> client_;
+};
diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp
index f00e0ef5c..1d191ee62 100644
--- a/src/app/mainapplication.cpp
+++ b/src/app/mainapplication.cpp
@@ -27,6 +27,7 @@
 #include "connectivitymonitor.h"
 #include "systemtray.h"
 #include "previewengine.h"
+#include "crashreporter.h"
 
 #include <QWKQuick/qwkquickglobal.h>
 
@@ -42,8 +43,6 @@
 #include <QLibraryInfo>
 #include <QQuickWindow>
 
-#include <thread>
-
 #ifdef Q_OS_WIN
 #include <windows.h>
 #endif
@@ -183,11 +182,20 @@ MainApplication::~MainApplication()
 {
     engine_.reset();
     lrcInstance_.reset();
+
+    // Allow the crash reporter to do implementation-specific cleanup before the application exits.
+    delete crashReporter_;
 }
 
 bool
 MainApplication::init()
 {
+    // Let's make sure we can provide postmortem debugging information prior
+    // to any other initialization. This won't do anything if crashpad isn't
+    // enabled.
+    settingsManager_ = new AppSettingsManager(this);
+    crashReporter_ = new CrashReporter(settingsManager_, this);
+
     // This 2-phase initialisation prevents ephemeral instances from
     // performing unnecessary tasks, like initializing the WebEngine.
     engine_.reset(new QQmlApplicationEngine(this));
@@ -195,7 +203,6 @@ MainApplication::init()
     QWK::registerTypes(engine_.get());
 
     connectivityMonitor_ = new ConnectivityMonitor(this);
-    settingsManager_ = new AppSettingsManager(this);
     systemTray_ = new SystemTray(settingsManager_, this);
     previewEngine_ = new PreviewEngine(connectivityMonitor_, this);
 
@@ -425,6 +432,9 @@ MainApplication::initQmlLayer()
                          &screenInfo_,
                          this);
 
+    // Register the crash reporter as a context property in the QML engine.
+    engine_->rootContext()->setContextProperty("crashReporter", crashReporter_);
+
     QUrl url = u"qrc:/MainApplicationWindow.qml"_qs;
 #ifdef QT_DEBUG
     if (parser_.isSet("test")) {
diff --git a/src/app/mainapplication.h b/src/app/mainapplication.h
index e318ad73c..1e36cfca4 100644
--- a/src/app/mainapplication.h
+++ b/src/app/mainapplication.h
@@ -31,11 +31,10 @@
 #include <QWindow>
 #include <QCommandLineParser>
 
-#include <memory>
-
 class ConnectivityMonitor;
-class AppSettingsManager;
 class SystemTray;
+class AppSettingsManager;
+class CrashReporter;
 class PreviewEngine;
 
 // Provides information about the screen the app is displayed on
@@ -123,6 +122,8 @@ private:
     SystemTray* systemTray_;
     AppSettingsManager* settingsManager_;
     PreviewEngine* previewEngine_;
+    CrashReporter* crashReporter_;
+
     ScreenInfo screenInfo_;
     QCommandLineParser parser_;
 };
diff --git a/src/app/net/jami/Constants/JamiStrings.qml b/src/app/net/jami/Constants/JamiStrings.qml
index c3d41877d..8dfdf2ffc 100644
--- a/src/app/net/jami/Constants/JamiStrings.qml
+++ b/src/app/net/jami/Constants/JamiStrings.qml
@@ -46,7 +46,6 @@ Item {
     // AboutPopUp
     property string buildID: qsTr("Build ID")
     property string version: qsTr("Version")
-
     property string declarationYear: "© 2015-2024"
     property string slogan: "Astarte"
     property string declaration: qsTr('Jami, a GNU package, is software for universal and distributed peer-to-peer communication that respects the freedom and privacy of its users. Visit <a href="https://jami.net" style="color: ' + JamiTheme.buttonTintedBlue + '">jami.net</a>' + ' to learn more.')
@@ -54,6 +53,11 @@ Item {
     property string contribute: qsTr('Contribute')
     property string feedback: qsTr('Feedback')
 
+    // Crash report popup
+    property string crashReportTitle: qsTr("Application Recovery")
+    property string crashReportMessage: qsTr("Jami has recovered from a crash. Would you like to send a crash report to help us fix the issue?")
+    property string crashReportMessageExtra: qsTr("Only essential data, including the app version, platform information, and a snapshot of the program's state at the time of the crash, will be shared.")
+
     // AccountComboBox
     property string displayQRCode: qsTr("Display QR code")
     property string openSettings: qsTr("Open settings")
@@ -769,6 +773,7 @@ Item {
     property string shiftEnterNewLine: qsTr("Press Shift+Enter to insert a new line")
     property string enterNewLine: qsTr("Press Enter to insert a new line")
     property string send: qsTr("Send")
+    property string dontSend: qsTr("Don't send")
     property string replyTo: qsTr("Reply to")
     property string inReplyTo: qsTr("In reply to")
     property string repliedTo: qsTr(" replied to")
diff --git a/src/app/qmlregister.cpp b/src/app/qmlregister.cpp
index 57649f834..4c6c52aaa 100644
--- a/src/app/qmlregister.cpp
+++ b/src/app/qmlregister.cpp
@@ -280,6 +280,7 @@ registerTypes(QQmlEngine* engine,
     auto videoProvider = new VideoProvider(lrcInstance->avModel(), app);
     engine->rootContext()->setContextProperty("videoProvider", videoProvider);
 
+    engine->rootContext()->setContextProperty("ENABLE_CRASHREPORTS", ENABLE_CRASHREPORTS);
     engine->rootContext()->setContextProperty("WITH_WEBENGINE", WITH_WEBENGINE);
     engine->rootContext()->setContextProperty("APPSTORE", APPSTORE);
 }
diff --git a/src/app/settingsview/components/TroubleshootSettingsPage.qml b/src/app/settingsview/components/TroubleshootSettingsPage.qml
index de41a9627..be4c781a0 100644
--- a/src/app/settingsview/components/TroubleshootSettingsPage.qml
+++ b/src/app/settingsview/components/TroubleshootSettingsPage.qml
@@ -49,47 +49,80 @@ SettingsPageBase {
         anchors.left: parent.left
         anchors.leftMargin: JamiTheme.preferredSettingsMarginSize
 
-        RowLayout {
-            id: rawLayout
-            Text {
-                Layout.fillWidth: true
-                Layout.preferredHeight: 30
-                Layout.rightMargin: JamiTheme.preferredMarginSize
+        ColumnLayout {
+            width: parent.width
+
+            spacing: 10
 
-                text: JamiStrings.troubleshootText
-                font.pointSize: JamiTheme.settingsFontSize
-                font.kerning: true
-                wrapMode: Text.WordWrap
-                horizontalAlignment: Text.AlignLeft
-                verticalAlignment: Text.AlignVCenter
+            ToggleSwitch {
+                id: enableCrashReports
+                visible: ENABLE_CRASHREPORTS
+                Layout.fillWidth: true
+                labelText: qsTr("Enable crash reports")
+                checked: UtilsAdapter.getAppValue(Settings.EnableCrashReporting)
 
-                color: JamiTheme.textColor
+                onSwitchToggled: {
+                    UtilsAdapter.setAppValue(Settings.EnableCrashReporting, checked);
+                    crashReporter.syncHandlerWithSettings();
+                }
             }
 
-            MaterialButton {
-                id: enableTroubleshootingButton
+            ToggleSwitch {
+                id: enableAutomaticCrashReporting
+                visible: ENABLE_CRASHREPORTS
+                enabled: enableCrashReports.checked
+                Layout.fillWidth: true
+                labelText: qsTr("Automatically send crash reports")
+                checked: UtilsAdapter.getAppValue(Settings.EnableAutomaticCrashReporting)
+
+                onSwitchToggled: {
+                    UtilsAdapter.setAppValue(Settings.EnableAutomaticCrashReporting, checked);
+                    crashReporter.syncHandlerWithSettings();
+                }
+            }
 
-                TextMetrics {
-                    id: enableTroubleshootingButtonTextSize
-                    font.weight: Font.Bold
-                    font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
-                    font.capitalization: Font.AllUppercase
-                    text: enableTroubleshootingButton.text
+            RowLayout {
+                id: rawLayout
+                Text {
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: 30
+                    Layout.rightMargin: JamiTheme.preferredMarginSize
+
+                    text: JamiStrings.troubleshootText
+                    font.pointSize: JamiTheme.settingsFontSize
+                    font.kerning: true
+                    wrapMode: Text.WordWrap
+                    horizontalAlignment: Text.AlignLeft
+                    verticalAlignment: Text.AlignVCenter
+
+                    color: JamiTheme.textColor
                 }
 
-                Layout.alignment: Qt.AlignRight
+                MaterialButton {
+                    id: enableTroubleshootingButton
+
+                    TextMetrics {
+                        id: enableTroubleshootingButtonTextSize
+                        font.weight: Font.Bold
+                        font.pixelSize: JamiTheme.wizardViewButtonFontPixelSize
+                        font.capitalization: Font.AllUppercase
+                        text: enableTroubleshootingButton.text
+                    }
+
+                    Layout.alignment: Qt.AlignRight
 
-                preferredWidth: enableTroubleshootingButtonTextSize.width + 2 * JamiTheme.buttontextWizzardPadding
-                buttontextHeightMargin: JamiTheme.buttontextHeightMargin
+                    preferredWidth: enableTroubleshootingButtonTextSize.width + 2 * JamiTheme.buttontextWizzardPadding
+                    buttontextHeightMargin: JamiTheme.buttontextHeightMargin
 
-                primary: true
+                    primary: true
 
-                text: JamiStrings.troubleshootButton
-                toolTipText: JamiStrings.troubleshootButton
+                    text: JamiStrings.troubleshootButton
+                    toolTipText: JamiStrings.troubleshootButton
 
-                onClicked: {
-                    LogViewWindowCreation.createlogViewWindowObject();
-                    LogViewWindowCreation.showLogViewWindow();
+                    onClicked: {
+                        LogViewWindowCreation.createlogViewWindowObject();
+                        LogViewWindowCreation.showLogViewWindow();
+                    }
                 }
             }
         }
-- 
GitLab