spiegel-keyman/linux/keyman-config/keyman_config/sentry_handling.py
Eberhard Beilharz d7a72e6baf
fix(linux): Fix crash initializing Sentry with Python < 3.10
Ubuntu 20.04 comes with Python 3.8 which doesn't have
`platform.freedesktop_os_release()`. This change catches the exception
if this happens, as well as other exceptions we might get so that the
initialization of Sentry will succeed.

Fixes #9773.
2023-10-16 18:44:18 +02:00

177 lines
7 KiB
Python

#!/usr/bin/python3
import getpass
import hashlib
import importlib
import logging
import os
import platform
import sys
import traceback
from keyman_config.version import (
__version__,
__versionwithtag__,
__versiongittag__,
__majorversion__,
__releaseversion__,
__tier__,
__pkgversion__,
__environment__,
__uploadsentry__
)
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk
class SentryErrorHandling:
def __init__(self) -> None:
self.settings = Gio.Settings.new('com.keyman.options')
def initialize_sentry(self):
(enabled, reason) = self.is_sentry_enabled()
if not enabled:
print(reason, file=sys.stderr)
logging.info(reason)
return (enabled, reason)
else:
try:
self._sentry_sdk_initialize()
except ImportError:
try:
self._raven_initialize()
except ImportError:
# even raven is not available. This is the case on Ubuntu 16.04. Just ignore.
print(_('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.'),
file=sys.stderr)
logging.info('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.')
return (False, _('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.'))
return (True, '')
def is_sentry_enabled(self):
if self._is_unit_test():
return (False, 'Running unit tests, not reporting to Sentry')
elif self._get_environ_nosentry():
return (False, 'Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set')
elif not __uploadsentry__:
return (False, f'Not reporting to Sentry because UPLOAD_SENTRY is false ({__environment__})')
elif not self._get_setting():
return (False, 'Not reporting to Sentry because disabled in GSettings')
return (True, 'Reporting to Sentry')
def is_sentry_disabled_by_variable(self):
return self._get_environ_nosentry() or not __uploadsentry__
def bind_checkbutton(self, button: Gtk.CheckButton):
self.settings.bind("error-reporting", button, "active", Gio.SettingsBindFlags.NO_SENSITIVITY)
self.settings.connect("changed::error-reporting", self._on_sentry_reporting_toggled)
def set_enabled(self, enabled):
assert not self.is_sentry_disabled_by_variable()
was_enabled = self.is_sentry_enabled()
self._save_setting(enabled)
if enabled != was_enabled:
self._handle_enabled(enabled)
def _get_environ_nosentry(self):
keyman_nosentry = os.environ.get('KEYMAN_NOSENTRY')
return keyman_nosentry and (int(keyman_nosentry) == 1)
def _is_unit_test(self): # sourcery skip: use-any, use-next
# The suggested refactorings (using any() or next()) don't work
# when testing on Ubuntu 20.04
for line in traceback.format_stack():
if '/unittest/' in line:
return True
return False
def _handle_enabled(self, enabled):
if enabled:
self.initialize_sentry()
else:
self._close_sentry()
def _save_setting(self, enabled: bool):
self.settings.set_boolean('error-reporting', enabled)
def _get_setting(self) -> bool:
return self.settings.get_boolean('error-reporting')
def _on_sentry_reporting_toggled(self, settings, key):
self._handle_enabled(self.settings.get_boolean('error-reporting'))
def _close_sentry(self):
from sentry_sdk import Hub
logging.info("Shutting down Sentry error reporting")
client = Hub.current.client
if client is not None:
client.close(timeout=2.0)
def _sentry_sdk_initialize(self):
# Try new sentry-sdk first
sentry_sdk = importlib.import_module('sentry_sdk')
from sentry_sdk import configure_scope, set_user
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_logging = LoggingIntegration(
level=logging.INFO, # Capture info and above as breadcrumbs
event_level=logging.CRITICAL # Send critical errors as events
)
SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357@o1005580.ingest.sentry.io/5983525"
sentry_sdk.init(
dsn=SentryUrl,
environment=__environment__,
release=__versiongittag__,
integrations=[sentry_logging],
before_send=self._before_send
)
hash = hashlib.md5()
hash.update(getpass.getuser().encode())
set_user({'id': hash.hexdigest()})
with configure_scope() as scope:
scope.set_tag("app", os.path.basename(sys.argv[0]))
scope.set_tag("pkgversion", __pkgversion__)
scope.set_tag("platform", platform.platform())
scope.set_tag("system", platform.system())
scope.set_tag("tier", __tier__)
scope.set_tag("device", platform.node())
try:
os_release = platform.freedesktop_os_release()
scope.set_tag('os', os_release['PRETTY_NAME'])
scope.set_tag('os.name', os_release['NAME'])
if 'VERSION' in os_release:
scope.set_tag('os.version', os_release['VERSION'])
except OSError as e:
logging.debug(f'System does not have os_release file: {e.strerror}')
except AttributeError:
logging.debug('System does not have platform.freedesktop_os_release() method')
except:
logging.debug(
'Got exception trying to access platform.freedesktop_os_release() method or os_release information')
logging.info("Initialized Sentry error reporting")
def _raven_initialize(self):
# sentry-sdk is not available, so use older raven
raven = importlib.import_module('raven')
from raven import Client
# Note, legacy raven API requires secret (https://github.com/keymanapp/keyman/pull/5787#discussion_r721457909)
SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357:e6d5a81ee6944fc79bd9f0cbb1f2c2a4@o1005580.ingest.sentry.io/5983525"
client = Client(SentryUrl, environment=__environment__, release=__versiongittag__)
client.user_context({'id': hash(getpass.getuser())})
client.tags_context({
'app': os.path.basename(sys.argv[0]),
'pkgversion': __pkgversion__,
'platform': platform.platform(),
'system': platform.system(),
'tier': __tier__,
})
logging.info("Initialized Sentry error reporting (raven)")
def _before_send(self, event, hint):
if 'exc_info' in hint:
exc_type, exc_value, tb = hint['exc_info']
if isinstance(exc_value, KeyboardInterrupt):
# Ignore KeyboardInterrupt exception
return None
return event