2525import traceback
2626import sys
2727import queue
28- from typing import TYPE_CHECKING , NamedTuple , Optional
28+ from typing import TYPE_CHECKING , NamedTuple , Optional , TypedDict
29+ from types import TracebackType
2930
3031from .version import ELECTRUM_VERSION
3132from . import constants
3233from .i18n import _
3334from .util import make_aiohttp_session , error_text_str_to_safe_str
3435from .logging import describe_os_version , Logger , get_git_version
36+ from .crypto import sha256
3537
3638if 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 ()
0 commit comments