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:
+4
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user