feat(backend): add Sentry/GlitchTip error reporting via sentry-spring-boot-starter-jakarta #592
Reference in New Issue
Block a user
Delete Branch "feat/issue-580-sentry-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
sentry-spring-boot-starter-jakarta8.5.0 tobackend/pom.xml— GlitchTip-compatible Sentry SDK for Spring Boot (Jakarta namespace)sentry:block toapplication.yaml: DSN from env var, env name from active Spring profile, 100% trace sampling, PII disabled, andDomainExceptionexcluded so 4xx domain errors do not flood GlitchTipSENTRY_DSN: ${SENTRY_DSN:-}into the backend service indocker-compose.yml(empty default = SDK silently disabled)SENTRY_DSN=was already present in.env.example— no change neededCloses #580
Test plan
./mvnw clean package -DskipTestspasses (BUILD SUCCESS verified locally)SENTRY_DSNset — confirm a test error appears in GlitchTipDomainException(e.g. 404) does NOT appear in GlitchTip🤖 Generated with Claude Code
🏗️ Markus Keller — Senior Application Architect
Verdict: ⚠️ Approved with concerns
Blockers
traces-sample-rate: 1.0is fine for dev but should be env-configurable for production100% 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:
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
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.sentry-spring-boot-starter-jakarta 8.5.0is version-pinned inline. The project uses no Sentry BOM, so this is acceptable. Note: checkhttps://github.com/getsentry/sentry-java/releaseswhen upgrading — Sentry releases frequently.ignored-exceptions-for-type: Correctly preventsDomainException(4xx) from flooding GlitchTip. Architecture decision is sound — domain exceptions are user-generated conditions, not system failures.👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Blockers
None — the implementation is correct and minimal.
Suggestions
traces-sample-rate: 1.0should be env-configurableSame as Markus's concern. Hardcoding 1.0 means ops can't tune it without a redeploy. One-liner fix:
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: falseis the safe default.docker-compose.yml:${SENTRY_DSN:-}empty-string default keeps the SDK silent without the env var. Placement is correct../mvnw clean package -DskipTestsverified — no classpath conflicts with existing OTel dependencies.🔒 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:-}indocker-compose.yml— the DSN is passed via env var, not hardcoded. The:-default produces an empty string, not a fallback DSN..env.examplehasSENTRY_DSN=(empty) — no real DSN committed to git. ✅DomainException exclusion — correct threat model ✅
ignored-exceptions-for-type: org.raddatz.familienarchiv.exception.DomainExceptioncorrectly 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.0at 100% means request spans are sent.send-default-pii: falsesuppresses 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, addsentry.trace-options.include-query-parameters: falseor 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.
🧪 Sara Holt — QA Engineer & Test Automation Specialist
Verdict: ⚠️ Approved with concerns
Blockers
None — pure configuration, no testable logic at the unit layer.
Concerns
DomainExceptionfilter is not regression-testedThe most important behavioral guarantee of this PR — that
DomainExceptiondoes NOT produce a GlitchTip event — has no automated test. If someone later renamesDomainException, 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):
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_DSNis empty, application startup log containsSentry 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@SpringBootTestwould confirm the autoconfiguration is wired correctly.What I verified:
-DskipTestsper PR description ✅ignored-exceptions-for-typesyntax is correct YAML ✅${SENTRY_DSN:}(empty string default) is the standard Spring EL pattern for optional env vars ✅🛠️ Tobias — DevOps Engineer
Verdict: ⚠️ Approved with concerns
Blockers
traces-sample-rate: 1.0— ops cannot tune without redeployAt 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:
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:-}indocker-compose.yml— empty-string default keeps backend healthy with no GlitchTip configured ✅8.5.0— no floating tags ✅docker-compose.ymlexplains the Tempo OTLP non-fatal pattern — same pattern applies here (Sentry non-fatal when DSN absent) ✅📋 Elena Richter — Requirements Engineer
Verdict: ✅ Approved
Spec compliance check vs. issue #580
sentry-spring-boot-starter-jakarta8.5.0 inpom.xmlsentry.dsn: ${SENTRY_DSN:}inapplication.yamlsentry.environmentfromSPRING_PROFILES_ACTIVEsentry.send-default-pii: falsesentry.enable-tracing: trueignored-exceptions-for-type: DomainExceptionSENTRY_DSN: ${SENTRY_DSN:-}indocker-compose.ymlSENTRY_DSN=in.env.example./mvnw clean packagesucceedsAC gaps (deferred to staging)
DomainExceptionnon-reporting verification — staging-only, noted in test planAll spec requirements are satisfied. The test plan correctly separates compile-time verification from runtime GlitchTip integration.
🎨 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.
Blocker resolved in commit
5ee59ae9.traces-sample-rateis now env-configurable viaSENTRY_TRACES_SAMPLE_RATE(defaults to1.0if 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 serviceenvironment:block.env.example—SENTRY_TRACES_SAMPLE_RATE=stub added afterSENTRY_DSN=Build verified with
./mvnw clean package -DskipTests→ BUILD SUCCESS.5ee59ae9f7to68e4ff4121👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Blockers
No tests for
SentryConfig.init()— this is a TDD violation. The@PostConstructmethod 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'smockStatic. Two options:Sentry.init()in a package-private method and override it in tests.SentryInitializerinterface with a no-op test double.Without this, we have a
@Configurationclass with zero test coverage that gates our entire error reporting path.No test for
handleGeneric()Sentry capture (GlobalExceptionHandler.java:67) — theSentry.captureException(ex)call was added to the catch-all handler but there is no test verifying it fires. At minimum a@WebMvcTestslice that triggers a 500 and asserts Sentry was called (viamockStatic(Sentry.class)) would cover this.Suggestions
SentryAutoConfigurationexclusion is not asserted — the comment inapplication-test.yamlexplains 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 noSentryAutoConfigurationbeans are present would prevent silent regressions.The
application-test.yamlcomment is excellent ("Sentry is wired manually via SentryConfig instead. See #580.") — links the workaround back to the issue for future readers.🏛️ Markus Keller — Software Architect
Verdict: ✅ Approved
Findings
Placement is correct.
SentryConfiglives inconfig/— the designated home for infrastructure wiring. No cross-domain coupling was introduced.The workaround is pragmatic and well-documented. Excluding
SentryAutoConfigurationbecause Spring Framework 7 cannot generate a bean name forSentryAutoConfiguration$HubConfiguration$SentrySpanRestClientConfigurationis a legitimate framework-level fix, not a design smell. The comments inapplication.yaml,application-test.yaml, and the class itself all explain the why — future maintainers will not be confused when they encounterspring.autoconfigure.exclude.Layering is respected.
GlobalExceptionHandleris the correct integration point forSentry.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, deleteSentryConfig, keep thesentry:block inapplication.yaml. That path requires no architectural refactoring — the design supports it cleanly.🔐 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 withoutSENTRY_DSNset, Sentry is simply not initialised — no silent partial init that could leak data.DomainExceptionexcluded from capture — correct.DomainExceptioncovers 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 inhandleGeneric(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_RATEin docker-compose defaults to1.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.🚀 Tobias Wendt — DevOps Engineer
Verdict: ⚠️ Approved with concerns
Blockers
SENTRY_DSNis missing from.env.example—SENTRY_TRACES_SAMPLE_RATEwas added butSENTRY_DSNwas not. Any operator setting up a new deployment will see no hint thatSENTRY_DSNneeds to be set..env.exampleis 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 to0.1(10%) and letting the operator opt up.management.server.port: 0in test config (application-test.yaml:33) — this is a genuinely good operational fix. The TIME_WAIT problem it solves (@DirtiesContextwith a fixed management port causing 17+ minute test suite delays) is well-documented with a comment. No concerns here.otel.sdk.disabled: truein 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_DSNis an opt-in production env var, not a CI requirement. Clean.🧪 Sara Holt — QA Engineer / Tester
Verdict: 🚫 Changes requested
Blockers
Zero test coverage for
SentryConfig— the class has two observable behaviours:Sentry.init()must NOT be calledSentry.init()must be called with the configured options (DSN, environment, sample rate,sendDefaultPii=false,DomainExceptionignored)Neither branch is tested.
Sentry.init()is a static method, but Mockito'smockStatic(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@WebMvcTestslice test that throws an arbitraryRuntimeExceptionthrough a controller and verifiesSentry.captureException()was called would close this gap. Currently there is no assertion that the Sentry capture actually fires.Suggestions
Regression guard for
SentryAutoConfigurationexclusion — the exclusion inapplication-test.yamlis load-bearing: without it, integration tests fail with a bean-name-generation error. A test that assertsSentryAutoConfigurationis not in theApplicationContextwould catch an accidental removal. This can be a trivial@SpringBootTestslice:management.server.port: 0fix (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.📋 Elicit — Requirements Engineer
Verdict: ⚠️ Approved with concerns
Requirements Traceability (Issue #580)
Checking stated acceptance criteria against the implementation:
sentry-spring-boot-starter-jakartadependencypom.xml— version 8.5.0SENTRY_DSNapplication.yaml,docker-compose.ymlSPRING_PROFILES_ACTIVEsentry.environment: ${SPRING_PROFILES_ACTIVE:dev}send-default-pii: falseapplication.yaml:133DomainExceptionfrom Sentry captureSentryConfig.java:36+application.yaml:136GlobalExceptionHandler.java:67SentryConfigSentryConfig.init()Concerns
SENTRY_DSNabsent 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_RATEdefaults to1.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.🎨 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.
Reviewer concerns addressed — updated push
7c2e75faFelix / Sara blockers: test coverage for Sentry wiring
Added in commits
fa191b5cand20edc047:GlobalExceptionHandlerTest— verifiesSentry.captureException(ex)is called and 500 is returnedApplicationContextTest— assertsSentry.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.0does not support Spring Boot 4.0. Sentry added a dedicatedsentry-spring-boot-4module 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.0SentryConfig.java: the manualSentry.init()workaround is no longer needed — the new module's auto-configuration handles initialization correctly viasentry.*propertiesspring.autoconfigure.excludefrom bothapplication.yamlandapplication-test.yaml— the new module'sSentryAutoConfiguration(packageio.sentry.spring.boot4) does not crash under SF7SentryConfigTest.java: tested the now-deleted workaround classThe
Sentry.captureException()call inGlobalExceptionHandler.handleGeneric()and allsentry:properties inapplication.yamlare unchanged and fully compatible with the new module.