show nicely formatted stack trace if there's an unhandled exception

This commit is contained in:
Senko Rasic
2024-05-23 23:26:06 +02:00
parent 1e5740506b
commit ae86198294
4 changed files with 54 additions and 52 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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"]

View File

@@ -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")