#!/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