Merge pull request #6803 from Ultimaker/sentry_crash_integration

Sentry crash integration
This commit is contained in:
ninovanhooff 2019-12-19 14:29:07 +01:00 committed by GitHub
commit 4773c4eaf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 183 additions and 44 deletions

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import platform import platform
@ -7,13 +7,14 @@ import faulthandler
import tempfile import tempfile
import os import os
import os.path import os.path
import time import uuid
import json import json
import ssl import locale
import urllib.request from typing import cast
import urllib.error
import certifi from sentry_sdk.hub import Hub
from sentry_sdk.utils import event_from_exception
from sentry_sdk import configure_scope
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
@ -24,7 +25,6 @@ from UM.Logger import Logger
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Resources import Resources from UM.Resources import Resources
from cura import ApplicationMetadata from cura import ApplicationMetadata
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -46,9 +46,8 @@ skip_exception_types = [
GeneratorExit GeneratorExit
] ]
class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura"
class CrashHandler:
def __init__(self, exception_type, value, tb, has_started = True): def __init__(self, exception_type, value, tb, has_started = True):
self.exception_type = exception_type self.exception_type = exception_type
self.value = value self.value = value
@ -56,21 +55,20 @@ class CrashHandler:
self.has_started = has_started self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication self.dialog = None # Don't create a QDialog before there is a QApplication
# While we create the GUI, the information will be stored for sending afterwards
self.data = dict()
self.data["time_stamp"] = time.time()
Logger.log("c", "An uncaught error has occurred!") Logger.log("c", "An uncaught error has occurred!")
for line in traceback.format_exception(exception_type, value, tb): for line in traceback.format_exception(exception_type, value, tb):
for part in line.rstrip("\n").split("\n"): for part in line.rstrip("\n").split("\n"):
Logger.log("c", part) Logger.log("c", part)
self.data = {}
# If Cura has fully started, we only show fatal errors. # If Cura has fully started, we only show fatal errors.
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash # If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
# without any information. # without any information.
if has_started and exception_type in skip_exception_types: if has_started and exception_type in skip_exception_types:
return return
with configure_scope() as scope:
scope.set_tag("during_startup", not has_started)
if not has_started: if not has_started:
self._send_report_checkbox = None self._send_report_checkbox = None
self.early_crash_dialog = self._createEarlyCrashDialog() self.early_crash_dialog = self._createEarlyCrashDialog()
@ -179,25 +177,42 @@ class CrashHandler:
try: try:
from UM.Application import Application from UM.Application import Application
self.cura_version = Application.getInstance().getVersion() self.cura_version = Application.getInstance().getVersion()
self.cura_locale = Application.getInstance().getPreferences().getValue("general/language")
except: except:
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown") self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
self.cura_locale = "??_??"
self.data["cura_version"] = self.cura_version
self.data["os"] = {"type": platform.system(), "version": platform.version()}
self.data["qt_version"] = QT_VERSION_STR
self.data["pyqt_version"] = PYQT_VERSION_STR
self.data["locale_os"] = locale.getlocale(locale.LC_MESSAGES)[0] if hasattr(locale, "LC_MESSAGES") else \
locale.getdefaultlocale()[0]
self.data["locale_cura"] = self.cura_locale
crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>" crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label Cura build type", "Cura build type") + ":</b> " + str(ApplicationMetadata.CuraBuildType) + "<br/>" crash_info += "<b>" + catalog.i18nc("@label", "Cura language") + ":</b> " + str(self.cura_locale) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label", "OS language") + ":</b> " + str(self.data["locale_os"]) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>" crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>" crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>" crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>"
crash_info += "<b>" + catalog.i18nc("@label OpenGL version", "OpenGL") + ":</b> " + str(self._getOpenGLInfo()) + "<br/>" crash_info += "<b>" + catalog.i18nc("@label OpenGL version", "OpenGL") + ":</b> " + str(self._getOpenGLInfo()) + "<br/>"
label.setText(crash_info) label.setText(crash_info)
layout.addWidget(label) layout.addWidget(label)
group.setLayout(layout) group.setLayout(layout)
self.data["cura_version"] = self.cura_version with configure_scope() as scope:
self.data["cura_build_type"] = ApplicationMetadata.CuraBuildType scope.set_tag("qt_version", QT_VERSION_STR)
self.data["os"] = {"type": platform.system(), "version": platform.version()} scope.set_tag("pyqt_version", PYQT_VERSION_STR)
self.data["qt_version"] = QT_VERSION_STR scope.set_tag("os", platform.system())
self.data["pyqt_version"] = PYQT_VERSION_STR scope.set_tag("os_version", platform.version())
scope.set_tag("locale_os", self.data["locale_os"])
scope.set_tag("locale_cura", self.cura_locale)
scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion)
scope.set_user({"id": str(uuid.getnode())})
return group return group
@ -215,6 +230,30 @@ class CrashHandler:
self.data["opengl"] = {"version": opengl_instance.getOpenGLVersion(), "vendor": opengl_instance.getGPUVendorName(), "type": opengl_instance.getGPUType()} self.data["opengl"] = {"version": opengl_instance.getOpenGLVersion(), "vendor": opengl_instance.getGPUVendorName(), "type": opengl_instance.getGPUType()}
active_machine_definition_id = "unknown"
active_machine_manufacturer = "unknown"
try:
from cura.CuraApplication import CuraApplication
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager()
global_stack = machine_manager.activeMachine
if global_stack is None:
active_machine_definition_id = "empty"
active_machine_manufacturer = "empty"
else:
active_machine_definition_id = global_stack.definition.getId()
active_machine_manufacturer = global_stack.definition.getMetaDataEntry("manufacturer", "unknown")
except:
pass
with configure_scope() as scope:
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
scope.set_tag("gpu_type", opengl_instance.getGPUType())
scope.set_tag("active_machine", active_machine_definition_id)
scope.set_tag("active_machine_manufacturer", active_machine_manufacturer)
return info return info
def _exceptionInfoWidget(self): def _exceptionInfoWidget(self):
@ -296,6 +335,10 @@ class CrashHandler:
"module_name": module_name, "version": module_version, "is_plugin": isPlugin} "module_name": module_name, "version": module_version, "is_plugin": isPlugin}
self.data["exception"] = exception_dict self.data["exception"] = exception_dict
with configure_scope() as scope:
scope.set_tag("is_plugin", isPlugin)
scope.set_tag("module", module_name)
return group return group
def _logInfoWidget(self): def _logInfoWidget(self):
@ -353,31 +396,11 @@ class CrashHandler:
# Before sending data, the user comments are stored # Before sending data, the user comments are stored
self.data["user_info"] = self.user_description_text_area.toPlainText() self.data["user_info"] = self.user_description_text_area.toPlainText()
# Convert data to bytes
binary_data = json.dumps(self.data).encode("utf-8")
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(cafile = certifi.where())
# Submit data
kwoptions = {"data": binary_data,
"timeout": 5,
"context": context}
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
if not self.has_started:
print("Sending crash report info to [%s]...\n" % self.crash_url)
try: try:
f = urllib.request.urlopen(self.crash_url, **kwoptions) hub = Hub.current
Logger.log("i", "Sent crash report info.") event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
if not self.has_started: hub.capture_event(event, hint=hint)
print("Sent crash report info.\n") hub.flush()
f.close()
except urllib.error.HTTPError as e:
Logger.logException("e", "An HTTP error occurred while trying to send crash report")
if not self.has_started:
print("An HTTP error occurred while trying to send crash report: %s" % e)
except Exception as e: # We don't want any exception to cause problems except Exception as e: # We don't want any exception to cause problems
Logger.logException("e", "An exception occurred while trying to send crash report") Logger.logException("e", "An exception occurred while trying to send crash report")
if not self.has_started: if not self.has_started:

View File

@ -9,8 +9,11 @@ import os
import sys import sys
from UM.Platform import Platform from UM.Platform import Platform
from cura import ApplicationMetadata
from cura.ApplicationMetadata import CuraAppName from cura.ApplicationMetadata import CuraAppName
import sentry_sdk
parser = argparse.ArgumentParser(prog = "cura", parser = argparse.ArgumentParser(prog = "cura",
add_help = False) add_help = False)
parser.add_argument("--debug", parser.add_argument("--debug",
@ -18,8 +21,25 @@ parser.add_argument("--debug",
default = False, default = False,
help = "Turn on the debug mode by setting this option." help = "Turn on the debug mode by setting this option."
) )
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
sentry_env = "production"
if ApplicationMetadata.CuraVersion == "master":
sentry_env = "development"
try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
sentry_env = "nightly"
except IndexError:
pass
sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564",
environment = sentry_env,
release = "cura%s" % ApplicationMetadata.CuraVersion,
default_integrations = False,
max_breadcrumbs = 300,
server_name = "cura")
if not known_args["debug"]: if not known_args["debug"]:
def get_cura_dir_path(): def get_cura_dir_path():
if Platform.isWindows(): if Platform.isWindows():

View File

@ -0,0 +1,55 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import LogOutput
from typing import Set
from sentry_sdk import add_breadcrumb
from typing import Optional
import os
home_dir = os.path.expanduser("~")
class SentryLogger(LogOutput):
# Sentry (https://sentry.io) is the service that Cura uses for logging crashes. This logger ensures that the
# regular log entries that we create are added as breadcrumbs so when a crash actually happens, they are already
# processed and ready for sending.
# Note that this only prepares them for sending. It only sends them when the user actually agrees to sending the
# information.
_levels = {
"w": "warning",
"i": "info",
"c": "fatal",
"e": "error",
"d": "debug"
}
def __init__(self) -> None:
super().__init__()
self._show_once = set() # type: Set[str]
## Log the message to the sentry hub as a breadcrumb
# \param log_type "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once")
# \param message String containing message to be logged
def log(self, log_type: str, message: str) -> None:
level = self._translateLogType(log_type)
message = self._pruneSensitiveData(message)
if level is None:
if message not in self._show_once:
level = self._translateLogType(log_type[0])
if level is not None:
self._show_once.add(message)
add_breadcrumb(level = level, message = message)
else:
add_breadcrumb(level = level, message = message)
@staticmethod
def _pruneSensitiveData(message):
if home_dir in message:
message = message.replace(home_dir, "<user_home>")
return message
@staticmethod
def _translateLogType(log_type: str) -> Optional[str]:
return SentryLogger._levels.get(log_type)

View File

@ -0,0 +1,16 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Dict, Any
from . import SentryLogger
if TYPE_CHECKING:
from UM.Application import Application
def getMetaData() -> Dict[str, Any]:
return {}
def register(app: "Application") -> Dict[str, Any]:
return {"logger": SentryLogger.SentryLogger()}

View File

@ -0,0 +1,8 @@
{
"name": "Sentry Logger",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter",
"api": "7.0",
"i18n-catalog": "cura"
}

View File

@ -407,6 +407,23 @@
} }
} }
}, },
"SentryLogger": {
"package_info": {
"package_id": "SentryLogger",
"package_type": "plugin",
"display_name": "Sentry Logger",
"description": "Logs certain events so that they can be used by the crash reporter",
"package_version": "1.0.0",
"sdk_version": "7.0.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
"display_name": "Ultimaker B.V.",
"email": "plugins@ultimaker.com",
"website": "https://ultimaker.com"
}
}
},
"SimulationView": { "SimulationView": {
"package_info": { "package_info": {
"package_id": "SimulationView", "package_id": "SimulationView",