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" 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 @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -74,6 +71,10 @@ async def lifespan(app: FastAPI):
finally: finally:
db.close() 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) # Ensure OCR language data is available (background download, non-blocking)
from app.utils.ocr_language_manager import ensure_ocr_languages_async 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> 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 ```bash
SENTRY_DSN=https://<public_key>@o<org_id>.ingest.sentry.io/<project_id> 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_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. | | `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 ### No events appear in Sentry
1. Verify the DSN is correct and the Sentry project is active. 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. 3. Test with `SENTRY_TRACES_SAMPLE_RATE=1.0` so that every request is sent.
### `sentry-sdk` import error ### `sentry-sdk` import error
+31
View File
@@ -48,6 +48,7 @@ class TestLifespanEvents:
patch("app.utils.notification.init_apprise"), patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_startup"),
patch("app.utils.notification.notify_shutdown"), patch("app.utils.notification.notify_shutdown"),
patch("app.utils.sentry.init_sentry"),
): ):
# Mock database session # Mock database session
mock_db = MagicMock() mock_db = MagicMock()
@@ -74,6 +75,7 @@ class TestLifespanEvents:
patch("app.utils.notification.init_apprise"), patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_startup"),
patch("app.utils.notification.notify_shutdown"), patch("app.utils.notification.notify_shutdown"),
patch("app.utils.sentry.init_sentry"),
patch("logging.warning") as mock_warning, patch("logging.warning") as mock_warning,
): ):
mock_db = MagicMock() mock_db = MagicMock()
@@ -101,6 +103,7 @@ class TestLifespanEvents:
patch("app.utils.notification.init_apprise"), patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_startup"), patch("app.utils.notification.notify_startup"),
patch("app.utils.notification.notify_shutdown"), patch("app.utils.notification.notify_shutdown"),
patch("app.utils.sentry.init_sentry"),
patch("logging.error") as mock_error, patch("logging.error") as mock_error,
): ):
mock_db = MagicMock() mock_db = MagicMock()
@@ -114,6 +117,34 @@ class TestLifespanEvents:
mock_error.assert_called() 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 @pytest.mark.unit
class TestExceptionHandlers: class TestExceptionHandlers: