Philipp Hörist pushed to branch master at gajim / gajim
Commits: 38a3b673 by Daniel Brötzmann at 2022-03-23T20:21:54+00:00 Exceptions: Rework and basic Sentry integration
- - - - -
8 changed files:
- README.md - gajim/common/app.py - gajim/common/helpers.py - gajim/data/gui/exception_dialog.ui - gajim/gtk/builder.pyi - gajim/gtk/exception.py - gajim/gtk/util.py - win/_base.sh
Changes:
===================================== README.md ===================================== @@ -21,6 +21,7 @@ ### Runtime Requirements ### Optional Runtime Requirements
- python3-pil (pillow) for support of webp avatars +- python3-sentry-sdk for Sentry error reporting to dev.gajim.org (users decide whether to send reports or not) - gir1.2-gspell-1 and hunspell-LANG where lang is your locale eg. en, fr etc - gir1.2-secret-1 for GNOME Keyring or KDE support as password storage - D-Bus running to have gajim-remote working
===================================== gajim/common/app.py ===================================== @@ -126,6 +126,7 @@ def __init__(self): 'IDLE': False, 'APPINDICATOR': False, 'AYATANA_APPINDICATOR': False, + 'SENTRY_SDK': False, }
_tasks: dict[int, list[Any]] = defaultdict(list) @@ -277,6 +278,13 @@ def detect_dependencies() -> None: except (ImportError, ValueError): pass
+ # SENTRY SDK + try: + import sentry_sdk # pylint: disable=unused-import + _dependencies['SENTRY_SDK'] = True + except ImportError: + pass + # Print results for dep, val in _dependencies.items(): log('gajim').info('%-13s %s', dep, val)
===================================== gajim/common/helpers.py ===================================== @@ -78,6 +78,8 @@
from gi.repository import Gio from gi.repository import GLib +from gi.repository import GObject +from gi.repository import Soup
import precis_i18n.codec # pylint: disable=unused-import
@@ -757,6 +759,18 @@ def get_proxy(proxy_name: str) -> Optional[ProxyData]: password=password)
+def is_proxy_in_use() -> bool: + return get_global_proxy() is not None or is_account_proxy_in_use() + + +def is_account_proxy_in_use() -> bool: + for client in app.get_clients(): + account_proxy = get_account_proxy(client.account, fallback=False) + if account_proxy is not None: + return True + return False + + def version_condition(current_version: str, required_version: str) -> bool: if V(current_version) < V(required_version): return False @@ -1444,6 +1458,42 @@ def get_start_of_day(date_time: datetime) -> datetime: microsecond=0)
+def get_os_name() -> str: + if sys.platform in ('win32', 'darwin'): + return platform.system() + if os.name == 'posix': + try: + import distro + return distro.name(pretty=True) + except ImportError: + return platform.system() + return '' + + +def get_os_version() -> str: + if sys.platform in ('win32', 'darwin'): + return platform.release() + if os.name == 'posix': + try: + import distro + return distro.version(pretty=True) + except ImportError: + return platform.release() + return '' + + +def get_gobject_version() -> str: + gobject_ver = '.'.join(map(str, GObject.pygobject_version)) + return gobject_ver + + +def get_glib_version() -> str: + glib_ver = '.'.join(map(str, [GLib.MAJOR_VERSION, + GLib.MINOR_VERSION, + GLib.MICRO_VERSION])) + return glib_ver + + def make_path_from_jid(base_path: Path, jid: JID) -> Path: assert jid.domain is not None domain = jid.domain[:50] @@ -1455,3 +1505,21 @@ def make_path_from_jid(base_path: Path, jid: JID) -> Path: if jid.resource is not None: return path / jid.resource[:30] return path + + +def make_http_request(uri: str, callback: Any) -> None: + proxy = get_global_proxy() + if proxy is None: + if is_account_proxy_in_use(): + raise ValueError('No global proxy found, ' + 'but account proxies are in use') + resolver = None + + else: + resolver = proxy.get_resolver() + + session = Soup.Session() + session.props.proxy_resolver = resolver + session.props.user_agent = f'Gajim {app.version}' + message = Soup.Message.new('GET', uri) + session.queue_message(message, callback)
===================================== gajim/data/gui/exception_dialog.ui ===================================== @@ -1,29 +1,91 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.36.0 --> +<!-- Generated with glade 3.38.2 --> <interface> - <requires lib="gtk+" version="3.22"/> - <object class="GtkApplicationWindow" id="exception_dialog"> - <property name="can_focus">False</property> - <property name="window_position">center</property> - <property name="type_hint">dialog</property> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="exception_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkInfoBar" id="infobar"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child internal-child="action_area"> + <object class="GtkButtonBox"> + <property name="can-focus">False</property> + <property name="layout-style">end</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child internal-child="content_area"> + <object class="GtkBox"> + <property name="can-focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">dialog-information-symbolic</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Install <tt>sentry-sdk</tt> to make reporting issues easier.</property> + <property name="use-markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> <child> <object class="GtkBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="border_width">18</property> + <property name="can-focus">False</property> + <property name="border-width">18</property> <property name="orientation">vertical</property> - <property name="spacing">12</property> + <property name="spacing">24</property> <child> <object class="GtkBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">12</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> <child> <object class="GtkImage"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="valign">center</property> - <property name="icon_name">dialog-error</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="icon-name">face-embarrassed</property> <property name="icon_size">6</property> </object> <packing> @@ -33,22 +95,87 @@ </packing> </child> <child> - <object class="GtkBox"> + <object class="GtkLabel"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">6</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Sorry, that should not have happened</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max-width-chars">50</property> + <style> + <class name="large-header"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Gajim encountered an error. +Please help us fixing this issue by sending this report.</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <property name="lines">3</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="height-request">200</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="margin-top">6</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkTextView" id="exception_view"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="editable">False</property> + <property name="left-margin">4</property> + <property name="right-margin">4</property> + <property name="top-margin">4</property> + <property name="bottom-margin">4</property> + <property name="accepts-tab">False</property> + <property name="monospace">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="user_feedback_box"> + <property name="can-focus">False</property> + <property name="halign">center</property> + <property name="margin-top">6</property> + <property name="spacing">12</property> <child> <object class="GtkLabel"> <property name="visible">True</property> - <property name="can_focus">False</property> + <property name="can-focus">False</property> <property name="halign">start</property> - <property name="label" translatable="yes">Sorry, that should not have happened</property> + <property name="label" translatable="yes">What happened</property> <property name="wrap">True</property> - <property name="max_width_chars">30</property> - <property name="xalign">0</property> <style> - <class name="large-header"/> + <class name="dim-label"/> </style> </object> <packing> @@ -58,16 +185,13 @@ </packing> </child> <child> - <object class="GtkLabel"> + <object class="GtkEntry" id="user_feedback_entry"> + <property name="width-request">400</property> <property name="visible">True</property> - <property name="can_focus">False</property> + <property name="can-focus">True</property> <property name="halign">start</property> - <property name="label" translatable="yes">Gajim encountered an error. A report is shown below. -By reporting this bug you might help people to fix this.</property> - <property name="wrap">True</property> - <property name="max_width_chars">60</property> - <property name="lines">3</property> - <property name="xalign">0</property> + <property name="activates-default">True</property> + <property name="placeholder-text" translatable="yes">Add some details…</property> </object> <packing> <property name="expand">False</property> @@ -79,7 +203,7 @@ By reporting this bug you might help people to fix this.</property> <packing> <property name="expand">False</property> <property name="fill">True</property> - <property name="position">1</property> + <property name="position">4</property> </packing> </child> </object> @@ -90,79 +214,72 @@ By reporting this bug you might help people to fix this.</property> </packing> </child> <child> - <object class="GtkButtonBox"> + <object class="GtkBox"> <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">6</property> - <property name="layout_style">end</property> + <property name="can-focus">False</property> + <property name="spacing">12</property> <child> - <object class="GtkButton" id="close_btn"> - <property name="label">_Close</property> + <object class="GtkButton" id="close_button"> + <property name="label" translatable="yes">_Close</property> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_close_clicked" swapped="no"/> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="use-underline">True</property> + <signal name="clicked" handler="_on_close_clicked" swapped="no"/> </object> <packing> - <property name="expand">True</property> + <property name="expand">False</property> <property name="fill">True</property> + <property name="pack-type">end</property> <property name="position">0</property> </packing> </child> <child> - <object class="GtkButton" id="report_btn"> - <property name="label" translatable="yes">_Report Bug</property> + <object class="GtkButton" id="report_button"> + <property name="label" translatable="yes">Send _Report</property> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_report_clicked" swapped="no"/> + <property name="can-focus">True</property> + <property name="can-default">True</property> + <property name="receives-default">True</property> + <property name="use-underline">True</property> + <signal name="clicked" handler="_on_report_clicked" swapped="no"/> <style> <class name="suggested-action"/> </style> </object> <packing> - <property name="expand">True</property> + <property name="expand">False</property> <property name="fill">True</property> - <property name="position">1</property> + <property name="pack-type">end</property> + <property name="position">0</property> </packing> </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="pack_type">end</property> - <property name="position">1</property> - </packing> - </child> - <child> - <object class="GtkScrolledWindow"> - <property name="height_request">200</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="vexpand">True</property> - <property name="shadow_type">in</property> <child> - <object class="GtkTextView" id="exception_view"> + <object class="GtkSpinner" id="report_spinner"> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="vexpand">True</property> - <property name="editable">False</property> - <property name="left_margin">4</property> - <property name="right_margin">4</property> - <property name="accepts_tab">False</property> + <property name="can-focus">False</property> </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack-type">end</property> + <property name="position">2</property> + </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> - <property name="position">2</property> + <property name="pack-type">end</property> + <property name="position">4</property> </packing> </child> </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> </child> </object> </interface>
===================================== gajim/gtk/builder.pyi ===================================== @@ -313,10 +313,14 @@ class EmojiChooserBuilder(Builder):
class ExceptionDialogBuilder(Builder): - exception_dialog: Gtk.ApplicationWindow - close_btn: Gtk.Button - report_btn: Gtk.Button + exception_box: Gtk.Box + infobar: Gtk.InfoBar exception_view: Gtk.TextView + user_feedback_box: Gtk.Box + user_feedback_entry: Gtk.Entry + close_button: Gtk.Button + report_button: Gtk.Button + report_spinner: Gtk.Spinner
class FileTransferBuilder(Builder):
===================================== gajim/gtk/exception.py ===================================== @@ -17,29 +17,46 @@ # You should have received a copy of the GNU General Public License # along with Gajim. If not, see http://www.gnu.org/licenses/.
+from typing import Type +from types import TracebackType + import sys -import os +import json import traceback import threading import webbrowser -import platform from io import StringIO from urllib.parse import urlencode
+from gi.repository import Gdk from gi.repository import Gtk -from gi.repository import GObject -from gi.repository import GLib +from gi.repository import Soup
import nbxmpp
import gajim -from gajim.common import configpaths +from gajim.common import app +from gajim.common.helpers import get_glib_version +from gajim.common.helpers import get_gobject_version +from gajim.common.helpers import get_os_name +from gajim.common.helpers import get_os_version +from gajim.common.helpers import is_proxy_in_use +from gajim.common.helpers import make_http_request +from gajim.common.i18n import _ + from .builder import get_builder +from .util import get_gtk_version
+try: + import sentry_sdk +except ImportError: + pass
_exception_in_progress = threading.Lock()
-ISSUE_TEXT = '''## Versions +ISSUE_URL = 'https://dev.gajim.org/gajim/gajim/issues/new' + +ISSUE_TEXT = '''## Versions: - OS: {} - GTK Version: {} - PyGObject Version: {} @@ -55,7 +72,10 @@ ...'''
-def _hook(type_, value, tb): +def _hook(type_: Type[BaseException], + value: BaseException, + tb: TracebackType + ) -> None: if not _exception_in_progress.acquire(False): # Exceptions have piled up, so we use the default exception # handler for such exceptions @@ -66,65 +86,159 @@ def _hook(type_, value, tb): _exception_in_progress.release()
-class ExceptionDialog(): - def __init__(self, type_, value, tb): - path = configpaths.get('GUI') / 'exception_dialog.ui' - self._ui = get_builder(path.resolve()) - self._ui.connect_signals(self) +class ExceptionDialog(Gtk.ApplicationWindow): + def __init__(self, + type_: Type[BaseException], + value: BaseException, + tb: TracebackType + ) -> None: + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_show_menubar(False) + self.set_resizable(True) + self.set_default_size(700, -1) + self.set_title(_('Gajim - Error'))
- self._ui.report_btn.grab_focus() + self._traceback_data = (type_, value, tb) + self._sentry_available = app.is_installed('SENTRY_SDK') + + self._ui = get_builder('exception_dialog.ui') + self.add(self._ui.exception_box) + + if not self._sentry_available: + self._ui.user_feedback_box.set_no_show_all(True) + self._ui.infobar.set_revealed(True) + + self._ui.report_button.grab_focus() + self._ui.report_button.grab_default()
- buffer_ = self._ui.exception_view.get_buffer() trace = StringIO() traceback.print_exception(type_, value, tb, None, trace) - self.text = self.get_issue_text(trace.getvalue()) - buffer_.set_text(self.text) - print(self.text, file=sys.stderr)
- self._ui.exception_view.set_editable(False) - self._ui.exception_dialog.show() + self._issue_text = self._get_issue_text(trace.getvalue()) + buffer_ = self._ui.exception_view.get_buffer() + buffer_.set_text(self._issue_text)
- def on_report_clicked(self, *args): - issue_url = 'https://dev.gajim.org/gajim/gajim/issues/new' - params = {'issue[description]': self.text} - url = '{}?{}'.format(issue_url, urlencode(params)) + self._ui.connect_signals(self) + self.show_all() + + if self._sentry_available: + self._ui.user_feedback_entry.grab_focus() + + def _on_report_clicked(self, _button: Gtk.Button) -> None: + if self._sentry_available and not is_proxy_in_use(): + # sentry-sdk supports a http-proxy arg but for now only use + # sentry when no proxy is set, because we never tested if this + # works. It's not worth it to potentially leak users identity just + # because of error reporting. + self._report_with_sentry() + else: + self._report_with_browser() + + def _report_with_browser(self): + params = {'issue[description]': self._issue_text} + url = f'{ISSUE_URL}?{urlencode(params)}' webbrowser.open(url, new=2) + self.destroy()
- def on_close_clicked(self, *args): - self._ui.exception_dialog.destroy() + def _on_close_clicked(self, _button: Gtk.Button) -> None: + self.destroy()
@staticmethod - def get_issue_text(traceback_text): - gtk_ver = '%i.%i.%i' % ( - Gtk.get_major_version(), - Gtk.get_minor_version(), - Gtk.get_micro_version()) - gobject_ver = '.'.join(map(str, GObject.pygobject_version)) - glib_ver = '.'.join(map(str, [GLib.MAJOR_VERSION, - GLib.MINOR_VERSION, - GLib.MICRO_VERSION])) - - return ISSUE_TEXT.format(get_os_info(), - gtk_ver, - gobject_ver, - glib_ver, - nbxmpp.__version__, - gajim.__version__, - traceback_text) - - -def init(): - if os.name == 'nt' or not sys.stderr.isatty(): - sys.excepthook = _hook + def _get_issue_text(traceback_text: str) -> str: + return ISSUE_TEXT.format( + f'{get_os_name()} {get_os_version()}', + get_gtk_version(), + get_gobject_version(), + get_glib_version(), + nbxmpp.__version__, + gajim.__version__, + traceback_text) + + def _report_with_sentry(self) -> None: + if sentry_sdk.last_event_id() is None: + # Sentry has not been initialized yet: + # update sentry endpoint, init sentry, then capture exception + self._request_sentry_endpoint() + return + + self._capture_exception() + self.destroy() + + def _request_sentry_endpoint(self) -> None: + self._ui.report_button.set_sensitive(False) + self._ui.close_button.set_sensitive(False) + self._ui.report_spinner.show() + self._ui.report_spinner.start() + + make_http_request('https://gajim.org/updates.json', + self._on_endpoint_received) + + def _parse_endpoint(self, message: Soup.Message) -> str: + status = message.props.status_code + response_body = message.props.response_body + + if status != Soup.Status.OK: + error = message.props.reason_phrase + raise ValueError('Failed to retrieve sentry endpoint: %s %s' % ( + status, error)) + + if response_body is None or not response_body.data: + raise ValueError('Failed to retrieve sentry endpoint, ' + 'no response body')
+ try: + data = json.loads(response_body.data) + except Exception as error: + raise ValueError('Json parsing error: %s' % error)
-def get_os_info(): - if os.name == 'nt' or sys.platform == 'darwin': - return platform.system() + " " + platform.release() - if os.name == 'posix': + endpoint = data.get('sentry_endpoint') + if endpoint is None: + raise ValueError('Sentry endpoint missing in response') + + return endpoint + + def _on_endpoint_received(self, + _session: Soup.Session, + message: Soup.Message + ) -> None: try: - import distro - return distro.name(pretty=True) - except ImportError: - return platform.system() - return '' + endpoint = self._parse_endpoint(message) + except ValueError as error: + print(error) + self._report_with_browser() + + else: + self._init_sentry(endpoint) + self._capture_exception() + self.destroy() + + def _init_sentry(self, endpoint: str) -> None: + # pylint: disable=abstract-class-instantiated + sentry_sdk.init( + dsn=endpoint, + traces_sample_rate=0.0, + max_breadcrumbs=0, + release=gajim.__version__) + sentry_sdk.set_context('os', { + 'name': get_os_name(), + 'version': get_os_version()}) + sentry_sdk.set_context('app', { + 'app_version': gajim.__version__}) + sentry_sdk.set_context('software', { + 'python-nbxmpp': nbxmpp.__version__, + 'GTK': get_gtk_version(), + 'GObject': get_gobject_version(), + 'GLib': get_glib_version()}) + + def _capture_exception(self) -> None: + sentry_sdk.set_context('user feedback', { + 'Feedback': self._ui.user_feedback_entry.get_text()}) + sentry_sdk.capture_exception(self._traceback_data) + + +def init() -> None: + if sys.platform == 'win32' or not sys.stderr.isatty(): + sys.excepthook = _hook
===================================== gajim/gtk/util.py ===================================== @@ -779,6 +779,14 @@ def open_window(name: str, **kwargs: Any) -> Any: return window
+def get_gtk_version() -> str: + gtk_ver = '%i.%i.%i' % ( + Gtk.get_major_version(), + Gtk.get_minor_version(), + Gtk.get_micro_version()) + return gtk_ver + + class EventHelper(CommonEventHelper): def __init__(self): CommonEventHelper.__init__(self)
===================================== win/_base.sh ===================================== @@ -112,6 +112,7 @@ python-gnupg python-axolotl qrcode css_parser +sentry-sdk "
build_pip install $(echo "$PIP_REQUIREMENTS" | tr ["\n"] [" "])
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/commit/38a3b673eb1baa08ea98b148bcd0222b7...