def test_add_breadcrumb(): # test: None does not produce error assert SentryReporter.add_breadcrumb(None, None, None) is None assert SentryReporter.add_breadcrumb('message', 'category', 'level') is None assert SentryReporter.add_breadcrumb( 'message', 'category', 'level', named_arg='some') is None
def showEvent(self, *args): super().showEvent(*args) SentryReporter.add_breadcrumb( message=f'{self.__class__.__name__}.Show', category='UI', level='info')
def test_send_defaults(): assert SentryReporter.send_event(None, None, None) is None assert SentryReporter.send_event(event={}) == { 'contexts': { 'browser': { 'name': 'Tribler', 'version': None }, 'reporter': { '_stacktrace': [], '_stacktrace_context': [], '_stacktrace_extra': [], 'comments': None, OS_ENVIRON: {}, 'sysinfo': {}, 'events': {}, }, }, 'tags': { 'machine': None, 'os': None, 'platform': None, PLATFORM_DETAILS: None, 'version': None }, }
def on_send_clicked(self, checked): self.send_report_button.setEnabled(False) self.send_report_button.setText(tr("SENDING...")) endpoint = 'http://reporter.tribler.org/report' sys_info = "" sys_info_dict = defaultdict(lambda: []) for ind in range(self.env_variables_list.topLevelItemCount()): item = self.env_variables_list.topLevelItem(ind) key = item.text(0) value = item.text(1) sys_info += f"{key}\t{value}\n" sys_info_dict[key].append(value) comments = self.comments_text_edit.toPlainText() if len(comments) == 0: comments = tr("Not provided") stack = self.error_text_edit.toPlainText() post_data = { "version": self.tribler_version, "machine": platform.machine(), "os": platform.platform(), "timestamp": int(time.time()), "sysinfo": sys_info, "comments": comments, "stack": stack, } SentryReporter.send_event(self.sentry_event, post_data, sys_info_dict, self.additional_tags) TriblerNetworkRequest(endpoint, self.on_report_sent, raw_data=tribler_urlencode(post_data), method='POST')
def test_before_send_scrubber_exists(sentry_reporter: SentryReporter): # test that in case of a set scrubber, it will be called for scrubbing an event event = {'some': 'event'} sentry_reporter.global_strategy = SentryStrategy.SEND_ALLOWED sentry_reporter.scrubber = Mock() assert sentry_reporter._before_send(event, None) sentry_reporter.scrubber.scrub_event.assert_called_with(event)
def on_tab_button_click(self, clicked_button): SentryReporter.add_breadcrumb( message=f'{clicked_button.objectName()}.Click', category='UI', level='info') self.deselect_all_buttons(except_select=clicked_button) self.clicked_tab_button.emit(clicked_button.objectName())
def tribler_started_event(self, event_dict): # if public key format will be changed, don't forget to change it # at the core side as well public_key = event_dict["public_key"] if public_key: SentryReporter.set_user(public_key.encode('utf-8')) self.tribler_started.emit(event_dict["version"])
def __init__(self, tribler_window): logger_name = self.__class__.__name__ self._logger = logging.getLogger(logger_name) SentryReporter.ignore_logger(logger_name) self.tribler_window = tribler_window self._handled_exceptions = set() self._tribler_stopped = False
def test_init(mocked_init: Mock, sentry_reporter: SentryReporter): # test that `init` method set all necessary variables and calls `sentry_sdk.init()` sentry_reporter.init(sentry_url='url', release_version='release', scrubber=SentryScrubber(), strategy=SentryStrategy.SEND_SUPPRESSED) assert sentry_reporter.scrubber assert sentry_reporter.global_strategy == SentryStrategy.SEND_SUPPRESSED mocked_init.assert_called_once()
def test_get_actual_strategy(): SentryReporter.thread_strategy.set(None) # default SentryReporter.global_strategy = SentryStrategy.SEND_ALLOWED_WITH_CONFIRMATION assert SentryReporter.get_actual_strategy( ) == SentryStrategy.SEND_ALLOWED_WITH_CONFIRMATION SentryReporter.thread_strategy.set(SentryStrategy.SEND_ALLOWED) assert SentryReporter.get_actual_strategy() == SentryStrategy.SEND_ALLOWED
def test_set_user(): assert SentryReporter.set_user(b'some_id') == { 'id': 'db69fe66ec6b6b013c2f7d271ce17cae', 'username': '******', } assert SentryReporter.set_user(b'11111100100') == { 'id': '91f900f528d5580581197c2c6a4adbbc', 'username': '******', }
def test_get_confirmation(sentry_reporter: SentryReporter): # test that `get_confirmation` calls `QApplication` and `QMessageBox` from `PyQt5.QtWidgets` mocked_QApplication = Mock() mocked_QMessageBox = MagicMock() with patch_import('PyQt5.QtWidgets', strict=True, QApplication=mocked_QApplication, QMessageBox=mocked_QMessageBox): sentry_reporter.get_confirmation(Exception('test')) mocked_QApplication.assert_called() mocked_QMessageBox.assert_called()
def unhandled_error_observer(self, loop, context): """ This method is called when an unhandled error in Tribler is observed. It broadcasts the tribler_exception event. """ try: SentryReporter.ignore_logger(self._logger.name) exception = context.get('exception') ignored_message = None try: ignored_message = IGNORED_ERRORS.get( (exception.__class__, exception.errno), IGNORED_ERRORS.get(exception.__class__)) except (ValueError, AttributeError): pass if ignored_message is not None: self._logger.error(ignored_message if ignored_message != "" else context.get('message')) return text = str(exception or context.get('message')) # We already have a check for invalid infohash when adding a torrent, but if somehow we get this # error then we simply log and ignore it. if isinstance(exception, RuntimeError) and 'invalid info-hash' in text: self._logger.error("Invalid info-hash found") return text_long = text exc = context.get('exception') if exc: with StringIO() as buffer: print_exception(type(exc), exc, exc.__traceback__, file=buffer) text_long = text_long + "\n--LONG TEXT--\n" + buffer.getvalue() text_long = text_long + "\n--CONTEXT--\n" + str(context) self._logger.error("Unhandled exception occurred! %s", text_long, exc_info=None) sentry_event = SentryReporter.event_from_exception(exception) if not self.api_manager: return events = self.api_manager.get_endpoint('events') events.on_tribler_exception(text_long, sentry_event, self.config.get_core_error_reporting_requires_user_consent()) state = self.api_manager.get_endpoint('state') state.on_tribler_exception(text_long, sentry_event) except Exception as ex: SentryReporter.capture_exception(ex) raise ex
def gui_error(self, *exc_info): if self._tribler_stopped: return info_type, info_error, tb = exc_info if info_type in self._handled_exceptions: return self._handled_exceptions.add(info_type) text = "".join(traceback.format_exception(info_type, info_error, tb)) if info_type is CoreConnectTimeoutError: text = text + self.tribler_window.core_manager.core_traceback self._stop_tribler(text) self._logger.error(text) FeedbackDialog( parent=self.tribler_window, exception_text=text, tribler_version=self.tribler_window.tribler_version, start_time=self.tribler_window.start_time, sentry_event=SentryReporter.event_from_exception(info_error), error_reporting_requires_user_consent=True, stop_application_on_close=self._tribler_stopped, additional_tags={ 'source': 'gui' }).show()
def on_error(self, error, reschedule_on_err): self._logger.info(f"Got Tribler core error: {error}") SentryReporter.ignore_logger(self._logger.name) if self.remaining_connection_attempts <= 0: raise CoreConnectTimeoutError( "Could not connect with the Tribler Core within 60 seconds") self.remaining_connection_attempts -= 1 if reschedule_on_err: # Reschedule an attempt self.connect_timer = QTimer() self.connect_timer.setSingleShot(True) connect(self.connect_timer.timeout, self.connect) self.connect_timer.start(500)
def test_send_additional_tags(): assert SentryReporter.send_event( event={}, additional_tags={'tag_key': 'tag_value'}) == { 'contexts': { 'browser': { 'name': 'Tribler', 'version': None }, 'reporter': { '_stacktrace': [], '_stacktrace_context': [], '_stacktrace_extra': [], 'comments': None, OS_ENVIRON: {}, 'sysinfo': {}, 'events': {}, }, }, 'tags': { 'machine': None, 'os': None, 'platform': None, 'platform.details': None, 'version': None, 'tag_key': 'tag_value', }, }
def run_tribler_core(api_port, api_key, state_dir, gui_test_mode=False): """ This method will start a new Tribler session. Note that there is no direct communication between the GUI process and the core: all communication is performed through the HTTP API. """ logger.info(f'Start tribler core. API port: "{api_port}". ' f'API key: "{api_key}". State dir: "{state_dir}". ' f'Core test mode: "{gui_test_mode}"') config = TriblerConfig.load( file=state_dir / CONFIG_FILE_NAME, state_dir=state_dir, reset_config_on_error=True) config.gui_test_mode = gui_test_mode if SentryReporter.is_in_test_mode(): default_core_exception_handler.sentry_reporter.global_strategy = SentryStrategy.SEND_ALLOWED config.api.http_port = int(api_port) # If the API key is set to an empty string, it will remain disabled if config.api.key not in ('', api_key): config.api.key = api_key config.write() # Immediately write the API key so other applications can use it config.api.http_enabled = True priority_order = config.resource_monitor.cpu_priority set_process_priority(pid=os.getpid(), priority_order=priority_order) # Enable tracer if --trace-debug or --trace-exceptions flag is present in sys.argv log_dir = config.general.get_path_as_absolute('log_dir', config.state_dir) trace_logger = check_and_enable_code_tracing('core', log_dir) logging.getLogger('asyncio').setLevel(logging.WARNING) if sys.platform.startswith('win'): # TODO for the moment being, we use the SelectorEventLoop on Windows, since with the ProactorEventLoop, ipv8 # peer discovery becomes unstable. Also see issue #5485. asyncio.set_event_loop(asyncio.SelectorEventLoop()) loop = asyncio.get_event_loop() exception_handler = default_core_exception_handler loop.set_exception_handler(exception_handler.unhandled_error_observer) try: loop.run_until_complete(core_session(config, components=list(components_gen(config)))) finally: if trace_logger: trace_logger.close() # Flush the logs to the file before exiting for handler in logging.getLogger().handlers: handler.flush()
def test_feedback_dialog(window): def screenshot_dialog(): screenshot(dialog, name="feedback_dialog") dialog.close() reported_error = ReportedError('type', 'text', {}) sentry_reporter = SentryReporter() dialog = FeedbackDialog(window, sentry_reporter, reported_error, "1.2.3", 23) dialog.closeEvent = lambda _: None # Otherwise, the application will stop QTimer.singleShot(1000, screenshot_dialog) dialog.exec_()
def test_event_from_exception(mocked_capture_exception: Mock, sentry_reporter: SentryReporter): # test that `event_from_exception` returns '{}' in case of an empty exception assert sentry_reporter.event_from_exception(None) == {} # test that `event_from_exception` calls `capture_exception` from `sentry_sdk` exception = Exception('test') sentry_reporter.thread_strategy = Mock() def capture_exception(_): # this behaviour normally is way more complicated, but at the end, `capture_exception` should transform # the exception to a sentry event and this event should be stored in `sentry_reporter.last_event` sentry_reporter.last_event = {'sentry': 'event'} mocked_capture_exception.side_effect = capture_exception sentry_reporter.event_from_exception(exception) mocked_capture_exception.assert_called_with(exception) sentry_reporter.thread_strategy.set.assert_any_call( SentryStrategy.SEND_SUPPRESSED) assert sentry_reporter.last_event == {'sentry': 'event'}
def init_sentry_reporter(): """ Initialise sentry reporter We use `sentry_url` as a URL for normal tribler mode and TEST_SENTRY_URL as a URL for sending sentry's reports while a Tribler client running in test mode """ test_sentry_url = os.environ.get('TEST_SENTRY_URL', None) if not test_sentry_url: SentryReporter.init( sentry_url=sentry_url, release_version=version_id, scrubber=SentryScrubber(), strategy=SentryStrategy.SEND_ALLOWED_WITH_CONFIRMATION) logger.info('Sentry has been initialised in normal mode') else: SentryReporter.init(sentry_url=test_sentry_url, release_version=version_id, scrubber=None, strategy=SentryStrategy.SEND_ALLOWED) logger.info('Sentry has been initialised in debug mode')
def test_send_sys_info(): assert SentryReporter.send_event( event={}, sys_info={ 'platform': ['darwin'], PLATFORM_DETAILS: ['details'], OS_ENVIRON: ['KEY:VALUE', 'KEY1:VALUE1'], 'event_1': [{ 'type': '' }], 'request_1': [{}], 'event_2': [], 'request_2': [], }, ) == { 'contexts': { 'browser': { 'name': 'Tribler', 'version': None }, 'reporter': { '_stacktrace': [], '_stacktrace_context': [], '_stacktrace_extra': [], 'comments': None, OS_ENVIRON: { 'KEY': 'VALUE', 'KEY1': 'VALUE1' }, 'sysinfo': { 'platform': ['darwin'], PLATFORM_DETAILS: ['details'] }, 'events': { 'event_1': [{ 'type': '' }], 'request_1': [{}], 'event_2': [], 'request_2': [] }, }, }, 'tags': { 'machine': None, 'os': None, 'platform': 'darwin', 'platform.details': 'details', 'version': None }, }
def test_add_breadcrumb(mocked_add_breadcrumb: Mock, sentry_reporter: SentryReporter): # test that `add_breadcrumb` passes all necessary arguments to `sentry_sdk` assert sentry_reporter.add_breadcrumb('message', 'category', 'level', named_arg='some') mocked_add_breadcrumb.assert_called_with( { 'message': 'message', 'category': 'category', 'level': 'level' }, named_arg='some')
def test_feedback_dialog_report_sent(window): def screenshot_dialog(): screenshot(dialog, name="feedback_dialog") dialog.close() def on_report_sent(): on_report_sent.did_send_report = True on_report_sent.did_send_report = False reported_error = ReportedError( '', 'Tribler GUI Test to test sending crash report works', {}) sentry_reporter = SentryReporter() dialog = FeedbackDialog(window, sentry_reporter, reported_error, "1.2.3", 23) dialog.closeEvent = lambda _: None # Otherwise, the application will stop dialog.on_report_sent = on_report_sent QTest.mouseClick(dialog.send_report_button, Qt.LeftButton) QTimer.singleShot(1000, screenshot_dialog) dialog.exec_() assert on_report_sent.did_send_report
def test_send_post_data(): assert SentryReporter.send_event( event={'a': 'b'}, post_data={ "version": '0.0.0', "machine": 'x86_64', "os": 'posix', "timestamp": 42, "sysinfo": '', "comments": 'comment', "stack": 'l1\nl2--LONG TEXT--l3\nl4', }, ) == { 'a': 'b', 'contexts': { 'browser': { 'name': 'Tribler', 'version': '0.0.0' }, 'reporter': { '_stacktrace': ['l1', 'l2'], '_stacktrace_context': [], '_stacktrace_extra': ['l3', 'l4'], 'comments': 'comment', 'os.environ': {}, 'sysinfo': {}, 'events': {}, }, }, 'tags': { 'machine': 'x86_64', 'os': 'posix', 'platform': None, PLATFORM_DETAILS: None, 'version': '0.0.0' }, }
""" This package contains the code for the GUI, written in pyQt. """ from tribler_common.sentry_reporter.sentry_reporter import SentryReporter gui_sentry_reporter = SentryReporter()
def __init__(self): self.logger = logging.getLogger("CoreExceptionHandler") self.report_callback: Optional[Callable[[ReportedError], None]] = None self.unreported_error: Optional[ReportedError] = None self.sentry_reporter = SentryReporter()
class CoreExceptionHandler: """ This class handles Python errors arising in the Core by catching them, adding necessary context, and sending them to the GUI through the events endpoint. It must be connected to the Asyncio loop. """ def __init__(self): self.logger = logging.getLogger("CoreExceptionHandler") self.report_callback: Optional[Callable[[ReportedError], None]] = None self.unreported_error: Optional[ReportedError] = None self.sentry_reporter = SentryReporter() @staticmethod def _get_long_text_from(exception: Exception): with StringIO() as buffer: print_exception(type(exception), exception, exception.__traceback__, file=buffer) return buffer.getvalue() @staticmethod def _is_ignored(exception: Exception): exception_class = exception.__class__ error_number = exception.errno if hasattr(exception, 'errno') else None if (exception_class, error_number) in IGNORED_ERRORS_BY_CODE: return True if exception_class not in IGNORED_ERRORS_BY_REGEX: return False pattern = IGNORED_ERRORS_BY_REGEX[exception_class] return re.search(pattern, str(exception)) is not None def _create_exception_from(self, message: str): text = f'Received error without exception: {message}' self.logger.warning(text) return Exception(text) def unhandled_error_observer(self, _, context): """ This method is called when an unhandled error in Tribler is observed. It broadcasts the tribler_exception event. """ try: self.sentry_reporter.ignore_logger(self.logger.name) context = context.copy() should_stop = context.pop('should_stop', True) message = context.pop('message', 'no message') exception = context.pop('exception', None) or self._create_exception_from(message) # Exception text = str(exception) if isinstance(exception, ComponentStartupException): should_stop = exception.component.tribler_should_stop_on_component_error exception = exception.__cause__ if self._is_ignored(exception): self.logger.warning(exception) return long_text = self._get_long_text_from(exception) self.logger.error(f"Unhandled exception occurred! {exception}\n{long_text}") reported_error = ReportedError( type=exception.__class__.__name__, text=text, long_text=long_text, context=str(context), event=self.sentry_reporter.event_from_exception(exception) or {}, should_stop=should_stop ) if self.report_callback: self.report_callback(reported_error) # pylint: disable=not-callable else: if not self.unreported_error: # We only remember the first unreported error, # as that was probably the root cause for # the crash self.unreported_error = reported_error except Exception as ex: self.sentry_reporter.capture_exception(ex) raise ex
def test_get_confirmation_no_qt(sentry_reporter: SentryReporter): assert not sentry_reporter.get_confirmation(Exception('test'))
def test_capture_exception(mocked_capture_exception: Mock, sentry_reporter: SentryReporter): # test that `capture_exception` passes an exception to `sentry_sdk` exception = Exception('test') sentry_reporter.capture_exception(exception) mocked_capture_exception.assert_called_with(exception)
def __init__( # pylint: disable=too-many-arguments, too-many-locals self, parent, sentry_reporter: SentryReporter, reported_error: ReportedError, tribler_version, start_time, stop_application_on_close=True, additional_tags=None, retrieve_error_message_from_stacktrace=False, ): QDialog.__init__(self, parent) uic.loadUi(get_ui_file_path('feedback_dialog.ui'), self) self.setWindowTitle(tr("Unexpected error")) self.selected_item_index = 0 self.tribler_version = tribler_version self.reported_error = reported_error self.scrubber = SentryScrubber() self.sentry_reporter = sentry_reporter self.stop_application_on_close = stop_application_on_close self.additional_tags = additional_tags self.retrieve_error_message_from_stacktrace = retrieve_error_message_from_stacktrace # Qt 5.2 does not have the setPlaceholderText property if hasattr(self.comments_text_edit, "setPlaceholderText"): placeholder = tr( "What were you doing before this crash happened? " "This information will help Tribler developers to figure out and fix the issue quickly." ) self.comments_text_edit.setPlaceholderText(placeholder) def add_item_to_info_widget(key, value): item = QTreeWidgetItem(self.env_variables_list) item.setText(0, key) scrubbed_value = self.scrubber.scrub_text(value) item.setText(1, scrubbed_value) text_for_viewing = '\n'.join( ( reported_error.text, LONG_TEXT_DELIMITER, reported_error.long_text, CONTEXT_DELIMITER, reported_error.context, ) ) stacktrace = self.scrubber.scrub_text(text_for_viewing.rstrip()) self.error_text_edit.setPlainText(stacktrace) connect(self.cancel_button.clicked, self.on_cancel_clicked) connect(self.send_report_button.clicked, self.on_send_clicked) # Add machine information to the tree widget add_item_to_info_widget('os.getcwd', f'{os.getcwd()}') add_item_to_info_widget('sys.executable', f'{sys.executable}') add_item_to_info_widget('os', os.name) add_item_to_info_widget('platform', sys.platform) add_item_to_info_widget('platform.details', platform.platform()) add_item_to_info_widget('platform.machine', platform.machine()) add_item_to_info_widget('python.version', sys.version) add_item_to_info_widget('indebug', str(__debug__)) add_item_to_info_widget('tribler_uptime', f"{time.time() - start_time}") for argv in sys.argv: add_item_to_info_widget('sys.argv', f'{argv}') for path in sys.path: add_item_to_info_widget('sys.path', f'{path}') for key in os.environ.keys(): add_item_to_info_widget('os.environ', f'{key}: {os.environ[key]}') # Add recent requests to feedback dialog request_ind = 1 for request, status_code in sorted(tribler_performed_requests, key=lambda rq: rq[0].time)[-30:]: add_item_to_info_widget( 'request_%d' % request_ind, '%s %s %s (time: %s, code: %s)' % (request.url, request.method, request.raw_data, request.time, status_code), ) request_ind += 1 # Add recent events to feedback dialog events_ind = 1 for event, event_time in received_events[:30][::-1]: add_item_to_info_widget('event_%d' % events_ind, f'{json.dumps(event)} (time: {event_time})') events_ind += 1 # Users can remove specific lines in the report connect(self.env_variables_list.customContextMenuRequested, self.on_right_click_item) self.send_automatically = SentryReporter.is_in_test_mode() if self.send_automatically: self.stop_application_on_close = True self.on_send_clicked(True)