spiegel-keyman/linux/keyman-config/keyman_config/sentry_handling.py
2025-10-13 18:29:24 +02:00

213 lines
7.8 KiB
Python

#!/usr/bin/python3
'''
Keyman is copyright (C) SIL Global. MIT License.
Implements the Sentry error handling
'''
import getpass
import hashlib
import importlib
import logging
import os
import platform
import sys
import traceback
from keyman_config.keyman_option import KeymanOption
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
is_sentry_sdk = False
class SentryErrorHandling:
def __init__(self) -> None:
self.error_reporting_setting = KeymanOption('error-reporting')
def initialize_sentry(self):
(enabled, reason) = self.is_sentry_enabled()
if not enabled:
if not self._is_running_tests():
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_running_tests():
return (False, 'Running 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.error_reporting_setting.bind_checkbutton(button, 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 add_breadcrumb(self, category, message, level='info'):
global is_sentry_sdk
if not self.is_sentry_enabled() or not is_sentry_sdk:
return
# sentry_sdk = importlib.import_module('sentry_sdk')
import sentry_sdk
sentry_sdk.add_breadcrumb(category=category, message=message, level=level)
def _get_environ_nosentry(self):
keyman_nosentry = os.environ.get('KEYMAN_NOSENTRY')
return keyman_nosentry and (int(keyman_nosentry) == 1)
def _is_running_tests(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
if platform.node() == 'autopkgtest':
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.error_reporting_setting.set(enabled)
def _get_setting(self) -> bool:
return self.error_reporting_setting.get()
def _on_sentry_reporting_toggled(self, settings, key):
self._handle_enabled(self._get_setting())
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
global is_sentry_sdk
sentry_sdk = importlib.import_module('sentry_sdk')
from sentry_sdk import configure_scope, set_user
from sentry_sdk.integrations.logging import LoggingIntegration
is_sentry_sdk = True
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"
os_release = None
dist = None
try:
os_release = platform.freedesktop_os_release()
dist = os_release['VERSION_CODENAME']
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 Exception:
logging.debug(
'Got exception trying to access platform.freedesktop_os_release() method or os_release information')
sentry_sdk.init(
dsn=SentryUrl,
environment=__environment__,
release=__versiongittag__,
integrations=[sentry_logging],
dist=dist,
in_app_include=['keyman_config'],
before_send=self._before_send
)
md5Hash = hashlib.md5()
md5Hash.update(getpass.getuser().encode())
set_user({'id': md5Hash.hexdigest()})
with configure_scope() as scope:
self._set_system_info(scope, os_release)
logging.info("Initialized Sentry error reporting")
def _set_system_info(self, scope, os_release):
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())
if 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'])
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