mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 12:15:58 +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.
|
||||
|
||||
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:
|
||||
|
20
cura_app.py
20
cura_app.py
@ -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():
|
||||
|
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": {
|
||||
"package_info": {
|
||||
"package_id": "SimulationView",
|
||||
|
Loading…
x
Reference in New Issue
Block a user