diff --git a/core/cli/main.py b/core/cli/main.py index f6718a75..2c55ae5a 100644 --- a/core/cli/main.py +++ b/core/cli/main.py @@ -10,7 +10,7 @@ from core.llm.base import APIError from core.log import get_logger from core.state.state_manager import StateManager from core.telemetry import telemetry -from core.ui.base import UIBase +from core.ui.base import UIBase, pythagora_source log = get_logger(__name__) @@ -35,28 +35,24 @@ async def run_project(sm: StateManager, ui: UIBase) -> bool: success = False try: success = await orca.run() + telemetry.set("end_result", "success:exit" if success else "failure:api-error") except KeyboardInterrupt: log.info("Interrupted by user") telemetry.set("end_result", "interrupt") await sm.rollback() except APIError as err: log.warning(f"LLM API error occurred: {err.message}") - await ui.send_message(f"LLM API error occurred: {err.message}") - await ui.send_message("Stopping Pythagora due to previous error.") + await ui.send_message( + f"Stopping Pythagora due to an error while calling the LLM API: {err.message}", + source=pythagora_source, + ) telemetry.set("end_result", "failure:api-error") await sm.rollback() except Exception as err: - telemetry.record_crash(err) - await sm.rollback() log.error(f"Uncaught exception: {err}", exc_info=True) - await ui.send_message(f"Unrecoverable error occurred: {err}") - - if success: - telemetry.set("end_result", "success:exit") - else: - # We assume unsuccessful exit (but not an exception) is a result - # of an API error that the user didn't retry. - telemetry.set("end_result", "failure:api-error") + stack_trace = telemetry.record_crash(err) + await sm.rollback() + await ui.send_message(f"Stopping Pythagora due to error:\n\n{stack_trace}", source=pythagora_source) await telemetry.send() return success @@ -70,7 +66,7 @@ async def start_new_project(sm: StateManager, ui: UIBase) -> bool: :param ui: User interface. :return: True if the project was created successfully, False otherwise. """ - user_input = await ui.ask_question("What is the name of the project", allow_empty=False) + user_input = await ui.ask_question("What is the name of the project", allow_empty=False, source=pythagora_source) if user_input.cancelled: return False diff --git a/core/telemetry/__init__.py b/core/telemetry/__init__.py index 186a016a..8165fe28 100644 --- a/core/telemetry/__init__.py +++ b/core/telemetry/__init__.py @@ -204,45 +204,62 @@ class Telemetry: self, exception: Exception, end_result: str = "failure", - ): + ) -> str: """ Record crash diagnostics. - :param error: exception that caused the crash + The formatted stack trace only contains frames from the `core` package + of gpt-pilot. + + :param exception: exception that caused the crash + :param end_result: end result of the application (default: "failure") + :return: formatted stack trace of the exception Records the following crash diagnostics data: - * full stack trace + * formatted stack trace * exception (class name and message) - * file:line for the last (innermost) 3 frames of the stack trace + * file:line for the last (innermost) 3 frames of the stack trace, only counting + the frames from the `core` package. """ self.set("end_result", end_result) root_dir = Path(__file__).parent.parent.parent - stack_trace = traceback.format_exc() exception_class_name = exception.__class__.__name__ exception_message = str(exception) - frames = [] - # Let's not crash if there's something funny in frame or path handling - try: - tb = exception.__traceback__ - while tb is not None: - frame = tb.tb_frame - file_path = Path(frame.f_code.co_filename).absolute().relative_to(root_dir).as_posix() - frame_info = {"file": file_path, "line": tb.tb_lineno} - if not file_path.startswith("pilot-env"): - frames.append(frame_info) - tb = tb.tb_next - except: # noqa - pass + frames = [] + info = [] + + for frame in traceback.extract_tb(exception.__traceback__): + try: + file_path = Path(frame.filename).absolute().relative_to(root_dir).as_posix() + except ValueError: + # outside of root_dir + continue + + if not file_path.startswith("core/"): + continue + + frames.append( + { + "file": file_path, + "line": frame.lineno, + "name": frame.name, + "code": frame.line, + } + ) + info.append(f"File `{file_path}`, line {frame.lineno}, in {frame.name}\n {frame.line}") frames.reverse() + stack_trace = "\n".join(info) + f"\n{exception.__class__.__name__}: {str(exception)}" + self.data["crash_diagnostics"] = { "stack_trace": stack_trace, "exception_class": exception_class_name, "exception_message": exception_message, "frames": frames[: self.MAX_CRASH_FRAMES], } + return stack_trace def record_llm_request( self, diff --git a/core/ui/base.py b/core/ui/base.py index 5fb11ffc..bd5a8c1b 100644 --- a/core/ui/base.py +++ b/core/ui/base.py @@ -234,4 +234,6 @@ class UIBase: raise NotImplementedError() -__all__ = ["UISource", "AgentSource", "UserInput", "UIBase"] +pythagora_source = UISource("Pythagora", "pythagora") + +__all__ = ["UISource", "AgentSource", "UserInput", "UIBase", "pythagora_source"] diff --git a/tests/telemetry/test_telemetry.py b/tests/telemetry/test_telemetry.py index 6b74b75a..c2c9d947 100644 --- a/tests/telemetry/test_telemetry.py +++ b/tests/telemetry/test_telemetry.py @@ -4,6 +4,7 @@ import httpx import pytest import pytest_asyncio +from core.config import loader from core.telemetry import Telemetry @@ -209,30 +210,16 @@ def test_record_crash(mock_settings): telemetry = Telemetry() try: - raise ValueError("test error") + loader.load("/tmp/this/file/does/not/exist") except Exception as err: telemetry.record_crash(err) assert telemetry.data["end_result"] == "failure" diag = telemetry.data["crash_diagnostics"] - assert diag["exception_class"] == "ValueError" - assert diag["exception_message"] == "test error" - assert diag["frames"][0]["file"] == "tests/telemetry/test_telemetry.py" - assert "ValueError: test error" in diag["stack_trace"] - - -@patch("core.telemetry.settings") -def test_record_crash_crashes(mock_settings): - mock_settings.telemetry = MagicMock(id="test-id", endpoint="test-endpoint", enabled=True) - - telemetry = Telemetry() - telemetry.record_crash(None) - - assert telemetry.data["end_result"] == "failure" - diag = telemetry.data["crash_diagnostics"] - assert diag["exception_class"] == "NoneType" - assert diag["exception_message"] == "None" - assert diag["frames"] == [] + assert diag["exception_class"] == "FileNotFoundError" + assert "/tmp/this/file/does/not/exist" in diag["exception_message"] + assert diag["frames"][0]["file"] == "core/config/__init__.py" + assert "FileNotFoundError" in diag["stack_trace"] @patch("core.telemetry.settings")