Merge pull request #792 from christianlouis/copilot/enable-sentry-client-side
feat(ui): Add Sentry Browser SDK for client-side error tracking
This commit is contained in:
@@ -636,9 +636,26 @@ EMBEDDING_MAX_TOKENS=8000
|
||||
# Profiling is only active when SENTRY_TRACES_SAMPLE_RATE > 0. Default: 0.0 (disabled).
|
||||
# SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
#
|
||||
# Attach PII (IP addresses, user agents) to Sentry events.
|
||||
# Disable (default) to stay GDPR/CCPA compliant.
|
||||
# SENTRY_SEND_DEFAULT_PII=false
|
||||
# Attach PII (IP addresses, user agents) to Sentry events.
|
||||
# Disable (default) to stay GDPR/CCPA compliant.
|
||||
# SENTRY_SEND_DEFAULT_PII=false
|
||||
#
|
||||
# --- Browser (JavaScript) SDK ---
|
||||
# The same DSN is reused for the Sentry Browser SDK which is injected into
|
||||
# every rendered page. The DSN is a *public* key and is intentionally
|
||||
# embedded in client-side code.
|
||||
#
|
||||
# Fraction of browser navigations captured for client-side performance tracing.
|
||||
# 0.0 (default) disables browser tracing; 1.0 captures every navigation.
|
||||
# SENTRY_JS_TRACES_SAMPLE_RATE=0.0
|
||||
#
|
||||
# Fraction of browser sessions recorded by Sentry Session Replay.
|
||||
# 0.0 (default) disables session recording; 1.0 records every session.
|
||||
# SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0
|
||||
#
|
||||
# Fraction of error sessions recorded by Sentry Session Replay.
|
||||
# Defaults to 0.1 (10 %) so errors are captured with replay context.
|
||||
# SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.1
|
||||
|
||||
# **Mobile App – Push Notifications**
|
||||
# Push notifications are delivered via Expo's push notification service
|
||||
|
||||
@@ -1351,6 +1351,40 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Observability – Sentry Browser JavaScript SDK (client-side)
|
||||
# ---------------------------------------------------------------------------
|
||||
# The same SENTRY_DSN is reused for the browser SDK. The DSN is a *public*
|
||||
# key in Sentry's model and is intentionally embedded in client-side code.
|
||||
# All three settings below default to 0.0 / disabled so that operators opt-in
|
||||
# to the level of browser monitoring they want.
|
||||
# ---------------------------------------------------------------------------
|
||||
sentry_js_traces_sample_rate: float = Field(
|
||||
default=0.0,
|
||||
description=(
|
||||
"Fraction of browser page-loads captured for client-side performance tracing "
|
||||
"(0.0 – 1.0). 0.0 disables browser tracing; 1.0 captures every navigation. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
)
|
||||
sentry_js_replay_session_sample_rate: float = Field(
|
||||
default=0.0,
|
||||
description=(
|
||||
"Fraction of sessions recorded by Sentry Session Replay (0.0 – 1.0). "
|
||||
"0.0 disables session recording; 1.0 records every session. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
)
|
||||
sentry_js_replay_on_error_sample_rate: float = Field(
|
||||
default=0.1,
|
||||
description=(
|
||||
"Fraction of sessions with an error that will be recorded by Sentry Session "
|
||||
"Replay (0.0 – 1.0). Defaults to 0.1 (10 %) so that errors are captured "
|
||||
"with replay context even when session-level recording is disabled. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def strip_outer_quotes(cls, data: Any) -> Any:
|
||||
|
||||
@@ -3196,6 +3196,43 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_js_traces_sample_rate": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Fraction of browser page-loads captured for client-side Sentry performance tracing (0.0–1.0). "
|
||||
"0.0 (default) disables browser tracing; 1.0 captures every navigation. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
"type": "float",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_js_replay_session_sample_rate": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Fraction of sessions recorded by Sentry Session Replay (0.0–1.0). "
|
||||
"0.0 (default) disables session recording; 1.0 records every session. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
"type": "float",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
"sentry_js_replay_on_error_sample_rate": {
|
||||
"category": "Observability",
|
||||
"description": (
|
||||
"Fraction of error sessions recorded by Sentry Session Replay (0.0–1.0). "
|
||||
"Defaults to 0.1 (10%) so that errors are captured with replay context "
|
||||
"even when session-level recording is disabled. "
|
||||
"Only active when SENTRY_DSN is set."
|
||||
),
|
||||
"type": "float",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +96,21 @@ def _inject_global_context(ctx: dict) -> None:
|
||||
)
|
||||
ctx.setdefault("enable_factory_reset", getattr(settings, "enable_factory_reset", False))
|
||||
|
||||
# Sentry Browser SDK config (injected into every page so the JS SDK can initialise)
|
||||
# Normalize empty-string DSN to None so the {% if sentry_dsn %} template guard works correctly.
|
||||
_raw_dsn = getattr(settings, "sentry_dsn", None)
|
||||
ctx.setdefault("sentry_dsn", _raw_dsn if _raw_dsn else None)
|
||||
ctx.setdefault("sentry_environment", getattr(settings, "sentry_environment", "production"))
|
||||
ctx.setdefault("sentry_js_traces_sample_rate", getattr(settings, "sentry_js_traces_sample_rate", 0.0))
|
||||
ctx.setdefault(
|
||||
"sentry_js_replay_session_sample_rate",
|
||||
getattr(settings, "sentry_js_replay_session_sample_rate", 0.0),
|
||||
)
|
||||
ctx.setdefault(
|
||||
"sentry_js_replay_on_error_sample_rate",
|
||||
getattr(settings, "sentry_js_replay_on_error_sample_rate", 0.1),
|
||||
)
|
||||
|
||||
req = ctx.get("request")
|
||||
if req is not None:
|
||||
# CSRF token
|
||||
|
||||
@@ -1605,6 +1605,8 @@ No additional configuration is required — the auto-fill uses the authenticated
|
||||
|
||||
DocuElevate integrates with [Sentry](https://sentry.io) for real-time error tracking and performance monitoring. See [SentrySetup.md](./SentrySetup.md) for a full setup guide.
|
||||
|
||||
### Server-side (Python SDK)
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SENTRY_DSN` | Sentry DSN URL. When set, error reporting and performance tracing are enabled automatically. Leave blank to disable. | *(unset)* |
|
||||
@@ -1613,18 +1615,33 @@ DocuElevate integrates with [Sentry](https://sentry.io) for real-time error trac
|
||||
| `SENTRY_PROFILES_SAMPLE_RATE` | Fraction of profiled transactions sent to Sentry (0.0 – 1.0). Only active when traces > 0. | `0.0` |
|
||||
| `SENTRY_SEND_DEFAULT_PII` | Attach PII (IP addresses, user agents) to Sentry events. Disabled by default for GDPR/CCPA compliance. | `false` |
|
||||
|
||||
### Browser SDK (JavaScript)
|
||||
|
||||
The Sentry Browser SDK is loaded automatically on every rendered page when `SENTRY_DSN` is set. The same DSN is used for both server and browser — the DSN is a *public* key in Sentry's security model and is intentionally embedded in client-side code.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `SENTRY_JS_TRACES_SAMPLE_RATE` | Fraction of browser page-loads captured for client-side performance tracing (0.0 – 1.0). | `0.0` |
|
||||
| `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE` | Fraction of sessions recorded by [Sentry Session Replay](https://docs.sentry.io/product/session-replay/) (0.0 – 1.0). | `0.0` |
|
||||
| `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE` | Fraction of error sessions captured with session replay context (0.0 – 1.0). | `0.1` |
|
||||
|
||||
```bash
|
||||
# Minimal example
|
||||
# Minimal example (server + browser)
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
# Optional tuning
|
||||
# Optional server-side tuning
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false
|
||||
|
||||
# Optional browser-side tuning
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
> **Note:** Sentry is completely opt-in — if `SENTRY_DSN` is not set, the SDK is never initialised and no data leaves your infrastructure.
|
||||
> **Note:** Sentry is completely opt-in — if `SENTRY_DSN` is not set, neither SDK is initialised and no data leaves your infrastructure.
|
||||
|
||||
## Duplicate Document Detection
|
||||
|
||||
|
||||
+60
-11
@@ -2,13 +2,13 @@
|
||||
|
||||
DocuElevate ships with first-class support for [Sentry](https://sentry.io) — an open-source observability platform that provides real-time error tracking and performance monitoring.
|
||||
|
||||
When a **Sentry DSN** is configured, every unhandled exception in the FastAPI web process and Celery worker is automatically captured and sent to your Sentry project. Performance transactions (request traces, database queries, background task durations) are also recorded, giving you end-to-end visibility into your deployment.
|
||||
When a **Sentry DSN** is configured, every unhandled exception in the FastAPI web process and Celery worker is automatically captured and sent to your Sentry project. The **Sentry Browser SDK** is also injected into every rendered page, capturing client-side JavaScript errors, browser performance transactions, and (optionally) session replays. Together these give you full-stack, end-to-end visibility into your deployment.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Create a Sentry project** at <https://sentry.io> (or your self-hosted Sentry instance). Choose the **Python** platform.
|
||||
1. **Create a Sentry project** at <https://sentry.io> (or your self-hosted Sentry instance). Choose the **Python** platform (the same project and DSN are used for both the server and browser SDKs).
|
||||
2. Copy the **DSN** from *Project → Settings → Client Keys (DSN)*. It looks like:
|
||||
```
|
||||
https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
|
||||
@@ -17,12 +17,14 @@ When a **Sentry DSN** is configured, every unhandled exception in the FastAPI we
|
||||
```bash
|
||||
SENTRY_DSN=https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>
|
||||
```
|
||||
4. Restart DocuElevate. Sentry initialises automatically on startup — you will see a log line confirming activation.
|
||||
4. Restart DocuElevate. Sentry initialises automatically on startup — you will see a log line confirming activation. The Sentry Browser SDK `<script>` tag is automatically injected into every rendered page.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Server-side (Python SDK)
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SENTRY_DSN` | *(empty)* | Sentry DSN URL. **Required** to enable Sentry. Leave unset to disable. |
|
||||
@@ -31,6 +33,16 @@ 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. |
|
||||
|
||||
### Browser SDK (JavaScript)
|
||||
|
||||
The Sentry Browser SDK is loaded automatically on every rendered page when `SENTRY_DSN` is set. The DSN is a *public* key in Sentry's security model and is intentionally embedded in client-side code.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SENTRY_JS_TRACES_SAMPLE_RATE` | `0.0` | Fraction of browser page-loads captured for client-side performance tracing (0.0 – 1.0). |
|
||||
| `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE` | `0.0` | Fraction of sessions recorded by [Sentry Session Replay](https://docs.sentry.io/product/session-replay/) (0.0 – 1.0). |
|
||||
| `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE` | `0.1` | Fraction of error sessions captured with session replay context (0.0 – 1.0). |
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
@@ -42,9 +54,13 @@ All variables can alternatively be managed through the **Settings → Observabil
|
||||
```bash
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=development
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0 # Capture every request during development
|
||||
SENTRY_TRACES_SAMPLE_RATE=1.0 # Capture every request during development
|
||||
SENTRY_PROFILES_SAMPLE_RATE=1.0
|
||||
SENTRY_SEND_DEFAULT_PII=true # OK in dev; disable in production
|
||||
SENTRY_SEND_DEFAULT_PII=true # OK in dev; disable in production
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=1.0 # Capture every browser navigation
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=1.0 # Record every session
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=1.0
|
||||
```
|
||||
|
||||
### Staging
|
||||
@@ -55,6 +71,10 @@ SENTRY_ENVIRONMENT=staging
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.5
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.5
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=1.0
|
||||
```
|
||||
|
||||
### Production
|
||||
@@ -62,9 +82,13 @@ SENTRY_SEND_DEFAULT_PII=false
|
||||
```bash
|
||||
SENTRY_DSN=https://<key>@o<org>.ingest.sentry.io/<project>
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1 # 10 % sampling keeps quota low
|
||||
SENTRY_TRACES_SAMPLE_RATE=0.1 # 10 % sampling keeps quota low
|
||||
SENTRY_PROFILES_SAMPLE_RATE=0.0
|
||||
SENTRY_SEND_DEFAULT_PII=false # Default — required for GDPR compliance
|
||||
SENTRY_SEND_DEFAULT_PII=false # Default — required for GDPR compliance
|
||||
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE=0.1
|
||||
SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0 # Disabled; rely on error-triggered replay
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.1
|
||||
```
|
||||
|
||||
---
|
||||
@@ -80,6 +104,8 @@ services:
|
||||
SENTRY_DSN: "https://<key>@o<org>.ingest.sentry.io/<project>"
|
||||
SENTRY_ENVIRONMENT: "production"
|
||||
SENTRY_TRACES_SAMPLE_RATE: "0.1"
|
||||
SENTRY_JS_TRACES_SAMPLE_RATE: "0.1"
|
||||
SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE: "0.1"
|
||||
|
||||
worker:
|
||||
environment:
|
||||
@@ -88,6 +114,8 @@ services:
|
||||
SENTRY_TRACES_SAMPLE_RATE: "0.1"
|
||||
```
|
||||
|
||||
> The `worker` service only needs server-side variables — the Browser SDK runs in the user's browser and is configured via the `api` service.
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes
|
||||
@@ -110,6 +138,10 @@ env:
|
||||
value: production
|
||||
- name: SENTRY_TRACES_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
- name: SENTRY_JS_TRACES_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
- name: SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE
|
||||
value: "0.1"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -132,17 +164,24 @@ env:
|
||||
|
||||
Log messages at `ERROR` level and above are automatically forwarded to Sentry as events. Messages at `INFO` level are recorded as breadcrumbs (contextual trail leading up to an error).
|
||||
|
||||
### Browser (JavaScript SDK)
|
||||
|
||||
- **Client-side errors** — unhandled JavaScript exceptions and Promise rejections are captured automatically with browser context (URL, user agent, breadcrumbs).
|
||||
- **Browser performance** — page load, navigation, and resource timing data is captured as Sentry transactions when `SENTRY_JS_TRACES_SAMPLE_RATE > 0`.
|
||||
- **Session Replay** — screen recordings of user sessions (or just sessions containing errors) can be captured when the relevant replay sample rates are set above `0.0`. Replay data helps reproduce and diagnose hard-to-find UI bugs.
|
||||
|
||||
---
|
||||
|
||||
## Disabling Sentry
|
||||
|
||||
Simply leave `SENTRY_DSN` unset (or set it to an empty string). The SDK is never initialised and no data is sent.
|
||||
Simply leave `SENTRY_DSN` unset (or set it to an empty string). Neither the Python SDK nor the Browser SDK `<script>` tag is loaded, and no data is sent.
|
||||
|
||||
---
|
||||
|
||||
## SDK Version
|
||||
|
||||
DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,<3.0.0` with the `fastapi`, `celery`, and `sqlalchemy` extras.
|
||||
- **Server:** DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,<3.0.0` with the `fastapi`, `celery`, and `sqlalchemy` extras.
|
||||
- **Browser:** The `bundle.tracing.replay.min.js` bundle is loaded from the official Sentry CDN (`browser.sentry-cdn.com`). The version pin is in `frontend/templates/base.html`.
|
||||
|
||||
---
|
||||
|
||||
@@ -154,6 +193,13 @@ DocuElevate uses [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) `>=2.20.0,
|
||||
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.
|
||||
|
||||
### No browser errors in Sentry
|
||||
|
||||
1. Confirm `SENTRY_DSN` is set — the Browser SDK `<script>` tag is only injected when the DSN is non-empty.
|
||||
2. Open browser DevTools → Network and verify that the Sentry CDN bundle (`bundle.tracing.replay.min.js`) loads successfully (HTTP 200).
|
||||
3. Open DevTools → Console and run `window.Sentry` — it should be an object if the SDK loaded correctly.
|
||||
4. Check that `SENTRY_JS_TRACES_SAMPLE_RATE` and replay rates are set to values > 0 if you expect performance / replay data (they default to `0.0`).
|
||||
|
||||
### `sentry-sdk` import error
|
||||
|
||||
```
|
||||
@@ -164,11 +210,13 @@ Run `pip install 'sentry-sdk[fastapi,celery,sqlalchemy]'` inside your container,
|
||||
|
||||
### PII / GDPR concerns
|
||||
|
||||
By default `SENTRY_SEND_DEFAULT_PII=false`, which prevents IP addresses and user agents from being attached to events. Review Sentry's [data management documentation](https://docs.sentry.io/product/data-management-settings/) and your organisation's privacy policy before enabling PII.
|
||||
By default `SENTRY_SEND_DEFAULT_PII=false`, which prevents IP addresses and user agents from being attached to server-side events. Session Replay data may capture user interactions — review [Sentry's privacy documentation](https://docs.sentry.io/product/session-replay/privacy/) and your organisation's privacy policy before enabling replay.
|
||||
|
||||
### High Sentry quota usage
|
||||
|
||||
Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to disable performance tracing entirely.
|
||||
- Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to disable server-side performance tracing.
|
||||
- Set `SENTRY_JS_TRACES_SAMPLE_RATE=0.0` to disable browser performance tracing.
|
||||
- Set `SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE=0.0` and `SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE=0.0` to disable Session Replay entirely.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,6 +227,7 @@ Lower `SENTRY_TRACES_SAMPLE_RATE` (e.g. `0.05` for 5 %) or set it to `0.0` to di
|
||||
- Use **release tracking**: DocuElevate automatically sets `release` to the current `VERSION` string, enabling you to correlate errors with specific releases.
|
||||
- Set up **performance baselines** using Sentry's Performance dashboard so that you can detect regressions after deployments.
|
||||
- Review the [Sentry Python documentation](https://docs.sentry.io/platforms/python/) for advanced configuration options such as custom tags, user context, and scrubbing sensitive data.
|
||||
- Review the [Sentry Browser SDK documentation](https://docs.sentry.io/platforms/javascript/) for advanced browser SDK configuration.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,6 +34,32 @@
|
||||
{% block head_extra %}{% endblock %}
|
||||
<!-- CSRF token for AJAX/fetch requests -->
|
||||
<meta name="csrf-token" content="{{ csrf_token | default('', true) }}">
|
||||
{# ── Sentry Browser SDK ─────────────────────────────────────────────────
|
||||
Loaded only when SENTRY_DSN is configured. The DSN is a *public*
|
||||
Sentry key and is intentionally embedded in client-side code.
|
||||
Update the version pin at browser.sentry-cdn.com/releases when
|
||||
upgrading the SDK.
|
||||
──────────────────────────────────────────────────────────────────────── #}
|
||||
{% if sentry_dsn %}
|
||||
<script src="https://browser.sentry-cdn.com/9.x.x/bundle.tracing.replay.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
<script>
|
||||
if (window.Sentry) {
|
||||
Sentry.init({
|
||||
dsn: {{ sentry_dsn | tojson }},
|
||||
environment: {{ sentry_environment | default("production") | tojson }},
|
||||
release: {{ version | default(None) | tojson }},
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration(),
|
||||
],
|
||||
tracesSampleRate: {{ sentry_js_traces_sample_rate | default(0.0) }},
|
||||
replaysSessionSampleRate: {{ sentry_js_replay_session_sample_rate | default(0.0) }},
|
||||
replaysOnErrorSampleRate: {{ sentry_js_replay_on_error_sample_rate | default(0.1) }},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 min-h-screen flex flex-col"
|
||||
|
||||
@@ -210,6 +210,141 @@ class TestGetAppVersion:
|
||||
assert _get_app_version() is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSentryJsTemplateContext:
|
||||
"""Test that Sentry Browser SDK config is injected into the template context."""
|
||||
|
||||
def test_sentry_dsn_exposed_when_configured(self, mocker):
|
||||
"""sentry_dsn is set in the template context when SENTRY_DSN is configured."""
|
||||
mock_settings = mocker.patch("app.views.base.settings")
|
||||
mock_settings.sentry_dsn = "https://key@o1.ingest.sentry.io/1"
|
||||
mock_settings.sentry_environment = "production"
|
||||
mock_settings.sentry_js_traces_sample_rate = 0.1
|
||||
mock_settings.sentry_js_replay_session_sample_rate = 0.0
|
||||
mock_settings.sentry_js_replay_on_error_sample_rate = 0.1
|
||||
|
||||
from app.views.base import _inject_global_context
|
||||
|
||||
ctx: dict = {}
|
||||
_inject_global_context(ctx)
|
||||
|
||||
assert ctx["sentry_dsn"] == "https://key@o1.ingest.sentry.io/1"
|
||||
|
||||
def test_sentry_dsn_none_when_not_configured(self, mocker):
|
||||
"""sentry_dsn is None in the template context when SENTRY_DSN is unset."""
|
||||
mock_settings = mocker.patch("app.views.base.settings")
|
||||
mock_settings.sentry_dsn = None
|
||||
mock_settings.sentry_environment = "production"
|
||||
mock_settings.sentry_js_traces_sample_rate = 0.0
|
||||
mock_settings.sentry_js_replay_session_sample_rate = 0.0
|
||||
mock_settings.sentry_js_replay_on_error_sample_rate = 0.1
|
||||
|
||||
from app.views.base import _inject_global_context
|
||||
|
||||
ctx: dict = {}
|
||||
_inject_global_context(ctx)
|
||||
|
||||
assert ctx["sentry_dsn"] is None
|
||||
|
||||
def test_sentry_dsn_empty_string_becomes_none(self, mocker):
|
||||
"""An empty-string SENTRY_DSN is normalised to None in the template context."""
|
||||
mock_settings = mocker.patch("app.views.base.settings")
|
||||
mock_settings.sentry_dsn = ""
|
||||
mock_settings.sentry_environment = "production"
|
||||
mock_settings.sentry_js_traces_sample_rate = 0.0
|
||||
mock_settings.sentry_js_replay_session_sample_rate = 0.0
|
||||
mock_settings.sentry_js_replay_on_error_sample_rate = 0.1
|
||||
|
||||
from app.views.base import _inject_global_context
|
||||
|
||||
ctx: dict = {}
|
||||
_inject_global_context(ctx)
|
||||
|
||||
assert ctx["sentry_dsn"] is None
|
||||
|
||||
def test_js_sample_rates_exposed_in_context(self, mocker):
|
||||
"""Browser SDK sample rates are passed through to the template context."""
|
||||
mock_settings = mocker.patch("app.views.base.settings")
|
||||
mock_settings.sentry_dsn = "https://key@o1.ingest.sentry.io/1"
|
||||
mock_settings.sentry_environment = "staging"
|
||||
mock_settings.sentry_js_traces_sample_rate = 0.5
|
||||
mock_settings.sentry_js_replay_session_sample_rate = 0.2
|
||||
mock_settings.sentry_js_replay_on_error_sample_rate = 0.8
|
||||
|
||||
from app.views.base import _inject_global_context
|
||||
|
||||
ctx: dict = {}
|
||||
_inject_global_context(ctx)
|
||||
|
||||
assert ctx["sentry_environment"] == "staging"
|
||||
assert ctx["sentry_js_traces_sample_rate"] == 0.5
|
||||
assert ctx["sentry_js_replay_session_sample_rate"] == 0.2
|
||||
assert ctx["sentry_js_replay_on_error_sample_rate"] == 0.8
|
||||
|
||||
def test_js_sample_rates_default_values(self, mocker):
|
||||
"""Browser SDK sample rates fall back to safe defaults when attrs are absent."""
|
||||
mock_settings = mocker.patch("app.views.base.settings")
|
||||
# Simulate settings object without the new JS attributes
|
||||
del mock_settings.sentry_js_traces_sample_rate
|
||||
del mock_settings.sentry_js_replay_session_sample_rate
|
||||
del mock_settings.sentry_js_replay_on_error_sample_rate
|
||||
mock_settings.sentry_dsn = None
|
||||
mock_settings.sentry_environment = "production"
|
||||
|
||||
from app.views.base import _inject_global_context
|
||||
|
||||
ctx: dict = {}
|
||||
_inject_global_context(ctx)
|
||||
|
||||
assert ctx["sentry_js_traces_sample_rate"] == 0.0
|
||||
assert ctx["sentry_js_replay_session_sample_rate"] == 0.0
|
||||
assert ctx["sentry_js_replay_on_error_sample_rate"] == 0.1
|
||||
|
||||
|
||||
_MINIMAL_SETTINGS_KWARGS = {
|
||||
"database_url": "sqlite:///./test.db",
|
||||
"redis_url": "redis://localhost:6379",
|
||||
"openai_api_key": "test",
|
||||
"azure_ai_key": "test",
|
||||
"azure_region": "test",
|
||||
"azure_endpoint": "https://test.example.com",
|
||||
"gotenberg_url": "http://localhost:3000",
|
||||
"workdir": "/tmp",
|
||||
"auth_enabled": False,
|
||||
"session_secret": "a-test-secret-that-is-at-least-32-chars-long!",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSentryJsConfig:
|
||||
"""Test the new JS-specific Settings fields."""
|
||||
|
||||
def test_js_traces_sample_rate_default(self):
|
||||
"""SENTRY_JS_TRACES_SAMPLE_RATE defaults to 0.0."""
|
||||
from app.config import settings
|
||||
|
||||
assert settings.sentry_js_traces_sample_rate == 0.0
|
||||
|
||||
def test_js_replay_session_sample_rate_default(self):
|
||||
"""SENTRY_JS_REPLAY_SESSION_SAMPLE_RATE defaults to 0.0."""
|
||||
from app.config import settings
|
||||
|
||||
assert settings.sentry_js_replay_session_sample_rate == 0.0
|
||||
|
||||
def test_js_replay_on_error_sample_rate_default(self):
|
||||
"""SENTRY_JS_REPLAY_ON_ERROR_SAMPLE_RATE defaults to 0.1."""
|
||||
from app.config import settings
|
||||
|
||||
assert settings.sentry_js_replay_on_error_sample_rate == 0.1
|
||||
|
||||
def test_js_traces_sample_rate_can_be_set(self):
|
||||
"""SENTRY_JS_TRACES_SAMPLE_RATE can be set directly via constructor."""
|
||||
from app.config import Settings
|
||||
|
||||
s = Settings(**_MINIMAL_SETTINGS_KWARGS, sentry_js_traces_sample_rate=0.5)
|
||||
assert s.sentry_js_traces_sample_rate == 0.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user