Merge pull request #573 from christianlouis/copilot/fix-sentry-settings-startup

fix(sentry): initialize Sentry after DB settings load in lifespan
This commit is contained in:
Christian Krakau-Louis
2026-03-09 00:46:36 +01:00
committed by GitHub
3 changed files with 38 additions and 6 deletions
+4 -3
View File
@@ -48,9 +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 +71,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
+3 -3
View File
@@ -13,7 +13,7 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we
```
https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
```
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://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
```
@@ -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
+31
View File
@@ -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,34 @@ 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."""
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: