mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 15:45:56 +08:00
Merge pull request #6803 from Ultimaker/sentry_crash_integration
Sentry crash integration
This commit is contained in:
commit
4773c4eaf3
@ -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:
|
||||||
|
20
cura_app.py
20
cura_app.py
@ -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():
|
||||||
|
55
plugins/SentryLogger/SentryLogger.py
Normal file
55
plugins/SentryLogger/SentryLogger.py
Normal 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)
|
16
plugins/SentryLogger/__init__.py
Normal file
16
plugins/SentryLogger/__init__.py
Normal 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()}
|
8
plugins/SentryLogger/plugin.json
Normal file
8
plugins/SentryLogger/plugin.json
Normal 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"
|
||||||
|
}
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user