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.
import platform
@ -7,13 +7,14 @@ import faulthandler
import tempfile
import os
import os.path
import time
import uuid
import json
import ssl
import urllib.request
import urllib.error
import locale
from typing import cast
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.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.i18n import i18nCatalog
from UM.Resources import Resources
from cura import ApplicationMetadata
catalog = i18nCatalog("cura")
@ -46,9 +46,8 @@ skip_exception_types = [
GeneratorExit
]
class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura"
class CrashHandler:
def __init__(self, exception_type, value, tb, has_started = True):
self.exception_type = exception_type
self.value = value
@ -56,21 +55,20 @@ class CrashHandler:
self.has_started = has_started
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!")
for line in traceback.format_exception(exception_type, value, tb):
for part in line.rstrip("\n").split("\n"):
Logger.log("c", part)
self.data = {}
# 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
# without any information.
if has_started and exception_type in skip_exception_types:
return
with configure_scope() as scope:
scope.set_tag("during_startup", not has_started)
if not has_started:
self._send_report_checkbox = None
self.early_crash_dialog = self._createEarlyCrashDialog()
@ -179,25 +177,42 @@ class CrashHandler:
try:
from UM.Application import Application
self.cura_version = Application.getInstance().getVersion()
self.cura_locale = Application.getInstance().getPreferences().getValue("general/language")
except:
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 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", "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 OpenGL version", "OpenGL") + ":</b> " + str(self._getOpenGLInfo()) + "<br/>"
label.setText(crash_info)
layout.addWidget(label)
group.setLayout(layout)
self.data["cura_version"] = self.cura_version
self.data["cura_build_type"] = ApplicationMetadata.CuraBuildType
self.data["os"] = {"type": platform.system(), "version": platform.version()}
self.data["qt_version"] = QT_VERSION_STR
self.data["pyqt_version"] = PYQT_VERSION_STR
with configure_scope() as scope:
scope.set_tag("qt_version", QT_VERSION_STR)
scope.set_tag("pyqt_version", PYQT_VERSION_STR)
scope.set_tag("os", platform.system())
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
@ -215,6 +230,30 @@ class CrashHandler:
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
def _exceptionInfoWidget(self):
@ -296,6 +335,10 @@ class CrashHandler:
"module_name": module_name, "version": module_version, "is_plugin": isPlugin}
self.data["exception"] = exception_dict
with configure_scope() as scope:
scope.set_tag("is_plugin", isPlugin)
scope.set_tag("module", module_name)
return group
def _logInfoWidget(self):
@ -353,31 +396,11 @@ class CrashHandler:
# Before sending data, the user comments are stored
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:
f = urllib.request.urlopen(self.crash_url, **kwoptions)
Logger.log("i", "Sent crash report info.")
if not self.has_started:
print("Sent crash report info.\n")
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)
hub = Hub.current
event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
hub.capture_event(event, hint=hint)
hub.flush()
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")
if not self.has_started:

View File

@ -9,8 +9,11 @@ import os
import sys
from UM.Platform import Platform
from cura import ApplicationMetadata
from cura.ApplicationMetadata import CuraAppName
import sentry_sdk
parser = argparse.ArgumentParser(prog = "cura",
add_help = False)
parser.add_argument("--debug",
@ -18,8 +21,25 @@ parser.add_argument("--debug",
default = False,
help = "Turn on the debug mode by setting this option."
)
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"]:
def get_cura_dir_path():
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": {
"package_info": {
"package_id": "SimulationView",