# Self-hosted Sentry observability for the Gitea MCP server (#606) Optional, **off-by-default** instrumentation that reports MCP runtime errors, fail-closed workflow blockers, lease / terminal-lock / stale-runtime collisions, and recurring watchdog check-ins to a **self-hosted** Sentry at `https://sentry.prgs.cc/`. > **Gitea remains the source of truth.** Sentry is observe-only. It never > approves, merges, closes, or otherwise mutates Gitea workflow state, and it > never bypasses leases, #332, workflow roles, or the MCP gates. Sentry alerts > may only feed the *sanctioned* Gitea issue/comment path via the #612 incident > bridge — never a direct write. Implemented by [`sentry_observability.py`](../../sentry_observability.py). --- ## 1. Create the Sentry project 1. Sign in to the self-hosted Sentry at **`https://sentry.prgs.cc/`** (this is **not** Sentry Cloud — do not use `*.ingest.sentry.io`). 2. Create a new **Python** project named **`gitea-tools-mcp`**. 3. Open **Settings → Projects → gitea-tools-mcp → Client Keys (DSN)** and copy the DSN. It looks like `https://@sentry.prgs.cc/`. 4. **Never commit the DSN.** It is a runtime secret supplied via env var only. ## 2. Configure the environment All configuration is env-var driven (see [`.env.example`](../../.env.example)): | Variable | Purpose | Default | |----------|---------|---------| | `MCP_SENTRY_ENABLED` | Master gate (`1/true/yes/on`). Required. | off | | `SENTRY_DSN` | Self-hosted DSN. Required. | *(empty)* | | `SENTRY_ENVIRONMENT` | `local` / `dev` / `prod` tag. | `development` | | `SENTRY_RELEASE` | Release id (git SHA or version). | *(none)* | | `MCP_SENTRY_TRACES_SAMPLE_RATE` | Perf-trace sample rate `0.0–1.0` (clamped). | `0.0` | | `MCP_SENTRY_ENABLE_LOGS` | Forward Python logs as structured logs. | off | **The feature stays completely off unless `MCP_SENTRY_ENABLED` is truthy *and* `SENTRY_DSN` is non-empty.** With either missing, `init_sentry()` is a no-op, the SDK is never initialised, and no events are sent — existing tool behaviour and API-call patterns are unchanged. ### Per-environment examples ```bash # local (quiet: capture errors/blockers, no traces) export MCP_SENTRY_ENABLED=1 export SENTRY_DSN="https://@sentry.prgs.cc/" export SENTRY_ENVIRONMENT=local # dev (light tracing + logs) export MCP_SENTRY_ENABLED=1 export SENTRY_DSN="https://@sentry.prgs.cc/" export SENTRY_ENVIRONMENT=dev export MCP_SENTRY_TRACES_SAMPLE_RATE=0.2 export MCP_SENTRY_ENABLE_LOGS=1 # prod (errors/blockers + low-rate tracing, release-tagged) export MCP_SENTRY_ENABLED=1 export SENTRY_DSN="https://@sentry.prgs.cc/" export SENTRY_ENVIRONMENT=prod export SENTRY_RELEASE="$(git rev-parse --short HEAD)" export MCP_SENTRY_TRACES_SAMPLE_RATE=0.05 ``` The optional SDK is pinned in [`requirements.txt`](../../requirements.txt) (`sentry-sdk==2.20.0`). It is imported lazily: if the package is absent, the module still imports and every entry point is a safe no-op. ## 3. What is instrumented | Signal | Where | Notes | |--------|-------|-------| | Startup init | `gitea_mcp_server.py` `__main__`, before `mcp.run` | Prints a redaction-safe status line to stderr. | | Failing mutations (exceptions) | `_audited(...)` context manager | `capture_exception` with scrubbed tags. | | Fail-closed blockers / failed mutations | `_audit_pr_result(...)` (BLOCKED/FAILED) | Structured `capture_workflow_blocker` event incl. the canonical next action when available (criterion 7). | | Allocator watchdog check-ins | `gitea_allocate_next_work` tool | `allocator_health`, `stale_lease_scan`, `terminal_lock_scan`. | | Namespace-health check-in | `gitea_assess_mcp_namespace_health` tool | `namespace_health`. | All capture paths are **best-effort / fail open**: a Sentry outage or capture error never breaks an MCP tool success path. ## 4. Cron / watchdog monitors `sentry_observability.MONITOR_SLUGS` defines stable check-in slugs: | Registry key | Sentry monitor slug | Wired at | |--------------|--------------------|----------| | `stale_lease_scan` | `gitea-mcp-stale-lease-scan` | allocator run (global lease expiry) | | `terminal_lock_scan` | `gitea-mcp-terminal-lock-scan` | allocator run (terminal-lock lookup) | | `allocator_health` | `gitea-mcp-allocator-health` | allocator run | | `namespace_health` | `gitea-mcp-namespace-health` | namespace-health probe | | `dashboard_freshness` | `gitea-mcp-dashboard-freshness` | call `monitor_checkin("dashboard_freshness", ...)` from the dashboard refresh job (#605) | | `reconciler_cleanup` | `gitea-mcp-reconciler-cleanup` | call `monitor_checkin("reconciler_cleanup", ...)` from the reconciler cleanup entrypoint | Create matching Cron monitors in Sentry with those slugs. Emit an `in_progress` check-in at job start and `ok`/`error` at completion via `sentry_observability.monitor_checkin(slug_key, status)`. ## 5. Redaction guarantees (fail closed) Redaction fails *closed*: if a field cannot be proven safe it is dropped rather than sent. The `before_send` (and `before_send_log`) hook `scrub_event` recursively redacts every outgoing event; on any error it drops the event entirely. Guarantees, proven by `tests/test_sentry_observability.py`: - **No** tokens, passwords, keychain IDs, DSNs, cookies, or `user:pass@host`. - **No** raw session-state or full prompt/comment bodies — `session_id` is only ever surfaced as a 12-char `session_id_hash`. - **No** private config contents or raw credential headers. - **No** full local filesystem paths — a worktree path collapses to a coarse `worktree_category` (`author` / `reviewer` / `merger` / `reconciler` / `branches` / `root` / `other`). - Only the allowlisted tag keys in `ALLOWED_TAG_KEYS` are ever attached. ## 6. Coexistence with GlitchTip / the #612 incident bridge This is the **outbound** path (MCP → Sentry SDK). It complements — it does not replace — the **inbound** [`incident_bridge.py`](../../incident_bridge.py) (#612), which turns Sentry/GlitchTip *observations* into durable Gitea issues and `incident_links` rows. - Prefer **one** observability path per environment. Point the MCP server's `SENTRY_DSN` at the same self-hosted `gitea-tools-mcp` project that the #612 bridge reconciles from, so an MCP-reported error and its Gitea issue line up. - GlitchTip is Sentry-protocol compatible; if an existing GlitchTip DSN is in use, either migrate it to `https://sentry.prgs.cc/` or document the split (MCP → Sentry, legacy → GlitchTip) explicitly for operators. - The bridge remains the **only** sanctioned route from an alert back into Gitea workflow state. ## 7. Non-goals - Sentry must **not** become the workflow source of truth. - Sentry must **not** approve, merge, close, or mutate Gitea workflow state. - Sentry must **not** bypass leases, #332, workflow roles, or the MCP gates.