Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the possibility to define and pass custom front- and backend event exception handlers to the rx.Config. #5

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ exclude_also =
ignore_errors = True

[html]
directory = coverage_html_report
directory = coverage_html_report
223 changes: 223 additions & 0 deletions integration/test_event_exception_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""Integration tests for event exception handlers."""
from __future__ import annotations

from typing import Generator
from unittest.mock import AsyncMock

import pytest

import time

from reflex.app import process
from reflex.event import Event
from reflex.state import StateManagerRedis

from reflex.testing import AppHarness
from reflex.config import get_config

from typing import Generator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the redundant import of Generator to avoid confusion and maintain clean code.

- from typing import Generator

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
from typing import Generator


import pytest
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the redundant import of pytest to avoid confusion and maintain clean code.

- import pytest

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
import pytest

from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


def TestApp():
Comment on lines +2 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort and format the import block for better readability and maintainability.

- from typing import Generator
- from unittest.mock import AsyncMock
- import pytest
- import time
- from reflex.app import process
- from reflex.event import Event
- from reflex.state import StateManagerRedis
- from reflex.testing import AppHarness
- from reflex.config import get_config
- from typing import Generator
- import pytest
- from selenium.webdriver.common.by import By
- from selenium.webdriver.remote.webdriver import WebDriver
- from selenium.webdriver.support.ui import WebDriverWait
- from selenium.webdriver.support import expected_conditions as EC
+ from __future__ import annotations
+ import time
+ import pytest
+ from unittest.mock import AsyncMock
+ from typing import Generator
+ from selenium.webdriver.common.by import By
+ from selenium.webdriver.remote.webdriver import WebDriver
+ from selenium.webdriver.support.ui import WebDriverWait
+ from selenium.webdriver.support import expected_conditions as EC
+ from reflex.app import process
+ from reflex.event import Event
+ from reflex.state import StateManagerRedis
+ from reflex.testing import AppHarness
+ from reflex.config import get_config

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
from __future__ import annotations
from typing import Generator
from unittest.mock import AsyncMock
import pytest
import time
from reflex.app import process
from reflex.event import Event
from reflex.state import StateManagerRedis
from reflex.testing import AppHarness
from reflex.config import get_config
from typing import Generator
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def TestApp():
from __future__ import annotations
import time
import pytest
from unittest.mock import AsyncMock
from typing import Generator
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from reflex.app import process
from reflex.event import Event
from reflex.state import StateManagerRedis
from reflex.testing import AppHarness
from reflex.config import get_config
def TestApp():

"""A test app for event exception handler integration."""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the blank line after the function docstring to adhere to PEP 257.

- 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
"""A test app for event exception handler integration."""
"""A test app for event exception handler integration."""


import reflex as rx

def frontend_exception_handler(message: str, stack: str):
print(f"[Fasfadgasdg] {message} {stack}")

class TestAppConfig(rx.Config):
"""Config for the TestApp app."""

class TestAppState(rx.State):
"""State for the TestApp app."""

value: int

def go(self, c: int):
"""Increment the value c times and update each time.

Args:
c: The number of times to increment.

Yields:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line after the last section ("Yields") for better readability and adherence to PEP 257.

+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
Yields:
Yields:

After each increment.
"""
for _ in range(c):
self.value += 1
yield

app = rx.App(state=rx.State)

@app.add_page
def index():
return rx.vstack(
rx.button(
"induce_frontend_error",
on_click=rx.call_script("induce_frontend_error()"),
id="induce-frontend-error-btn",
),
)


@pytest.fixture(scope="module")
def test_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start TestApp app at tmp_path via AppHarness.

Args:
tmp_path_factory: pytest tmp_path_factory fixture

Yields:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line after the last section ("Yields") for better readability and adherence to PEP 257.

+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
Yields:
Yields:

running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("test_app"),
app_source=TestApp, # type: ignore
) as harness:
yield harness


@pytest.fixture
def driver(test_app: AppHarness) -> Generator[WebDriver, None, None]:
"""Get an instance of the browser open to the test_app app.

Args:
test_app: harness for TestApp app

Yields:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line after the last section ("Yields") for better readability and adherence to PEP 257.

+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
Yields:
Yields:

WebDriver instance.
"""
assert test_app.app_instance is not None, "app is not running"
driver = test_app.frontend()
try:
yield driver
finally:
driver.quit()


def test_frontend_exception_handler_during_runtime(
driver: WebDriver,
capsys,
):
"""Test calling frontend exception handler during runtime.

We send an event containing a call to a non-existent function in the frontend.
This should trigger the default frontend exception handler.

Args:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line after the last section ("Args") for better readability and adherence to PEP 257.

+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
Args:
Args:

driver: WebDriver instance.
capsys: pytest fixture for capturing stdout and stderr.
"""
reset_button = WebDriverWait(driver, 20).until(
EC.element_to_be_clickable((By.ID, "induce-frontend-error-btn"))
)

# 1. Test the default frontend exception handler

reset_button.click()

# Wait for the error to be logged
time.sleep(2)

captured_default_handler_output = capsys.readouterr()
assert (
"[Reflex Frontend Exception]" in captured_default_handler_output.out
and "induce_frontend_error" in captured_default_handler_output.out
and "ReferenceError" in captured_default_handler_output.out
)

# 2. Test the custom frontend exception handler

def custom_frontend_exception_handler(message: str, stack: str) -> None:
print(f"[Custom Frontend Exception] {message} {stack}")

# Set the custom frontend exception handler
config = get_config()
config.frontend_exception_handler = custom_frontend_exception_handler

reset_button.click()

# Wait for the error to be logged
time.sleep(2)

captured_custom_handler_output = capsys.readouterr()
assert (
"[Custom Frontend Exception]" in captured_custom_handler_output.out
and "induce_frontend_error" in captured_custom_handler_output.out
and "ReferenceError" in captured_custom_handler_output.out
)


@pytest.mark.asyncio
async def test_backend_exception_handler_during_runtime(mocker, capsys, test_app):
"""Test calling backend exception handler during runtime.

Args:
mocker: mocker object.
capsys: capsys fixture.
test_app: harness for CallScript app.
driver: WebDriver instance.

"""
token = "mock_token"

router_data = {
"pathname": "/",
"query": {},
"token": token,
"sid": "mock_sid",
"headers": {},
"ip": "127.0.0.1",
}

app = test_app.app_instance
mocker.patch.object(app, "postprocess", AsyncMock())

payload = {"c": "5"} # should be an int

# 1. Test the default backend exception handler

event = Event(
token=token, name="test_app_state.go", payload=payload, router_data=router_data
)

async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
pass

captured_default_handler_output = capsys.readouterr()
assert (
"[Reflex Backend Exception]" in captured_default_handler_output.out
and "'str' object cannot be interpreted as an integer"
in captured_default_handler_output.out
)

# 2. Test the custom backend exception handler

def custom_backend_exception_handler(message: str, stack: str) -> None:
print(f"[Custom Backend Exception] {message} {stack}")

config = get_config()
config.backend_exception_handler = custom_backend_exception_handler

event = Event(
token=token, name="test_app_state.go", payload=payload, router_data=router_data
)

async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
pass

captured_custom_handler_output = capsys.readouterr()
assert (
"[Custom Backend Exception]" in captured_custom_handler_output.out
and "'str' object cannot be interpreted as an integer"
in captured_custom_handler_output.out
)

if isinstance(app.state_manager, StateManagerRedis):
await app.state_manager.close()
29 changes: 29 additions & 0 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ export const applyEvent = async (event, socket) => {
}
} catch (e) {
console.log("_call_script", e);
if (window && window?.onerror) {
window.onerror(e.message, null, null, null, e)
}
}
return false;
}
Expand Down Expand Up @@ -574,6 +577,32 @@ export const useEventLoop = (
queueEvents(events, socket);
};

// Handle frontend errors and send them to the backend via websocket.
useEffect(() => {

if (typeof window === 'undefined') {
return;

}

window.onerror = function (msg, url, lineNo, columnNo, error) {
addEvents([Event("state.handle_frontend_exception", {
message: error.message,
stack: error.stack,
})])
return false;
}

window.onunhandledrejection = function (event) {
addEvents([Event("state.handle_frontend_exception", {
message: event.reason.message,
stack: event.reason.stack,
})])
return false;
}

},[])

const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
useEffect(() => {
if (router.isReady && !sentHydrate.current) {
Expand Down
Loading
Loading