mirror of
https://github.com/Pythagora-io/gpt-pilot.git
synced 2026-01-09 21:27:53 -05:00
show nicely formatted stack trace if there's an unhandled exception
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user