diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py
index 1ec00787d7..09fda25a73 100644
--- a/cura/CrashHandler.py
+++ b/cura/CrashHandler.py
@@ -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 = "" + catalog.i18nc("@label Cura version number", "Cura version") + ": " + str(self.cura_version) + "
"
- crash_info += "" + catalog.i18nc("@label Cura build type", "Cura build type") + ": " + str(ApplicationMetadata.CuraBuildType) + "
"
+ crash_info += "" + catalog.i18nc("@label", "Cura language") + ": " + str(self.cura_locale) + "
"
+ crash_info += "" + catalog.i18nc("@label", "OS language") + ": " + str(self.data["locale_os"]) + "
"
crash_info += "" + catalog.i18nc("@label Type of platform", "Platform") + ": " + str(platform.platform()) + "
"
crash_info += "" + catalog.i18nc("@label", "Qt version") + ": " + str(QT_VERSION_STR) + "
"
crash_info += "" + catalog.i18nc("@label", "PyQt version") + ": " + str(PYQT_VERSION_STR) + "
"
crash_info += "" + catalog.i18nc("@label OpenGL version", "OpenGL") + ": " + str(self._getOpenGLInfo()) + "
"
+
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:
diff --git a/cura_app.py b/cura_app.py
index e14b4410bc..51f9041e86 100755
--- a/cura_app.py
+++ b/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():
diff --git a/plugins/SentryLogger/SentryLogger.py b/plugins/SentryLogger/SentryLogger.py
new file mode 100644
index 0000000000..8367cfc26e
--- /dev/null
+++ b/plugins/SentryLogger/SentryLogger.py
@@ -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, "")
+ return message
+
+ @staticmethod
+ def _translateLogType(log_type: str) -> Optional[str]:
+ return SentryLogger._levels.get(log_type)
diff --git a/plugins/SentryLogger/__init__.py b/plugins/SentryLogger/__init__.py
new file mode 100644
index 0000000000..c464de5fd4
--- /dev/null
+++ b/plugins/SentryLogger/__init__.py
@@ -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()}
diff --git a/plugins/SentryLogger/plugin.json b/plugins/SentryLogger/plugin.json
new file mode 100644
index 0000000000..fef4e1c554
--- /dev/null
+++ b/plugins/SentryLogger/plugin.json
@@ -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"
+}
diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json
index 22642cd38e..7cb229150c 100644
--- a/resources/bundled_packages/cura.json
+++ b/resources/bundled_packages/cura.json
@@ -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",