feat(linux): improve Sentry error reports

- add breadcrumbs with keyboard details
- add some more information about system
- mark files in `km_config` as to the app belonging
- some refactorings

Test-bot: skip
This commit is contained in:
Eberhard Beilharz 2025-09-16 15:44:59 +02:00
parent f3643827d1
commit 596371dde2
No known key found for this signature in database
GPG key ID: E9140597606020D3
9 changed files with 117 additions and 62 deletions

View file

@ -110,27 +110,30 @@ def _set_dbus_started_for_session(value):
def verify_dbus_running():
if not 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
try:
# Seems dbus isn't running for the current user. Try to start it
# and set these environment variables
logging.info('Starting dbus with dbus-launch')
stdout = subprocess.run(
('dbus-launch', '--exit-with-session'),
stdout=subprocess.PIPE, check=False).stdout
_set_dbus_started_for_session(True)
lines = stdout.decode('utf-8').splitlines()
for line in lines:
equal_sign = line.find('=')
if equal_sign <= 0:
logging.warning('Got unexpected line from dbus-launch: %s', line)
continue
name = line[:equal_sign]
value = line[equal_sign+1:]
logging.debug('Setting environment %s=%s', name, value)
os.environ[name] = value
except Exception as e:
logging.error('Starting dbus-launch failed with %s', e)
if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
# already running - nothing to do
return
try:
# Seems dbus isn't running for the current user. Try to start it
# and set these environment variables
logging.info('Starting dbus with dbus-launch')
stdout = subprocess.run(
('dbus-launch', '--exit-with-session'),
stdout=subprocess.PIPE, check=False).stdout
_set_dbus_started_for_session(True)
lines = stdout.decode('utf-8').splitlines()
for line in lines:
equal_sign = line.find('=')
if equal_sign <= 0:
logging.warning('Got unexpected line from dbus-launch: %s', line)
continue
name = line[:equal_sign]
value = line[equal_sign+1:]
logging.debug('Setting environment %s=%s', name, value)
os.environ[name] = value
except Exception as e:
logging.error('Starting dbus-launch failed with %s', e)
def add_standard_arguments(parser):

View file

@ -94,8 +94,7 @@ class DownloadKmpWindow(Gtk.Dialog):
qs = urllib.parse.parse_qs(parsed.query)
package_id = parsed.path.split('/')[-1]
downloadfile = os.path.join(get_download_folder(), package_id)
download_url = KeymanComUrl + '/go/package/download/' + package_id + \
'?platform=linux&tier=' + __tier__
download_url = f'{KeymanComUrl}/go/package/download/{package_id}?platform=linux&tier={__tier__}'
if 'bcp47' in qs:
self.language = qs['bcp47'][0]
download_url += '&bcp47=' + qs['bcp47'][0]

View file

@ -13,6 +13,7 @@ from gi.repository import GObject
from keyman_config import _
from keyman_config import KeymanApiUrl, KeymanDownloadsUrl
from keyman_config.deprecated_decorator import deprecated
from keyman_config.sentry_handling import SentryErrorHandling
class InstallLocation(GObject.GEnum):
@ -105,7 +106,7 @@ def get_keyboard_data(keyboardID, weekCache=False):
dict: Keyboard data
"""
logging.info('Getting data for keyboard %s', keyboardID)
api_url = KeymanApiUrl + '/keyboard/' + keyboardID
api_url = f'{KeymanApiUrl}/keyboard/{keyboardID}'
logging.debug('At URL %s', api_url)
cache_dir = keyman_cache_dir()
current_dir = os.getcwd()
@ -123,10 +124,7 @@ def get_keyboard_data(keyboardID, weekCache=False):
logging.debug('Time: {0} / Used Cache: {1}'.format(now, _did_use_cache(response)))
os.chdir(current_dir)
_uninstall_cache()
if response.status_code == 200:
return response.json()
else:
return None
return response.json() if response.status_code == 200 else None
def get_download_folder():
@ -234,6 +232,8 @@ def download_kmp_file(url, kmpfile, cache=False):
"""
logging.info('Download URL: %s', url)
downloadfile = None
sentry = SentryErrorHandling()
sentry.add_breadcrumb(category='download', message=f'Downloading keyboard from {url}')
if cache:
cache_dir = keyman_cache_dir()

View file

@ -3,6 +3,7 @@ import logging
import os
from keyman_config.gsettings import GSettings
from keyman_config.sentry_handling import SentryErrorHandling
# pylint: disable=global-statement
@ -52,15 +53,20 @@ def _reset_gnome_shell():
def get_ibus_keyboard_id(keyboard, packageDir, language=None, ignore_language=False):
if not keyboard:
return None
sentry = SentryErrorHandling()
kmx_file = os.path.join(packageDir, keyboard['id'] + ".kmx")
if ignore_language:
sentry.add_breadcrumb(category='keyboard', message=f'ibus_keyboard_id(1): {kmx_file}')
return kmx_file
if language is not None and language != '':
logging.debug(language)
sentry.add_breadcrumb(category='keyboard', message=f'ibus_keyboard_id(2): {language}:{kmx_file}')
return f"{language}:{kmx_file}"
if "languages" in keyboard and len(keyboard["languages"]) > 0:
firstLanguage = keyboard["languages"][0]
logging.debug(firstLanguage)
if 'id' in firstLanguage:
sentry.add_breadcrumb(category='keyboard', message=f'ibus_keyboard_id(3): {firstLanguage["id"]}:{kmx_file}')
return f"{firstLanguage['id']}:{kmx_file}"
sentry.add_breadcrumb(category='keyboard', message=f'ibus_keyboard_id(4): {kmx_file}')
return kmx_file

View file

@ -6,6 +6,7 @@ from zipfile import is_zipfile
from keyman_config import KeymanComUrl, __tier__
from keyman_config.get_kmp import download_kmp_file, get_download_folder
from keyman_config.install_window import InstallKmpWindow
from keyman_config.sentry_handling import SentryErrorHandling
def download_and_install_package(url):
@ -18,6 +19,7 @@ def download_and_install_package(url):
url: a .kmp file, a keyman:// URL, or a file:// URL pointing to a .kmp file,
possibly with a bcp47=<language> specified
"""
sentry = SentryErrorHandling()
parsedUrl = urlparse(url)
bcp47 = _extract_bcp47(parsedUrl.query)
@ -34,11 +36,13 @@ def download_and_install_package(url):
downloadFile = os.path.join(get_download_folder(), packageId)
downloadUrl = f'{KeymanComUrl}/go/package/download/{packageId}?platform=linux&tier={__tier__}'
sentry.add_breadcrumb(category='download', message=f'About to download keyboard {packageId} from {downloadUrl}')
packageFile = download_kmp_file(downloadUrl, downloadFile)
if packageFile is None:
return
elif parsedUrl.scheme in ['', 'file']:
packageFile = parsedUrl.path
sentry.add_breadcrumb(category='keyboard', message=f'adding keyboard from file {packageFile}')
else:
logging.error(f"Invalid URL: {url}")
return

View file

@ -23,6 +23,7 @@ from keyman_config.gnome_keyboards_util import (GnomeKeyboardsUtil,
from keyman_config.ibus_util import get_ibus_bus, install_to_ibus, restart_ibus
from keyman_config.kmpmetadata import KMFileTypes, get_metadata
from keyman_config.kvk2ldml import convert_kvk_to_ldml, output_ldml
from keyman_config.sentry_handling import SentryErrorHandling
# TODO userdir install
# special processing for kmn if needed
@ -105,6 +106,9 @@ class InstallKmp():
self.kmpdocdir = get_keyman_doc_dir(area, self.packageID)
self.kmpfontdir = get_keyman_font_dir(area, self.packageID)
sentry = SentryErrorHandling()
sentry.add_breadcrumb(category='install', message=f'Installing kmp: {self.packageID} from "{inputfile}" for lang: {language}; area: {"OS" if area == 1 else "Shared" if area == 2 else "User" if area == 3 else "Unknown"}')
if not os.path.isfile(inputfile):
message = _("File {kmpfile} doesn't exist").format(kmpfile=inputfile)
logging.error("install_kmp.py: %s", message)
@ -258,6 +262,9 @@ class InstallKmp():
if not language:
language = self._add_custom_keyboard(firstKeyboard, packageDir, requested_language)
sentry = SentryErrorHandling()
sentry.add_breadcrumb(category='install', message=f'Installing keyboards for lang: {language}')
if is_fcitx_running():
return self._install_keyboards_to_fcitx()

View file

@ -11,6 +11,7 @@ from json.decoder import JSONDecodeError
import magic
from keyman_config import secure_lookup
from keyman_config.sentry_handling import SentryErrorHandling
class KMFileTypes(Enum):
@ -455,6 +456,7 @@ def parsemetadata(jsonfile, verbose=False):
"""
info = system = keyboards = files = options = nonexistent = None
extracted_dir = os.path.dirname(jsonfile)
sentry = SentryErrorHandling()
logging.debug("parsemetadata: reading file:%s dir:%s", jsonfile, extracted_dir)
@ -496,6 +498,11 @@ def parsemetadata(jsonfile, verbose=False):
print_options(options)
print_keyboards(keyboards)
print_files(files, extracted_dir)
if system:
sentry.add_breadcrumb(
category='keyboard', message=f'kmp.json: pkg: "{secure_lookup(info, "name", "description")}", KeymanDeveloperVersion: {secure_lookup(system, "keymanDeveloperVersion")}, dir: {os.path.basename(extracted_dir)}')
return info, system, options, keyboards, files
@ -532,19 +539,22 @@ def get_and_convert_infdata(tmpdirname):
see kmpmetadata.parseinfdata for details
"""
kmpinf = os.path.join(tmpdirname, "kmp.inf")
if os.path.isfile(kmpinf):
info, system, options, keyboards, files = parseinfdata(kmpinf, False)
j = infmetadata_to_json(info, system, options, keyboards, files)
kmpjson = os.path.join(tmpdirname, "kmp.json")
try:
with open(kmpjson, "w") as write_file:
print(j, file=write_file)
except Exception as e:
logging.warning('Exception %s writing metadata %s %s', type(e), kmpjson, e.args)
return info, system, options, keyboards, files
else:
if not os.path.isfile(kmpinf):
return None, None, None, None, None
sentry = SentryErrorHandling()
info, system, options, keyboards, files = parseinfdata(kmpinf, False)
j = infmetadata_to_json(info, system, options, keyboards, files)
kmpjson = os.path.join(tmpdirname, "kmp.json")
try:
with open(kmpjson, "w") as write_file:
print(j, file=write_file)
except Exception as e:
logging.warning('Exception %s writing metadata %s %s', type(e), kmpjson, e.args)
sentry.add_breadcrumb(
category='keyboard', message=f'kmp.inf: pkg: "{secure_lookup(info, "name", "description")}", KeymanDeveloperVersion: {secure_lookup(system, "keymanDeveloperVersion")}, dir: {os.path.basename(tmpdirname)}')
return info, system, options, keyboards, files
def infmetadata_to_json(info, system, options, keyboards, files):
jsonfiles = []

View file

@ -30,6 +30,9 @@ 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')
@ -78,6 +81,15 @@ class SentryErrorHandling:
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)
@ -114,47 +126,61 @@ class SentryErrorHandling:
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
)
hash = hashlib.md5()
hash.update(getpass.getuser().encode())
set_user({'id': hash.hexdigest()})
md5Hash = hashlib.md5()
md5Hash.update(getpass.getuser().encode())
set_user({'id': md5Hash.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')
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')

View file

@ -71,7 +71,7 @@ if __name__ == '__main__':
download_and_install_package(args.url)
else:
# Workaround for bug in webkit2gtk (#12587)
if not 'WEBKIT_DISABLE_DMABUF_RENDERER' in os.environ:
if 'WEBKIT_DISABLE_DMABUF_RENDERER' not in os.environ:
os.environ['WEBKIT_DISABLE_DMABUF_RENDERER'] = '1'
w = ViewInstalledWindow()