feat(backend): add Sentry/GlitchTip error reporting via sentry-spring-boot-starter-jakarta #592

Merged
marcel merged 8 commits from feat/issue-580-sentry-backend into main 2026-05-15 11:08:48 +02:00
Owner

Summary

  • Adds sentry-spring-boot-starter-jakarta 8.5.0 to backend/pom.xml — GlitchTip-compatible Sentry SDK for Spring Boot (Jakarta namespace)
  • Adds sentry: block to application.yaml: DSN from env var, env name from active Spring profile, 100% trace sampling, PII disabled, and DomainException excluded so 4xx domain errors do not flood GlitchTip
  • Passes SENTRY_DSN: ${SENTRY_DSN:-} into the backend service in docker-compose.yml (empty default = SDK silently disabled)
  • SENTRY_DSN= was already present in .env.example — no change needed

Closes #580

Test plan

  • ./mvnw clean package -DskipTests passes (BUILD SUCCESS verified locally)
  • Deploy to staging with SENTRY_DSN set — confirm a test error appears in GlitchTip
  • Confirm DomainException (e.g. 404) does NOT appear in GlitchTip

🤖 Generated with Claude Code

## Summary - Adds `sentry-spring-boot-starter-jakarta` 8.5.0 to `backend/pom.xml` — GlitchTip-compatible Sentry SDK for Spring Boot (Jakarta namespace) - Adds `sentry:` block to `application.yaml`: DSN from env var, env name from active Spring profile, 100% trace sampling, PII disabled, and `DomainException` excluded so 4xx domain errors do not flood GlitchTip - Passes `SENTRY_DSN: ${SENTRY_DSN:-}` into the backend service in `docker-compose.yml` (empty default = SDK silently disabled) - `SENTRY_DSN=` was already present in `.env.example` — no change needed Closes #580 ## Test plan - [x] `./mvnw clean package -DskipTests` passes (BUILD SUCCESS verified locally) - [ ] Deploy to staging with `SENTRY_DSN` set — confirm a test error appears in GlitchTip - [ ] Confirm `DomainException` (e.g. 404) does NOT appear in GlitchTip 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Owner

🏗️ Markus Keller — Senior Application Architect

Verdict: ⚠️ Approved with concerns

Blockers

traces-sample-rate: 1.0 is fine for dev but should be env-configurable for production

100% trace sampling means every request generates a span sent to GlitchTip. At the current volume this is fine, but the rate should be configurable so production can lower it without a code change:

sentry:
  traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0}

Add SENTRY_TRACES_SAMPLE_RATE= (empty = 1.0 default) to .env.example. Without this, any future tuning requires a redeploy.

This is a blocker because the observability stack is intended for production use and the config should be ops-adjustable from day one.

Suggestions

  • C4 L1 diagram: GlitchTip as an external system should appear in docs/architecture/c4/l1-context.puml. This was the responsibility of the #578 infrastructure PR — verify it was added there. If not, add it here.
  • Dependency management: sentry-spring-boot-starter-jakarta 8.5.0 is version-pinned inline. The project uses no Sentry BOM, so this is acceptable. Note: check https://github.com/getsentry/sentry-java/releases when upgrading — Sentry releases frequently.
  • ignored-exceptions-for-type: Correctly prevents DomainException (4xx) from flooding GlitchTip. Architecture decision is sound — domain exceptions are user-generated conditions, not system failures.
## 🏗️ Markus Keller — Senior Application Architect **Verdict: ⚠️ Approved with concerns** ### Blockers **`traces-sample-rate: 1.0` is fine for dev but should be env-configurable for production** 100% trace sampling means every request generates a span sent to GlitchTip. At the current volume this is fine, but the rate should be configurable so production can lower it without a code change: ```yaml sentry: traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0} ``` Add `SENTRY_TRACES_SAMPLE_RATE=` (empty = 1.0 default) to `.env.example`. Without this, any future tuning requires a redeploy. This is a blocker because the observability stack is intended for production use and the config should be ops-adjustable from day one. ### Suggestions - **C4 L1 diagram**: GlitchTip as an external system should appear in `docs/architecture/c4/l1-context.puml`. This was the responsibility of the #578 infrastructure PR — verify it was added there. If not, add it here. - **Dependency management**: `sentry-spring-boot-starter-jakarta 8.5.0` is version-pinned inline. The project uses no Sentry BOM, so this is acceptable. Note: check `https://github.com/getsentry/sentry-java/releases` when upgrading — Sentry releases frequently. - **`ignored-exceptions-for-type`**: Correctly prevents `DomainException` (4xx) from flooding GlitchTip. Architecture decision is sound — domain exceptions are user-generated conditions, not system failures.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

Blockers

None — the implementation is correct and minimal.

Suggestions

traces-sample-rate: 1.0 should be env-configurable

Same as Markus's concern. Hardcoding 1.0 means ops can't tune it without a redeploy. One-liner fix:

traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0}

No tests — but there's nothing to unit test here

This is pure Spring Boot autoconfiguration driven by application.yaml. The Sentry starter wires itself via @ConditionalOnProperty(name = "sentry.dsn"). The correct way to verify the filter behavior (DomainException → not sent) would be an integration test, but that requires a live GlitchTip DSN and a test endpoint — out of scope for this PR.

What I checked:

  • pom.xml: dependency placed after OTel block — sensible grouping. Comment explains the purpose.
  • application.yaml: all required fields present. send-default-pii: false is the safe default.
  • docker-compose.yml: ${SENTRY_DSN:-} empty-string default keeps the SDK silent without the env var. Placement is correct.
  • Build: ./mvnw clean package -DskipTests verified — no classpath conflicts with existing OTel dependencies.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### Blockers None — the implementation is correct and minimal. ### Suggestions **`traces-sample-rate: 1.0` should be env-configurable** Same as Markus's concern. Hardcoding 1.0 means ops can't tune it without a redeploy. One-liner fix: ```yaml traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0} ``` **No tests — but there's nothing to unit test here** This is pure Spring Boot autoconfiguration driven by `application.yaml`. The Sentry starter wires itself via `@ConditionalOnProperty(name = "sentry.dsn")`. The correct way to verify the filter behavior (`DomainException` → not sent) would be an integration test, but that requires a live GlitchTip DSN and a test endpoint — out of scope for this PR. **What I checked:** - `pom.xml`: dependency placed after OTel block — sensible grouping. Comment explains the purpose. - `application.yaml`: all required fields present. `send-default-pii: false` is the safe default. - `docker-compose.yml`: `${SENTRY_DSN:-}` empty-string default keeps the SDK silent without the env var. Placement is correct. - Build: `./mvnw clean package -DskipTests` verified — no classpath conflicts with existing OTel dependencies.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: Approved

What I checked

PII handling — send-default-pii: false
Correct. Without this, Sentry captures authenticated user info (username, IP, session) and attaches it to events. With it disabled, error events contain only stack traces and the exception message — no user identity data leaves the application.

DSN secret handling

  • SENTRY_DSN: ${SENTRY_DSN:-} in docker-compose.yml — the DSN is passed via env var, not hardcoded. The :- default produces an empty string, not a fallback DSN.
  • .env.example has SENTRY_DSN= (empty) — no real DSN committed to git.

DomainException exclusion — correct threat model
ignored-exceptions-for-type: org.raddatz.familienarchiv.exception.DomainException correctly suppresses user-generated 4xx errors. Only true 5xx unhandled exceptions reach GlitchTip. This is the right design — it prevents attacker-triggered 403/404 storms from flooding the error tracker and masking real alerts.

Trace data — note but not a blocker
traces-sample-rate: 1.0 at 100% means request spans are sent. send-default-pii: false suppresses cookies and auth headers from spans, but request URLs (including query parameters) may still be captured. Verify that no sensitive data appears in query parameters for your endpoints (e.g., /api/auth/reset-password?token=...). If any endpoint carries secrets in query params, add sentry.trace-options.include-query-parameters: false or filter those endpoints.

GlitchTip is self-hosted
All Sentry data goes to your own GlitchTip instance — no data leaves the network. This is the privacy-correct deployment model.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ✅ Approved** ### What I checked **PII handling — `send-default-pii: false` ✅** Correct. Without this, Sentry captures authenticated user info (username, IP, session) and attaches it to events. With it disabled, error events contain only stack traces and the exception message — no user identity data leaves the application. **DSN secret handling ✅** - `SENTRY_DSN: ${SENTRY_DSN:-}` in `docker-compose.yml` — the DSN is passed via env var, not hardcoded. The `:-` default produces an empty string, not a fallback DSN. - `.env.example` has `SENTRY_DSN=` (empty) — no real DSN committed to git. ✅ **DomainException exclusion — correct threat model ✅** `ignored-exceptions-for-type: org.raddatz.familienarchiv.exception.DomainException` correctly suppresses user-generated 4xx errors. Only true 5xx unhandled exceptions reach GlitchTip. This is the right design — it prevents attacker-triggered 403/404 storms from flooding the error tracker and masking real alerts. **Trace data — note but not a blocker** `traces-sample-rate: 1.0` at 100% means request spans are sent. `send-default-pii: false` suppresses cookies and auth headers from spans, but request URLs (including query parameters) may still be captured. Verify that no sensitive data appears in query parameters for your endpoints (e.g., `/api/auth/reset-password?token=...`). If any endpoint carries secrets in query params, add `sentry.trace-options.include-query-parameters: false` or filter those endpoints. **GlitchTip is self-hosted ✅** All Sentry data goes to your own GlitchTip instance — no data leaves the network. This is the privacy-correct deployment model.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Automation Specialist

Verdict: ⚠️ Approved with concerns

Blockers

None — pure configuration, no testable logic at the unit layer.

Concerns

DomainException filter is not regression-tested

The most important behavioral guarantee of this PR — that DomainException does NOT produce a GlitchTip event — has no automated test. If someone later renames DomainException, changes its package, or removes the YAML key, the filter silently stops working and 4xx noise floods the error tracker.

A regression test to add (integration layer, requires Testcontainers + MockSentry or a test endpoint):

@Test
void domain_exception_does_not_send_sentry_event() {
    // Arrange: configure a Sentry transport spy
    // Act: trigger an endpoint that throws DomainException.notFound(...)
    // Assert: no Sentry event was dispatched
}

This requires a test Sentry DSN or a mock transport — non-trivial without GlitchTip running in CI. Acceptable to defer, but a ticket should be opened.

Acceptance criteria gap: Issue #580 requires "When SENTRY_DSN is empty, application startup log contains Sentry is disabled" — this is the Sentry SDK's default behavior but is not verified in the test plan. A startup smoke test in the existing @SpringBootTest would confirm the autoconfiguration is wired correctly.

What I verified:

  • Build passes with -DskipTests per PR description
  • ignored-exceptions-for-type syntax is correct YAML
  • ${SENTRY_DSN:} (empty string default) is the standard Spring EL pattern for optional env vars
## 🧪 Sara Holt — QA Engineer & Test Automation Specialist **Verdict: ⚠️ Approved with concerns** ### Blockers None — pure configuration, no testable logic at the unit layer. ### Concerns **`DomainException` filter is not regression-tested** The most important behavioral guarantee of this PR — that `DomainException` does NOT produce a GlitchTip event — has no automated test. If someone later renames `DomainException`, changes its package, or removes the YAML key, the filter silently stops working and 4xx noise floods the error tracker. A regression test to add (integration layer, requires Testcontainers + MockSentry or a test endpoint): ```java @Test void domain_exception_does_not_send_sentry_event() { // Arrange: configure a Sentry transport spy // Act: trigger an endpoint that throws DomainException.notFound(...) // Assert: no Sentry event was dispatched } ``` This requires a test Sentry DSN or a mock transport — non-trivial without GlitchTip running in CI. Acceptable to defer, but a ticket should be opened. **Acceptance criteria gap**: Issue #580 requires "When `SENTRY_DSN` is empty, application startup log contains `Sentry is disabled`" — this is the Sentry SDK's default behavior but is not verified in the test plan. A startup smoke test in the existing `@SpringBootTest` would confirm the autoconfiguration is wired correctly. **What I verified:** - Build passes with `-DskipTests` per PR description ✅ - `ignored-exceptions-for-type` syntax is correct YAML ✅ - `${SENTRY_DSN:}` (empty string default) is the standard Spring EL pattern for optional env vars ✅
Author
Owner

🛠️ Tobias — DevOps Engineer

Verdict: ⚠️ Approved with concerns

Blockers

traces-sample-rate: 1.0 — ops cannot tune without redeploy

At 100% sampling, every request generates a span payload sent over the network to GlitchTip. For the current single-server deployment this is acceptable, but when traffic grows, this becomes a real overhead. The rate needs to be env-configurable so it can be adjusted at runtime:

sentry:
  traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0}
# docker-compose.yml backend environment:
SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0}
# .env.example
SENTRY_TRACES_SAMPLE_RATE=

Without this, lowering the sampling rate requires a code change + redeploy. That's the wrong operational model for a tunable metric.

Verified

  • SENTRY_DSN: ${SENTRY_DSN:-} in docker-compose.yml — empty-string default keeps backend healthy with no GlitchTip configured
  • No new ports, no new services, no attack surface change
  • Version pinned to 8.5.0 — no floating tags
  • No changes to healthcheck or restart policies
  • Comment in docker-compose.yml explains the Tempo OTLP non-fatal pattern — same pattern applies here (Sentry non-fatal when DSN absent)
## 🛠️ Tobias — DevOps Engineer **Verdict: ⚠️ Approved with concerns** ### Blockers **`traces-sample-rate: 1.0` — ops cannot tune without redeploy** At 100% sampling, every request generates a span payload sent over the network to GlitchTip. For the current single-server deployment this is acceptable, but when traffic grows, this becomes a real overhead. The rate needs to be env-configurable so it can be adjusted at runtime: ```yaml sentry: traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0} ``` ```yaml # docker-compose.yml backend environment: SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0} ``` ``` # .env.example SENTRY_TRACES_SAMPLE_RATE= ``` Without this, lowering the sampling rate requires a code change + redeploy. That's the wrong operational model for a tunable metric. ### Verified - `SENTRY_DSN: ${SENTRY_DSN:-}` in `docker-compose.yml` — empty-string default keeps backend healthy with no GlitchTip configured ✅ - No new ports, no new services, no attack surface change ✅ - Version pinned to `8.5.0` — no floating tags ✅ - No changes to healthcheck or restart policies ✅ - Comment in `docker-compose.yml` explains the Tempo OTLP non-fatal pattern — same pattern applies here (Sentry non-fatal when DSN absent) ✅
Author
Owner

📋 Elena Richter — Requirements Engineer

Verdict: Approved

Spec compliance check vs. issue #580

Requirement Status
sentry-spring-boot-starter-jakarta 8.5.0 in pom.xml Present
sentry.dsn: ${SENTRY_DSN:} in application.yaml Present
sentry.environment from SPRING_PROFILES_ACTIVE Present
sentry.send-default-pii: false Present
sentry.enable-tracing: true Present
ignored-exceptions-for-type: DomainException Present (full package name)
SENTRY_DSN: ${SENTRY_DSN:-} in docker-compose.yml Present
SENTRY_DSN= in .env.example Already present (no edit needed)
./mvnw clean package succeeds Verified in PR description

AC gaps (deferred to staging)

  • End-to-end test with live DSN (error appears in GlitchTip within 30s) — correctly deferred to staging verification
  • DomainException non-reporting verification — staging-only, noted in test plan

All spec requirements are satisfied. The test plan correctly separates compile-time verification from runtime GlitchTip integration.

## 📋 Elena Richter — Requirements Engineer **Verdict: ✅ Approved** ### Spec compliance check vs. issue #580 | Requirement | Status | |---|---| | `sentry-spring-boot-starter-jakarta` 8.5.0 in `pom.xml` | ✅ Present | | `sentry.dsn: ${SENTRY_DSN:}` in `application.yaml` | ✅ Present | | `sentry.environment` from `SPRING_PROFILES_ACTIVE` | ✅ Present | | `sentry.send-default-pii: false` | ✅ Present | | `sentry.enable-tracing: true` | ✅ Present | | `ignored-exceptions-for-type: DomainException` | ✅ Present (full package name) | | `SENTRY_DSN: ${SENTRY_DSN:-}` in `docker-compose.yml` | ✅ Present | | `SENTRY_DSN=` in `.env.example` | ✅ Already present (no edit needed) | | `./mvnw clean package` succeeds | ✅ Verified in PR description | ### AC gaps (deferred to staging) - End-to-end test with live DSN (error appears in GlitchTip within 30s) — correctly deferred to staging verification - `DomainException` non-reporting verification — staging-only, noted in test plan All spec requirements are satisfied. The test plan correctly separates compile-time verification from runtime GlitchTip integration.
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Verdict: Approved

No UI changes in this PR. Backend-only: dependency addition, YAML configuration, and Docker Compose env var passthrough.

Nothing to review from a UX, accessibility, or brand compliance perspective.

## 🎨 Leonie Voss — UX Designer & Accessibility Strategist **Verdict: ✅ Approved** No UI changes in this PR. Backend-only: dependency addition, YAML configuration, and Docker Compose env var passthrough. Nothing to review from a UX, accessibility, or brand compliance perspective.
Author
Owner

Blocker resolved in commit 5ee59ae9.

traces-sample-rate is now env-configurable via SENTRY_TRACES_SAMPLE_RATE (defaults to 1.0 if unset, preserving existing behaviour):

  • backend/src/main/resources/application.yamltraces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0}
  • docker-compose.ymlSENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0} added to the backend service environment: block
  • .env.exampleSENTRY_TRACES_SAMPLE_RATE= stub added after SENTRY_DSN=

Build verified with ./mvnw clean package -DskipTests → BUILD SUCCESS.

Blocker resolved in commit **5ee59ae9**. `traces-sample-rate` is now env-configurable via `SENTRY_TRACES_SAMPLE_RATE` (defaults to `1.0` if unset, preserving existing behaviour): - `backend/src/main/resources/application.yaml` — `traces-sample-rate: ${SENTRY_TRACES_SAMPLE_RATE:1.0}` - `docker-compose.yml` — `SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0}` added to the backend service `environment:` block - `.env.example` — `SENTRY_TRACES_SAMPLE_RATE=` stub added after `SENTRY_DSN=` Build verified with `./mvnw clean package -DskipTests` → BUILD SUCCESS.
marcel added 2 commits 2026-05-15 08:55:42 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(backend): make sentry traces-sample-rate env-configurable
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 6m4s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 7m9s
CI / fail2ban Regex (pull_request) Successful in 2m27s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m59s
68e4ff4121
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel force-pushed feat/issue-580-sentry-backend from 5ee59ae9f7 to 68e4ff4121 2026-05-15 08:55:42 +02:00 Compare
marcel added 1 commit 2026-05-15 09:25:20 +02:00
fix(backend): exclude SentryAutoConfiguration — Spring Boot 4/SF7 bean name incompatibility
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 6m26s
CI / OCR Service Tests (pull_request) Successful in 43s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
2139d600f5
SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration is a triply-
nested @Configuration class conditionally loaded when RestClient is on the classpath
(always true on Spring Framework 7). Spring Framework 7's bean name generator fails
on such deeply-nested @Import-ed classes, crashing every @SpringBootTest context.

Replace the broken auto-configuration with a minimal SentryConfig bean that calls
Sentry.init() with the same properties (DSN, environment, sample rate, PII guard,
DomainException filter). Unexpected 5xx exceptions are forwarded to Sentry via
Sentry.captureException() in GlobalExceptionHandler.handleGeneric().

Also add management.server.port=0 to application-test.yaml to eliminate TIME_WAIT
conflicts from @DirtiesContext restarts on the fixed management port 8081 (see #593).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

Blockers

No tests for SentryConfig.init() — this is a TDD violation. The @PostConstruct method has two distinct branches (blank DSN → no-op; non-blank DSN → Sentry.init) and neither is covered. The problem is structural: Sentry.init() is a static call, making the class untestable without Mockito's mockStatic. Two options:

  1. Wrap Sentry.init() in a package-private method and override it in tests.
  2. Extract a SentryInitializer interface with a no-op test double.

Without this, we have a @Configuration class with zero test coverage that gates our entire error reporting path.

No test for handleGeneric() Sentry capture (GlobalExceptionHandler.java:67) — the Sentry.captureException(ex) call was added to the catch-all handler but there is no test verifying it fires. At minimum a @WebMvcTest slice that triggers a 500 and asserts Sentry was called (via mockStatic(Sentry.class)) would cover this.

Suggestions

SentryAutoConfiguration exclusion is not asserted — the comment in application-test.yaml explains the workaround, but there is no test that would catch a regression if the exclusion were accidentally removed. A test that loads the full Spring context and asserts no SentryAutoConfiguration beans are present would prevent silent regressions.

The application-test.yaml comment is excellent ("Sentry is wired manually via SentryConfig instead. See #580.") — links the workaround back to the issue for future readers.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### Blockers **No tests for `SentryConfig.init()`** — this is a TDD violation. The `@PostConstruct` method has two distinct branches (blank DSN → no-op; non-blank DSN → Sentry.init) and neither is covered. The problem is structural: `Sentry.init()` is a static call, making the class untestable without Mockito's `mockStatic`. Two options: 1. Wrap `Sentry.init()` in a package-private method and override it in tests. 2. Extract a `SentryInitializer` interface with a no-op test double. Without this, we have a `@Configuration` class with zero test coverage that gates our entire error reporting path. **No test for `handleGeneric()` Sentry capture** (`GlobalExceptionHandler.java:67`) — the `Sentry.captureException(ex)` call was added to the catch-all handler but there is no test verifying it fires. At minimum a `@WebMvcTest` slice that triggers a 500 and asserts Sentry was called (via `mockStatic(Sentry.class)`) would cover this. ### Suggestions **`SentryAutoConfiguration` exclusion is not asserted** — the comment in `application-test.yaml` explains the workaround, but there is no test that would catch a regression if the exclusion were accidentally removed. A test that loads the full Spring context and asserts no `SentryAutoConfiguration` beans are present would prevent silent regressions. The `application-test.yaml` comment is excellent ("Sentry is wired manually via SentryConfig instead. See #580.") — links the workaround back to the issue for future readers.
Author
Owner

🏛️ Markus Keller — Software Architect

Verdict: Approved

Findings

Placement is correct. SentryConfig lives in config/ — the designated home for infrastructure wiring. No cross-domain coupling was introduced.

The workaround is pragmatic and well-documented. Excluding SentryAutoConfiguration because Spring Framework 7 cannot generate a bean name for SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration is a legitimate framework-level fix, not a design smell. The comments in application.yaml, application-test.yaml, and the class itself all explain the why — future maintainers will not be confused when they encounter spring.autoconfigure.exclude.

Layering is respected. GlobalExceptionHandler is the correct integration point for Sentry.captureException(). The handler is already the single funnel for all unhandled exceptions, so adding the capture there is idiomatic. No service or repository is directly touching Sentry.

No ADR required. This is a framework workaround, not an architectural decision. The issue (#580) and inline comments provide sufficient context. An ADR would be overkill here.

Suggestions

If Sentry releases a Spring Boot 4-compatible version (fixing the bean name generation), the migration path is clear: remove spring.autoconfigure.exclude, delete SentryConfig, keep the sentry: block in application.yaml. That path requires no architectural refactoring — the design supports it cleanly.

## 🏛️ Markus Keller — Software Architect **Verdict: ✅ Approved** ### Findings **Placement is correct.** `SentryConfig` lives in `config/` — the designated home for infrastructure wiring. No cross-domain coupling was introduced. **The workaround is pragmatic and well-documented.** Excluding `SentryAutoConfiguration` because Spring Framework 7 cannot generate a bean name for `SentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfiguration` is a legitimate framework-level fix, not a design smell. The comments in `application.yaml`, `application-test.yaml`, and the class itself all explain the *why* — future maintainers will not be confused when they encounter `spring.autoconfigure.exclude`. **Layering is respected.** `GlobalExceptionHandler` is the correct integration point for `Sentry.captureException()`. The handler is already the single funnel for all unhandled exceptions, so adding the capture there is idiomatic. No service or repository is directly touching Sentry. **No ADR required.** This is a framework workaround, not an architectural decision. The issue (#580) and inline comments provide sufficient context. An ADR would be overkill here. ### Suggestions If Sentry releases a Spring Boot 4-compatible version (fixing the bean name generation), the migration path is clear: remove `spring.autoconfigure.exclude`, delete `SentryConfig`, keep the `sentry:` block in `application.yaml`. That path requires no architectural refactoring — the design supports it cleanly.
Author
Owner

🔐 Nora "NullX" Steiner — Security Expert

Verdict: Approved

Findings

send-default-pii: false (application.yaml:133) — correct. No IP addresses, usernames, request headers, or session tokens will be sent to GlitchTip. This is the safe default and must stay.

DSN handling is safe. SentryConfig.init() returns early when DSN is blank (if (dsn == null || dsn.isBlank()) return). On environments without SENTRY_DSN set, Sentry is simply not initialised — no silent partial init that could leak data.

DomainException excluded from capture — correct. DomainException covers 4xx business errors (not found, forbidden, conflict) that are expected and not actionable. Sending them to Sentry would create noise and could leak user-intent data in exception messages.

Catch-all scope is correct. Sentry.captureException(ex) lives only in handleGeneric(Exception ex) — the handler that fires for unexpected 5xx errors. Validation errors, auth errors, and domain errors are handled in their dedicated handlers above it and intentionally bypass Sentry. This is the right signal/noise balance.

SENTRY_TRACES_SAMPLE_RATE in docker-compose defaults to 1.0. At 100% trace sampling, transaction payloads sent to GlitchTip could include URL parameters. For a family archive this is an acceptable risk (self-hosted GlitchTip, closed user base), but worth being explicit about in the deployment docs.

## 🔐 Nora "NullX" Steiner — Security Expert **Verdict: ✅ Approved** ### Findings **`send-default-pii: false`** (`application.yaml:133`) — correct. No IP addresses, usernames, request headers, or session tokens will be sent to GlitchTip. This is the safe default and must stay. **DSN handling is safe.** `SentryConfig.init()` returns early when DSN is blank (`if (dsn == null || dsn.isBlank()) return`). On environments without `SENTRY_DSN` set, Sentry is simply not initialised — no silent partial init that could leak data. **`DomainException` excluded from capture** — correct. `DomainException` covers 4xx business errors (not found, forbidden, conflict) that are expected and not actionable. Sending them to Sentry would create noise and could leak user-intent data in exception messages. **Catch-all scope is correct.** `Sentry.captureException(ex)` lives only in `handleGeneric(Exception ex)` — the handler that fires for unexpected 5xx errors. Validation errors, auth errors, and domain errors are handled in their dedicated handlers above it and intentionally bypass Sentry. This is the right signal/noise balance. **`SENTRY_TRACES_SAMPLE_RATE` in docker-compose defaults to `1.0`.** At 100% trace sampling, transaction payloads sent to GlitchTip could include URL parameters. For a family archive this is an acceptable risk (self-hosted GlitchTip, closed user base), but worth being explicit about in the deployment docs.
Author
Owner

🚀 Tobias Wendt — DevOps Engineer

Verdict: ⚠️ Approved with concerns

Blockers

SENTRY_DSN is missing from .env.exampleSENTRY_TRACES_SAMPLE_RATE was added but SENTRY_DSN was not. Any operator setting up a new deployment will see no hint that SENTRY_DSN needs to be set. .env.example is the operator's checklist. Both vars should appear there, with a comment explaining where to get the DSN from GlitchTip.

Suggestions

SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0} in docker-compose (docker-compose.yml) — 100% trace sampling is expensive on any non-trivial Sentry/GlitchTip plan. For a family archive on self-hosted GlitchTip this is probably fine, but the default could mislead operators who adopt this compose file as a template. Consider defaulting to 0.1 (10%) and letting the operator opt up.

management.server.port: 0 in test config (application-test.yaml:33) — this is a genuinely good operational fix. The TIME_WAIT problem it solves (@DirtiesContext with a fixed management port causing 17+ minute test suite delays) is well-documented with a comment. No concerns here.

otel.sdk.disabled: true in test config (application-test.yaml:25-27) — prevents Azure resource provider lookup failures in CI when no OTel stack is present. Correct and necessary.

CI workflow unchanged — no new secrets or environment variables need to be wired in the Gitea Actions workflow for this PR; SENTRY_DSN is an opt-in production env var, not a CI requirement. Clean.

## 🚀 Tobias Wendt — DevOps Engineer **Verdict: ⚠️ Approved with concerns** ### Blockers **`SENTRY_DSN` is missing from `.env.example`** — `SENTRY_TRACES_SAMPLE_RATE` was added but `SENTRY_DSN` was not. Any operator setting up a new deployment will see no hint that `SENTRY_DSN` needs to be set. `.env.example` is the operator's checklist. Both vars should appear there, with a comment explaining where to get the DSN from GlitchTip. ### Suggestions **`SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-1.0}` in docker-compose** (`docker-compose.yml`) — 100% trace sampling is expensive on any non-trivial Sentry/GlitchTip plan. For a family archive on self-hosted GlitchTip this is probably fine, but the default could mislead operators who adopt this compose file as a template. Consider defaulting to `0.1` (10%) and letting the operator opt up. **`management.server.port: 0` in test config** (`application-test.yaml:33`) — this is a genuinely good operational fix. The TIME_WAIT problem it solves (`@DirtiesContext` with a fixed management port causing 17+ minute test suite delays) is well-documented with a comment. No concerns here. **`otel.sdk.disabled: true` in test config** (`application-test.yaml:25-27`) — prevents Azure resource provider lookup failures in CI when no OTel stack is present. Correct and necessary. **CI workflow unchanged** — no new secrets or environment variables need to be wired in the Gitea Actions workflow for this PR; `SENTRY_DSN` is an opt-in production env var, not a CI requirement. Clean.
Author
Owner

🧪 Sara Holt — QA Engineer / Tester

Verdict: 🚫 Changes requested

Blockers

Zero test coverage for SentryConfig — the class has two observable behaviours:

  1. When DSN is blank → Sentry.init() must NOT be called
  2. When DSN is non-blank → Sentry.init() must be called with the configured options (DSN, environment, sample rate, sendDefaultPii=false, DomainException ignored)

Neither branch is tested. Sentry.init() is a static method, but Mockito's mockStatic(Sentry.class) makes it testable. Without coverage, a silent regression (e.g. init() being called even with a blank DSN) would go undetected in CI.

handleGeneric() Sentry capture is untested (GlobalExceptionHandler.java:67) — a @WebMvcTest slice test that throws an arbitrary RuntimeException through a controller and verifies Sentry.captureException() was called would close this gap. Currently there is no assertion that the Sentry capture actually fires.

Suggestions

Regression guard for SentryAutoConfiguration exclusion — the exclusion in application-test.yaml is load-bearing: without it, integration tests fail with a bean-name-generation error. A test that asserts SentryAutoConfiguration is not in the ApplicationContext would catch an accidental removal. This can be a trivial @SpringBootTest slice:

@SpringBootTest
class SentryAutoConfigurationExclusionTest {
    @Autowired
    private ApplicationContext ctx;

    @Test
    void sentryAutoConfigurationIsExcluded() {
        assertThat(ctx.containsBean("sentryAutoConfiguration")).isFalse();
    }
}

management.server.port: 0 fix (application-test.yaml:33) — this is the right fix for the TIME_WAIT test slowdown. Verified by the CI runtime improvement. No test concerns here.

## 🧪 Sara Holt — QA Engineer / Tester **Verdict: 🚫 Changes requested** ### Blockers **Zero test coverage for `SentryConfig`** — the class has two observable behaviours: 1. When DSN is blank → `Sentry.init()` must NOT be called 2. When DSN is non-blank → `Sentry.init()` must be called with the configured options (DSN, environment, sample rate, `sendDefaultPii=false`, `DomainException` ignored) Neither branch is tested. `Sentry.init()` is a static method, but Mockito's `mockStatic(Sentry.class)` makes it testable. Without coverage, a silent regression (e.g. `init()` being called even with a blank DSN) would go undetected in CI. **`handleGeneric()` Sentry capture is untested** (`GlobalExceptionHandler.java:67`) — a `@WebMvcTest` slice test that throws an arbitrary `RuntimeException` through a controller and verifies `Sentry.captureException()` was called would close this gap. Currently there is no assertion that the Sentry capture actually fires. ### Suggestions **Regression guard for `SentryAutoConfiguration` exclusion** — the exclusion in `application-test.yaml` is load-bearing: without it, integration tests fail with a bean-name-generation error. A test that asserts `SentryAutoConfiguration` is not in the `ApplicationContext` would catch an accidental removal. This can be a trivial `@SpringBootTest` slice: ```java @SpringBootTest class SentryAutoConfigurationExclusionTest { @Autowired private ApplicationContext ctx; @Test void sentryAutoConfigurationIsExcluded() { assertThat(ctx.containsBean("sentryAutoConfiguration")).isFalse(); } } ``` **`management.server.port: 0` fix** (`application-test.yaml:33`) — this is the right fix for the TIME_WAIT test slowdown. Verified by the CI runtime improvement. No test concerns here.
Author
Owner

📋 Elicit — Requirements Engineer

Verdict: ⚠️ Approved with concerns

Requirements Traceability (Issue #580)

Checking stated acceptance criteria against the implementation:

Requirement Status Notes
Add sentry-spring-boot-starter-jakarta dependency pom.xml — version 8.5.0
Configure DSN via env var SENTRY_DSN application.yaml, docker-compose.yml
Set environment from SPRING_PROFILES_ACTIVE sentry.environment: ${SPRING_PROFILES_ACTIVE:dev}
send-default-pii: false application.yaml:133
Exclude DomainException from Sentry capture SentryConfig.java:36 + application.yaml:136
Capture unhandled exceptions GlobalExceptionHandler.java:67
Works with Spring Boot 4 / Spring Framework 7 Auto-config excluded; manual init via SentryConfig
No-op when DSN is not configured Early return in SentryConfig.init()

Concerns

SENTRY_DSN absent from .env.example — this is an operator-facing acceptance criterion. Without the entry in .env.example, a new deployment operator has no indication that this variable exists. The feature is technically complete but not operationally discoverable.

Trace sampling defaultSENTRY_TRACES_SAMPLE_RATE defaults to 1.0 (100%). The issue #580 does not specify a required default, but the deployment guide (if updated in #581) should explicitly document this and recommend reducing it for production deployments where Sentry/GlitchTip quota matters.

## 📋 Elicit — Requirements Engineer **Verdict: ⚠️ Approved with concerns** ### Requirements Traceability (Issue #580) Checking stated acceptance criteria against the implementation: | Requirement | Status | Notes | |---|---|---| | Add `sentry-spring-boot-starter-jakarta` dependency | ✅ | `pom.xml` — version 8.5.0 | | Configure DSN via env var `SENTRY_DSN` | ✅ | `application.yaml`, `docker-compose.yml` | | Set environment from `SPRING_PROFILES_ACTIVE` | ✅ | `sentry.environment: ${SPRING_PROFILES_ACTIVE:dev}` | | `send-default-pii: false` | ✅ | `application.yaml:133` | | Exclude `DomainException` from Sentry capture | ✅ | `SentryConfig.java:36` + `application.yaml:136` | | Capture unhandled exceptions | ✅ | `GlobalExceptionHandler.java:67` | | Works with Spring Boot 4 / Spring Framework 7 | ✅ | Auto-config excluded; manual init via `SentryConfig` | | No-op when DSN is not configured | ✅ | Early return in `SentryConfig.init()` | ### Concerns **`SENTRY_DSN` absent from `.env.example`** — this is an operator-facing acceptance criterion. Without the entry in `.env.example`, a new deployment operator has no indication that this variable exists. The feature is technically complete but not operationally discoverable. **Trace sampling default** — `SENTRY_TRACES_SAMPLE_RATE` defaults to `1.0` (100%). The issue #580 does not specify a required default, but the deployment guide (if updated in #581) should explicitly document this and recommend reducing it for production deployments where Sentry/GlitchTip quota matters.
Author
Owner

🎨 Leonie Voss — UX/UI Expert

Verdict: Approved

No frontend or UI changes in this PR — pure backend infrastructure integration.

Checked: no new routes, no new components, no i18n keys, no API surface changes, no Svelte files modified. This PR is invisible to end users and has no UX implications.

LGTM from a UI perspective.

## 🎨 Leonie Voss — UX/UI Expert **Verdict: ✅ Approved** No frontend or UI changes in this PR — pure backend infrastructure integration. Checked: no new routes, no new components, no i18n keys, no API surface changes, no Svelte files modified. This PR is invisible to end users and has no UX implications. LGTM from a UI perspective.
marcel added 4 commits 2026-05-15 09:52:28 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(backend): switch to sentry-spring-boot-4:8.41.0 for Spring Boot 4/SF7 compatibility
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 6m12s
CI / OCR Service Tests (pull_request) Successful in 42s
CI / Backend Unit Tests (pull_request) Failing after 17m13s
CI / fail2ban Regex (pull_request) Successful in 2m37s
CI / Compose Bucket Idempotency (pull_request) Successful in 2m6s
7c2e75facc
sentry-spring-boot-starter-jakarta 8.5.0 does not support Spring Boot 4.0 —
it logs an "Incompatible Spring Boot Version" warning and its SentryAutoConfiguration
crashes SF7 bean-name generation. sentry-spring-boot-4 (added in 8.21.0) is the
dedicated Spring Boot 4 module with a fixed auto-configuration class.

- Replace sentry-spring-boot-starter-jakarta:8.5.0 with sentry-spring-boot-4:8.41.0
- Delete SentryConfig.java — workaround no longer needed, auto-config handles init
- Remove spring.autoconfigure.exclude from application.yaml + application-test.yaml
- Delete SentryConfigTest.java — tested the deleted workaround class
- Update ApplicationContextTest: assert Sentry.isEnabled() is false when no DSN set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Reviewer concerns addressed — updated push 7c2e75fa

Felix / Sara blockers: test coverage for Sentry wiring

Added in commits fa191b5c and 20edc047:

  • GlobalExceptionHandlerTest — verifies Sentry.captureException(ex) is called and 500 is returned
  • ApplicationContextTest — asserts Sentry.isEnabled() is false when no DSN is configured (no accidental init in tests)

Root cause fix: wrong Sentry module for Spring Boot 4 (7c2e75fa)

The "Incompatible Spring Boot Version" warning visible in CI logs (run 1690) revealed that sentry-spring-boot-starter-jakarta:8.5.0 does not support Spring Boot 4.0. Sentry added a dedicated sentry-spring-boot-4 module in version 8.21.0 specifically for Spring Boot 4 / Spring Framework 7.

Migrated in commit 7c2e75fa:

  • pom.xml: sentry-spring-boot-starter-jakarta:8.5.0sentry-spring-boot-4:8.41.0
  • Deleted SentryConfig.java: the manual Sentry.init() workaround is no longer needed — the new module's auto-configuration handles initialization correctly via sentry.* properties
  • Removed spring.autoconfigure.exclude from both application.yaml and application-test.yaml — the new module's SentryAutoConfiguration (package io.sentry.spring.boot4) does not crash under SF7
  • Deleted SentryConfigTest.java: tested the now-deleted workaround class

The Sentry.captureException() call in GlobalExceptionHandler.handleGeneric() and all sentry: properties in application.yaml are unchanged and fully compatible with the new module.

## Reviewer concerns addressed — updated push `7c2e75fa` ### Felix / Sara blockers: test coverage for Sentry wiring Added in commits `fa191b5c` and `20edc047`: - `GlobalExceptionHandlerTest` — verifies `Sentry.captureException(ex)` is called and 500 is returned - `ApplicationContextTest` — asserts `Sentry.isEnabled()` is false when no DSN is configured (no accidental init in tests) ### Root cause fix: wrong Sentry module for Spring Boot 4 (`7c2e75fa`) The "Incompatible Spring Boot Version" warning visible in CI logs (run 1690) revealed that `sentry-spring-boot-starter-jakarta:8.5.0` does not support Spring Boot 4.0. Sentry added a dedicated `sentry-spring-boot-4` module in version 8.21.0 specifically for Spring Boot 4 / Spring Framework 7. Migrated in commit `7c2e75fa`: - **`pom.xml`**: `sentry-spring-boot-starter-jakarta:8.5.0` → `sentry-spring-boot-4:8.41.0` - **Deleted `SentryConfig.java`**: the manual `Sentry.init()` workaround is no longer needed — the new module's auto-configuration handles initialization correctly via `sentry.*` properties - **Removed `spring.autoconfigure.exclude`** from both `application.yaml` and `application-test.yaml` — the new module's `SentryAutoConfiguration` (package `io.sentry.spring.boot4`) does not crash under SF7 - **Deleted `SentryConfigTest.java`**: tested the now-deleted workaround class The `Sentry.captureException()` call in `GlobalExceptionHandler.handleGeneric()` and all `sentry:` properties in `application.yaml` are unchanged and fully compatible with the new module.
marcel added 1 commit 2026-05-15 10:34:00 +02:00
perf(test): replace DirtiesContext(AFTER_EACH_TEST_METHOD) with @Transactional
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m40s
CI / OCR Service Tests (pull_request) Successful in 18s
CI / Backend Unit Tests (pull_request) Successful in 3m20s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
CI / Unit & Component Tests (push) Successful in 4m20s
CI / OCR Service Tests (push) Successful in 16s
CI / Backend Unit Tests (push) Successful in 3m8s
CI / fail2ban Regex (push) Successful in 44s
CI / Compose Bucket Idempotency (push) Successful in 1m1s
4358997482
4 integration test classes were restarting the full Spring context (and a new
Postgres Testcontainer, ~75s each) after every test method — 10 unnecessary
container startups adding ~12 minutes to CI. Fixed by:

- PersonServiceIntegrationTest, DocumentSearchPagedIntegrationTest,
  GeschichteServiceIntegrationTest: swap to @Transactional so each test
  rolls back instead of destroying the context.
- AuditServiceIntegrationTest: cannot use @Transactional (logAfterCommit
  hooks into AFTER_COMMIT which requires a real commit); reset state with
  @BeforeEach deleteAll() instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 4358997482 into main 2026-05-15 11:08:48 +02:00
marcel deleted branch feat/issue-580-sentry-backend 2026-05-15 11:08:49 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#592