From ec72448b8b570d05f44724e00535a5359930b8a2 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Mon, 20 May 2024 16:55:41 -0700 Subject: [PATCH] Catch more errors in frontend/backend (#3346) --- reflex/app.py | 7 +++-- reflex/app_module_for_backend.py | 3 +++ reflex/state.py | 4 +-- reflex/utils/prerequisites.py | 7 +++-- reflex/utils/telemetry.py | 46 ++++++++++++++++++++++++-------- tests/test_telemetry.py | 4 +-- 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 4a7c60e2e..72a09462a 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1068,13 +1068,12 @@ async def process( client_ip: The client_ip. Raises: - ReflexError: If a reflex specific error occurs during processing the event. + Exception: If a reflex specific error occurs during processing the event. Yields: The state updates after processing the event. """ from reflex.utils import telemetry - from reflex.utils.exceptions import ReflexError try: # Add request data to the state. @@ -1118,8 +1117,8 @@ async def process( # Yield the update. yield update - except ReflexError as ex: - telemetry.send("error", context="backend", detail=str(ex)) + except Exception as ex: + telemetry.send_error(ex, context="backend") raise diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index 8d97725f9..17cd1973c 100644 --- a/reflex/app_module_for_backend.py +++ b/reflex/app_module_for_backend.py @@ -4,12 +4,14 @@ Only the app attribute is explicitly exposed. from concurrent.futures import ThreadPoolExecutor from reflex import constants +from reflex.utils import telemetry from reflex.utils.exec import is_prod_mode from reflex.utils.prerequisites import get_app if "app" != constants.CompileVars.APP: raise AssertionError("unexpected variable name for 'app'") +telemetry.send("compile") app_module = get_app(reload=False) app = getattr(app_module, constants.CompileVars.APP) # For py3.8 and py3.9 compatibility when redis is used, we MUST add any decorator pages @@ -29,5 +31,6 @@ del app_module del compile_future del get_app del is_prod_mode +del telemetry del constants del ThreadPoolExecutor diff --git a/reflex/state.py b/reflex/state.py index 86a222b66..ba87029c8 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1476,7 +1476,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): StateUpdate object """ from reflex.utils import telemetry - from reflex.utils.exceptions import ReflexError # Get the function to process the event. fn = functools.partial(handler.fn, state) @@ -1516,8 +1515,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except Exception as ex: error = traceback.format_exc() print(error) - if isinstance(ex, ReflexError): - telemetry.send("error", context="backend", detail=str(ex)) + telemetry.send_error(ex, context="backend") yield state._as_state_update( handler, window_alert("An error occurred. See logs for details."), diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 1852f0434..f77e1d63c 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -233,9 +233,8 @@ def get_app(reload: bool = False) -> ModuleType: Raises: RuntimeError: If the app name is not set in the config. - exceptions.ReflexError: Reflex specific errors. """ - from reflex.utils import exceptions, telemetry + from reflex.utils import telemetry try: os.environ[constants.RELOAD_CONFIG] = str(reload) @@ -259,8 +258,8 @@ def get_app(reload: bool = False) -> ModuleType: importlib.reload(app) return app - except exceptions.ReflexError as ex: - telemetry.send("error", context="frontend", detail=str(ex)) + except Exception as ex: + telemetry.send_error(ex, context="frontend") raise diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index f7398f01a..e10ad94ee 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import multiprocessing import platform @@ -157,17 +158,7 @@ def _send_event(event_data: dict) -> bool: return False -def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool: - """Send anonymous telemetry for Reflex. - - Args: - event: The event name. - telemetry_enabled: Whether to send the telemetry (If None, get from config). - kwargs: Additional data to send with the event. - - Returns: - Whether the telemetry was sent successfully. - """ +def _send(event, telemetry_enabled, **kwargs): from reflex.config import get_config # Get the telemetry_enabled from the config if it is not specified. @@ -182,3 +173,36 @@ def send(event: str, telemetry_enabled: bool | None = None, **kwargs) -> bool: if not event_data: return False return _send_event(event_data) + + +def send(event: str, telemetry_enabled: bool | None = None, **kwargs): + """Send anonymous telemetry for Reflex. + + Args: + event: The event name. + telemetry_enabled: Whether to send the telemetry (If None, get from config). + kwargs: Additional data to send with the event. + """ + + async def async_send(event, telemetry_enabled, **kwargs): + return _send(event, telemetry_enabled, **kwargs) + + try: + # Within an event loop context, send the event asynchronously. + asyncio.create_task(async_send(event, telemetry_enabled, **kwargs)) + except RuntimeError: + # If there is no event loop, send the event synchronously. + _send(event, telemetry_enabled, **kwargs) + + +def send_error(error: Exception, context: str): + """Send an error event. + + Args: + error: The error to send. + context: The context of the error (e.g. "frontend" or "backend") + + Returns: + Whether the telemetry was sent successfully. + """ + return send("error", detail=type(error).__name__, context=context) diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 58786c67b..3a9eb17d0 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -29,7 +29,7 @@ def test_telemetry(): def test_disable(): """Test that disabling telemetry works.""" - assert not telemetry.send("test", telemetry_enabled=False) + assert not telemetry._send("test", telemetry_enabled=False) @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"]) @@ -43,7 +43,7 @@ def test_send(mocker, event): ) mocker.patch("platform.platform", return_value="Mocked Platform") - telemetry.send(event, telemetry_enabled=True) + telemetry._send(event, telemetry_enabled=True) httpx.post.assert_called_once() if telemetry.get_os() == "Windows": open.assert_called_with(".web\\reflex.json", "r")