From 5864a4ed0630b87469c49232653062f286297953 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:17:18 +0000 Subject: [PATCH] fix(sentry): initialize Sentry after DB settings are loaded in lifespan Move init_sentry() from module level into the FastAPI lifespan context manager, immediately after load_settings_from_db() completes. This ensures that SENTRY_DSN and other Sentry settings configured via the database admin UI are picked up on every restart. Also update tests and docs accordingly. Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/main.py | 8 ++++---- docs/SentrySetup.md | 6 +++--- tests/test_main.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/main.py b/app/main.py index f6f3c890..258897e1 100644 --- a/app/main.py +++ b/app/main.py @@ -48,10 +48,6 @@ SESSION_SECRET = ( settings.session_secret or "INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS" ) -# Initialise Sentry as early as possible so that any startup errors are captured -init_sentry() - - @asynccontextmanager async def lifespan(app: FastAPI): """ @@ -74,6 +70,10 @@ async def lifespan(app: FastAPI): finally: db.close() + # Initialize Sentry after DB settings are loaded so that values configured + # via the database UI (e.g. SENTRY_DSN) are respected in addition to env vars. + init_sentry() + # Ensure OCR language data is available (background download, non-blocking) from app.utils.ocr_language_manager import ensure_ocr_languages_async diff --git a/docs/SentrySetup.md b/docs/SentrySetup.md index 8bf5091d..357345ac 100644 --- a/docs/SentrySetup.md +++ b/docs/SentrySetup.md @@ -13,7 +13,7 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we ``` https://@o.ingest.sentry.io/ ``` -3. Set the DSN in your environment: +3. Set the DSN in your environment **or** via the **Settings → Observability** section of the DocuElevate admin UI: ```bash SENTRY_DSN=https://@o.ingest.sentry.io/ ``` @@ -31,7 +31,7 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we | `SENTRY_PROFILES_SAMPLE_RATE` | `0.0` | Fraction of profiled transactions sent to Sentry (0.0 – 1.0). Only active when traces > 0. | | `SENTRY_SEND_DEFAULT_PII` | `false` | Attach PII (IP addresses, user agents) to events. Disable for GDPR / CCPA compliance. | -All variables can alternatively be managed through the **Settings → Observability** section of the DocuElevate admin UI. +All variables can alternatively be managed through the **Settings → Observability** section of the DocuElevate admin UI. Settings stored in the database are applied before Sentry initialises on every startup, so changes made via the UI take effect after a restart without requiring any changes to environment variables or `.env` files. --- @@ -151,7 +151,7 @@ DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0, ### No events appear in Sentry 1. Verify the DSN is correct and the Sentry project is active. -2. Check the application logs for the `Sentry initialised` message at startup. If it is absent, the DSN is not being read — confirm the environment variable name is `SENTRY_DSN`. +2. Check the application logs for the `Sentry initialised` message at startup. If it is absent, the DSN is not being read — confirm the environment variable name is `SENTRY_DSN`, or check the **Settings → Observability** section of the admin UI if you configured it there. 3. Test with `SENTRY_TRACES_SAMPLE_RATE=1.0` so that every request is sent. ### `sentry-sdk` import error diff --git a/tests/test_main.py b/tests/test_main.py index 851057ce..f490d223 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -48,6 +48,7 @@ class TestLifespanEvents: patch("app.utils.notification.init_apprise"), patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_shutdown"), + patch("app.utils.sentry.init_sentry"), ): # Mock database session mock_db = MagicMock() @@ -74,6 +75,7 @@ class TestLifespanEvents: patch("app.utils.notification.init_apprise"), patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_shutdown"), + patch("app.utils.sentry.init_sentry"), patch("logging.warning") as mock_warning, ): mock_db = MagicMock() @@ -101,6 +103,7 @@ class TestLifespanEvents: patch("app.utils.notification.init_apprise"), patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_shutdown"), + patch("app.utils.sentry.init_sentry"), patch("logging.error") as mock_error, ): mock_db = MagicMock() @@ -114,6 +117,35 @@ class TestLifespanEvents: mock_error.assert_called() + @pytest.mark.asyncio + async def test_lifespan_calls_init_sentry_after_db_settings_load(self): + """Test that init_sentry is called inside lifespan after load_settings_from_db.""" + from unittest.mock import call + + with ( + patch("app.database.init_db"), + patch("app.database.SessionLocal") as mock_session_cls, + patch("app.utils.config_loader.load_settings_from_db") as mock_load_settings, + patch("app.utils.config_validator.dump_all_settings"), + patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}), + patch("app.utils.notification.init_apprise"), + patch("app.utils.notification.notify_startup"), + patch("app.utils.notification.notify_shutdown"), + patch("app.main.init_sentry") as mock_init_sentry, + ): + mock_db = MagicMock() + mock_session_cls.return_value = mock_db + + from app.main import app, lifespan + + async with lifespan(app): + pass + + # init_sentry must have been called exactly once during startup + mock_init_sentry.assert_called_once() + # load_settings_from_db must also have been called + mock_load_settings.assert_called_once() + @pytest.mark.unit class TestExceptionHandlers: