Skip to content

Commit 43ae7e6

Browse files
authored
Merge pull request spesmilo#10052 from SomberNight/202507_qt_crash_felix_2
crash reporter: rm `Never`, add confirm to `Send`, and only show window once per exc group
2 parents 729003e + c85272b commit 43ae7e6

File tree

5 files changed

+72
-44
lines changed

5 files changed

+72
-44
lines changed

electrum/base_crash_reporter.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import traceback
2626
import sys
2727
import queue
28-
from typing import TYPE_CHECKING, NamedTuple, Optional
28+
from typing import TYPE_CHECKING, NamedTuple, Optional, TypedDict
29+
from types import TracebackType
2930

3031
from .version import ELECTRUM_VERSION
3132
from . import constants
3233
from .i18n import _
3334
from .util import make_aiohttp_session, error_text_str_to_safe_str
3435
from .logging import describe_os_version, Logger, get_git_version
36+
from .crypto import sha256
3537

3638
if TYPE_CHECKING:
3739
from .network import ProxySettings
@@ -68,9 +70,16 @@ class BaseCrashReporter(Logger):
6870
USER_COMMENT_PLACEHOLDER = _("Do not enter sensitive/private information here. "
6971
"The report will be visible on the public issue tracker.")
7072

71-
def __init__(self, exctype, value, tb):
73+
exc_args: tuple[type[BaseException], BaseException, TracebackType | None]
74+
75+
def __init__(
76+
self,
77+
exctype: type[BaseException],
78+
excvalue: BaseException,
79+
tb: TracebackType | None,
80+
):
7281
Logger.__init__(self)
73-
self.exc_args = (exctype, value, tb)
82+
self.exc_args = (exctype, excvalue, tb)
7483

7584
def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse:
7685
# FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API...
@@ -82,7 +91,7 @@ def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) ->
8291
] and ".electrum.org" in BaseCrashReporter.report_server):
8392
# Gah! Some kind of altcoin wants to send us crash reports.
8493
raise Exception(_("Missing report URL."))
85-
report = self.get_traceback_info()
94+
report = self.get_traceback_info(*self.exc_args)
8695
report.update(self.get_additional_info())
8796
report = json.dumps(report)
8897
coro = self.do_post(proxy, BaseCrashReporter.report_server + "/crash.json", data=report)
@@ -111,21 +120,38 @@ async def do_post(self, proxy: 'ProxySettings', url, data) -> str:
111120
async with session.post(url, data=data, raise_for_status=True) as resp:
112121
return await resp.text()
113122

114-
def get_traceback_info(self):
115-
exc_string = str(self.exc_args[1])
116-
stack = traceback.extract_tb(self.exc_args[2])
117-
readable_trace = self.__get_traceback_str_to_send()
118-
id = {
123+
@classmethod
124+
def get_traceback_info(
125+
cls,
126+
exctype: type[BaseException],
127+
excvalue: BaseException,
128+
tb: TracebackType | None,
129+
) -> TypedDict('TBInfo', {'exc_string': str, 'stack': str, 'id': dict[str, str]}):
130+
exc_string = str(excvalue)
131+
stack = traceback.extract_tb(tb)
132+
readable_trace = cls._get_traceback_str_to_send(exctype, excvalue, tb)
133+
_id = {
119134
"file": stack[-1].filename if len(stack) else '<no stack>',
120135
"name": stack[-1].name if len(stack) else '<no stack>',
121-
"type": self.exc_args[0].__name__
122-
}
136+
"type": exctype.__name__
137+
} # note: this is the "id" the crash reporter server uses to group together reports.
123138
return {
124139
"exc_string": exc_string,
125140
"stack": readable_trace,
126-
"id": id
141+
"id": _id,
127142
}
128143

144+
@classmethod
145+
def get_traceback_groupid_hash(
146+
cls,
147+
exctype: type[BaseException],
148+
excvalue: BaseException,
149+
tb: TracebackType | None,
150+
) -> bytes:
151+
tb_info = cls.get_traceback_info(exctype, excvalue, tb)
152+
_id = tb_info["id"]
153+
return sha256(str(_id))
154+
129155
def get_additional_info(self):
130156
args = {
131157
"app_version": get_git_version() or ELECTRUM_VERSION,
@@ -142,15 +168,21 @@ def get_additional_info(self):
142168
pass
143169
return args
144170

145-
def __get_traceback_str_to_send(self) -> str:
171+
@classmethod
172+
def _get_traceback_str_to_send(
173+
cls,
174+
exctype: type[BaseException],
175+
excvalue: BaseException,
176+
tb: TracebackType | None,
177+
) -> str:
146178
# make sure that traceback sent to crash reporter contains
147179
# e.__context__ and e.__cause__, i.e. if there was a chain of
148180
# exceptions, we want the full traceback for the whole chain.
149-
return "".join(traceback.format_exception(*self.exc_args))
181+
return "".join(traceback.format_exception(exctype, excvalue, tb))
150182

151183
def _get_traceback_str_to_display(self) -> str:
152184
# overridden in Qt subclass
153-
return self.__get_traceback_str_to_send()
185+
return self._get_traceback_str_to_send(*self.exc_args)
154186

155187
def get_report_string(self):
156188
info = self.get_additional_info()

electrum/gui/qml/components/ExceptionDialog.qml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,16 @@ ElDialog
7777
Layout.fillWidth: true
7878
Layout.preferredWidth: 3
7979
text: qsTr('Send Bug Report')
80-
onClicked: AppController.sendReport(user_text.text)
81-
}
82-
Button {
83-
Layout.fillWidth: true
84-
Layout.preferredWidth: 2
85-
text: qsTr('Never')
8680
onClicked: {
87-
AppController.showNever()
88-
close()
81+
var dialog = app.messageDialog.createObject(app, {
82+
text: qsTr('Confirm to send bugreport?'),
83+
yesno: true,
84+
z: 1001 // assure topmost of all other dialogs
85+
})
86+
dialog.accepted.connect(function() {
87+
AppController.sendReport(user_text.text)
88+
})
89+
dialog.open()
8990
}
9091
}
9192
Button {

electrum/gui/qml/qeapp.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def isAndroid(self):
344344
@pyqtSlot(result='QVariantMap')
345345
def crashData(self):
346346
return {
347-
'traceback': self.get_traceback_info(),
347+
'traceback': self.get_traceback_info(*self.exc_args),
348348
'extra': self.get_additional_info(),
349349
'reportstring': self.get_report_string()
350350
}
@@ -379,10 +379,6 @@ def report_task():
379379
self.sendingBugreport.emit()
380380
threading.Thread(target=report_task, daemon=True).start()
381381

382-
@pyqtSlot()
383-
def showNever(self):
384-
self.config.SHOW_CRASH_REPORTER = False
385-
386382
def _get_traceback_str_to_display(self) -> str:
387383
# The msg_box that shows the report uses rich_text=True, so
388384
# if traceback contains special HTML characters, e.g. '<',
@@ -541,6 +537,7 @@ def __init__(self, *, slot):
541537
Logger.__init__(self)
542538
assert self._INSTANCE is None, "Exception_Hook is supposed to be a singleton"
543539
self.wallet_types_seen = set() # type: Set[str]
540+
self.exception_ids_seen = set() # type: Set[bytes]
544541

545542
sys.excepthook = self.handler
546543
threading.excepthook = self.handler
@@ -551,14 +548,15 @@ def __init__(self, *, slot):
551548

552549
@classmethod
553550
def maybe_setup(cls, *, wallet: 'Abstract_Wallet' = None, slot=None) -> None:
554-
if not QEConfig.instance.config.SHOW_CRASH_REPORTER:
555-
EarlyExceptionsQueue.set_hook_as_ready() # flush already queued exceptions
556-
return
557551
if not cls._INSTANCE:
558552
cls._INSTANCE = Exception_Hook(slot=slot)
559553
if wallet:
560554
cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type)
561555

562556
def handler(self, *exc_info):
563557
self.logger.error('exception caught by crash reporter', exc_info=exc_info)
558+
groupid_hash = BaseCrashReporter.get_traceback_groupid_hash(*exc_info)
559+
if groupid_hash in self.exception_ids_seen:
560+
return # to avoid annoying the user, only show crash reporter once per exception groupid
561+
self.exception_ids_seen.add(groupid_hash)
564562
self._report_exception.emit(*exc_info)

electrum/gui/qt/exception_window.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,10 @@ def __init__(self, config: 'SimpleConfig', exctype, value, tb):
8383
buttons = QHBoxLayout()
8484

8585
report_button = QPushButton(_('Send Bug Report'))
86-
report_button.clicked.connect(lambda _checked: self.send_report())
86+
report_button.clicked.connect(lambda _checked: self._ask_for_confirm_to_send_report())
8787
report_button.setIcon(read_QIcon("tab_send.png"))
8888
buttons.addWidget(report_button)
8989

90-
never_button = QPushButton(_('Never'))
91-
never_button.clicked.connect(lambda _checked: self.show_never())
92-
buttons.addWidget(never_button)
93-
9490
close_button = QPushButton(_('Not Now'))
9591
close_button.clicked.connect(lambda _checked: self.close())
9692
buttons.addWidget(close_button)
@@ -103,6 +99,10 @@ def __init__(self, config: 'SimpleConfig', exctype, value, tb):
10399
self.setLayout(main_box)
104100
self.show()
105101

102+
def _ask_for_confirm_to_send_report(self):
103+
if self.question("Confirm to send bugreport?"):
104+
self.send_report()
105+
106106
def send_report(self):
107107
def on_success(response: CrashReportResponse):
108108
text = response.text
@@ -133,10 +133,6 @@ def on_close(self):
133133
Exception_Window._active_window = None
134134
self.close()
135135

136-
def show_never(self):
137-
self.config.SHOW_CRASH_REPORTER = False
138-
self.close()
139-
140136
def closeEvent(self, event):
141137
self.on_close()
142138
event.accept()
@@ -181,23 +177,25 @@ def __init__(self, *, config: 'SimpleConfig'):
181177
assert self._INSTANCE is None, "Exception_Hook is supposed to be a singleton"
182178
self.config = config
183179
self.wallet_types_seen = set() # type: Set[str]
180+
self.exception_ids_seen = set() # type: Set[bytes]
184181

185182
sys.excepthook = self.handler
186183
self._report_exception.connect(_show_window)
187184
EarlyExceptionsQueue.set_hook_as_ready()
188185

189186
@classmethod
190187
def maybe_setup(cls, *, config: 'SimpleConfig', wallet: 'Abstract_Wallet' = None) -> None:
191-
if not config.SHOW_CRASH_REPORTER:
192-
EarlyExceptionsQueue.set_hook_as_ready() # flush already queued exceptions
193-
return
194188
if not cls._INSTANCE:
195189
cls._INSTANCE = Exception_Hook(config=config)
196190
if wallet:
197191
cls._INSTANCE.wallet_types_seen.add(wallet.wallet_type)
198192

199193
def handler(self, *exc_info):
200194
self.logger.error('exception caught by crash reporter', exc_info=exc_info)
195+
groupid_hash = BaseCrashReporter.get_traceback_groupid_hash(*exc_info)
196+
if groupid_hash in self.exception_ids_seen:
197+
return # to avoid annoying the user, only show crash reporter once per exception groupid
198+
self.exception_ids_seen.add(groupid_hash)
201199
self._report_exception.emit(self.config, *exc_info)
202200

203201

electrum/simple_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,6 @@ def __setattr__(self, name, value):
892892
long_desc=lambda: _("Select which language is used in the GUI (after restart)."),
893893
)
894894
BLOCKCHAIN_PREFERRED_BLOCK = ConfigVar('blockchain_preferred_block', default=None)
895-
SHOW_CRASH_REPORTER = ConfigVar('show_crash_reporter', default=True, type_=bool)
896895
DONT_SHOW_TESTNET_WARNING = ConfigVar('dont_show_testnet_warning', default=False, type_=bool)
897896
RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
898897
IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)

0 commit comments

Comments
 (0)