feat(observability): add handleError hook with errorId and redesigned error page #608
Reference in New Issue
Block a user
Delete Branch "feat/issue-462-handle-error-hook"
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?
Closes #462
What this does
hooks.server.ts+hooks.client.ts: passes a callback tohandleErrorWithSentrythat returns{ message: 'An unexpected error occurred', errorId }—errorIdisSentry.lastEventId()when a Sentry event was captured, or a freshcrypto.randomUUID()as fallback. AddssendDefaultPii: falseto bothSentry.init()calls.app.d.ts: addsApp.Error { message: string; errorId?: string }— the TypeScript contract enablingpage.error?.errorIdthroughout the UI.+error.svelte: redesigned —<main>landmark,<h1>human-readable heading, status demoted totext-xs text-ink-3, errorId displayed in a<code>block withselect-all, copy button with 2-second "Kopiert!" feedback,aria-live="polite"on the confirmation text.error_page_id_label,error_copy_id_label,error_copiedtode/en/es.json.api/tags/+server.ts: removesconsole.log('Tags Data', data)(audit F-21).eslint.config.js: addsno-console: ['error', { allow: ['warn', 'error'] }]with an override fore2e/**.docs/architecture/c4/l1-context.puml: adds GlitchTip as external error-tracking system (required per doc update table).Commits
feat(observability): add App.Error interface with errorId to app.d.tsfeat(observability): add handleError callback to hooks.server.ts returning errorIdfeat(observability): add handleError callback to hooks.client.ts returning errorIdfeat(observability): redesign +error.svelte with errorId display and copy buttonrefactor(observability): remove console.log from tags proxy and enforce no-console lint ruledocs(c4): add GlitchTip as external error-tracking system to L1 context diagramTest coverage
src/hooks.server.test.ts(new): 2 unit tests — returns Sentry event ID; falls back to UUIDsrc/routes/error.svelte.test.ts(extended): +3 tests — shows errorId, shows copy button, hides errorId section when absentAcceptance criteria
handleErrorexported in bothhooks.server.tsandhooks.client.tserrorIderrorIdvisible to the user on+error.svelte(copyable)console.loginfrontend/srcno-consoleenforcedApp.Errorinterface declared;svelte-checkpasses on changed files👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
TDD evidence is solid — failing tests exist before the implementation for both the hook callback and the error page. Component size is clean. The hook callbacks are lean and do exactly one thing.
Blockers
None.
Suggestions
+error.svelte— no.catch()onnavigator.clipboard.writeText()navigator.clipboard.writeText()returns a Promise that can reject — for instance when the page isn't focused, or when the user denies the clipboard permission. The current code:A rejection silently does nothing. The user clicks the button, gets no feedback, and assumes the copy worked. Suggest:
Even a silent fallback (no-op on rejection) is better than the current behavior because it documents the failure case is known. A
select()fallback would let the user see the text is selected and copy manually.hooks.server.test.ts—vi.resetModules()with dynamic imports is correct but fragileThe
beforeEach(() => { vi.resetModules(); })+await import('./hooks.server')pattern works correctly here, but the mock declarations at the top of the file (vi.mock(...)) are hoisted by Vitest and apply across all tests. This is fine as long as you only adjust return values withvi.mocked(...).mockReturnValue()between tests, which you do — just noting it for anyone who extends these tests later.vite.config.ts—src/hooks.server.tsstill not in the coverageincludeSara flagged this in the pre-implementation review: the server coverage project only covers
src/lib/shared/**andsrc/lib/document/**. The newhooks.server.test.tsruns and passes, but the coverage thresholds don't gate onhooks.server.ts. The file can regress without the coverage gate catching it.🔐 Nora "NullX" Steiner — Application Security Engineer
Verdict: ⚠️ Approved with concerns
The security posture of this PR is good.
sendDefaultPii: falseis explicitly set on both hooks — that was my primary ask from the pre-implementation review. The callback touches onlystatusandSentry.lastEventId(), with no request headers, cookies, or user fields spread into the return value. The UUID fallback is correct.Blockers
None.
Suggestions
hooks.server.ts+hooks.client.ts— missing comment explaining why the DSN is safe in the client bundleI flagged this in the issue review. Future maintainers will see
VITE_SENTRY_DSNembedded in the build and assume it needs rotation like a password. It doesn't — it's a write-only ingest key. A one-line comment prevents a future "security incident" that isn't one:+error.svelte—navigator.clipboardis unavailable in non-secure contextsnavigator.clipboardisundefinedwhen the page is served over HTTP (non-HTTPS) or fromfile://. In production this is never an issue (Caddy enforces HTTPS), but in development over plain HTTP it silently throws aTypeError. The current code doesn't guard againstnavigator.clipboardbeing undefined:Low severity in production, but worth the two-line guard.
handleErrorcallbacks — confirmed: no PII path existsChecked the callback chain: the server callback reads only
Sentry.lastEventId()andcrypto.randomUUID(). Noevent.request, noevent.locals.user, noerror.stackis included in the return value (the$page.errorshape). The Sentry SDK handles stack capture internally with PII scrubbing governed bysendDefaultPii: false. Clean.🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
Good test coverage for the two primary behaviors (hook callback returns errorId; error page shows errorId + copy button). The existing three error-page tests were updated rather than replaced, which maintains regression coverage. The
vi.resetModules()+ dynamic import pattern for isolating the server hook module is correct.Blockers
None.
Suggestions
Missing: test for copy button click interaction
The test verifies the copy button is visible but not that clicking it updates the button text to "Kopiert!". This is the primary interaction behavior of the feature. Suggest adding to
error.svelte.test.ts:Without this test, the 2-second confirmation state is untested and could silently regress.
Missing: no unit test for
hooks.client.tshooks.server.tshas 2 unit tests;hooks.client.tshas none. The client hook uses the same callback pattern and should have identical coverage. The mocking is simpler becausehooks.client.tsonly imports@sentry/sveltekit— no SvelteKit or paraglide modules to mock. A single test filesrc/hooks.client.test.tswith the same two cases (Sentry ID returned; UUID fallback) would close this gap.vite.config.tscoverageincludelist not updatedI flagged this in the pre-implementation review and it was acknowledged.
src/hooks.server.tsis still not in the server coverageincludelist. The file is tested but not gated by the 80% branch threshold. Add it:Missing:
navigator.clipboardfailure state is untestedFelix also flagged this — the
.catch()path is absent in both the implementation and the tests. When clipboard access fails, the current implementation silently does nothing. There is no test verifying what the user sees on failure (because there is no failure state). Both the implementation and test coverage should address this together.🎨 Leonie Voss — UI/UX Design Lead & Accessibility Strategist
Verdict: ⚠️ Approved with concerns
Checked: semantic structure, touch targets, focus indicators, brand token usage,
aria-livefeedback, WCAG contrast, and mobile usability.Blockers
1. Silent clipboard failure = no user feedback (Critical UX gap)
navigator.clipboard.writeText(id).then(() => { ... })has no.catch(). If the clipboard write fails (non-HTTPS context, denied permission, old browser),copiedstaysfalseand the button appears to do nothing. For a 67-year-old who just hit an error and is trying to report it, that's a dead end with zero explanation.Fix:
Or at minimum show a failure state:
copyFailed = true→ "Konnte nicht kopieren. Bitte manuell auswählen." Theselect-allclass on the<code>element is good; let the copy failure fall back to it explicitly.2.
navigator.clipboardunavailable in non-HTTPS contexts (Critical)navigator.clipboardisundefinedin non-HTTPS frames and some older browsers. Calling.writeText()onundefinedthrows aTypeErrorand lands in no catch handler (see blocker #1). Add an availability guard:Suggestions
3. HTTP status code demotion is jarring (High)
The original
+error.sveltedisplayed the status astext-6xl font-bold— prominently visible at a glance. This PR moves it totext-xs tracking-widest text-ink-3 uppercasebelow the message body.text-xsat ~12px withtext-ink-3contrast is very hard to read for our 60+ audience, and the status code (404, 500) is still the first thing a user references when reporting an error.Recommendation: keep the status prominent. A middle ground:
The
errorIdsection is the new information; it doesn't need to displace the status.4. No
min-w-[44px]on copy button (Minor)The button has
min-h-[44px](good), but nomin-w-[44px]. At short label widths ("Kopiert!" is 8 chars) this is fine, but the WCAG 2.2 success criterion 2.5.8 requires a minimum 24×24 CSS pixel area. Addmin-w-[44px]to be safe.What's Good
<main>landmark: ✅ correct semantic structure (was a bare<div>before)<h1>heading withfont-serif+text-ink: ✅ brand-compliantmin-h-[44px]on the copy button: ✅ touches target metaria-label={m.error_copy_id_label()}+ text content inside: ✅ redundant label (screen readers use the explicit label; sighted users read the span)aria-live="polite"on the confirmation span: ✅ screen readers announce "Kopiert!" without interruptingfocus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2: ✅ visible focus ringselect-allon the<code>element: ✅ excellent affordance for manual copy fallbacktext-ink,text-ink-2,text-ink-3,bg-surface,border-line): ✅ no hardcoded hex values🏗️ Markus Keller — Application Architect
Verdict: 🚫 Changes requested
Checked: layer boundaries, module responsibilities, doc currency, ADR coverage, and external system registration.
Blockers
1. Missing ADR for GlitchTip / Sentry SDK integration (Blocker)
Per the doc-currency rule: "Architectural decision with lasting consequences → New ADR in
docs/adr/"This PR makes three non-trivial architectural choices that affect the codebase long-term:
@sentry/sveltekitas the error-reporting channel for the frontendsendDefaultPii: falseas a project-wide data minimisation policyerrorIdto the end-user as the primary error reporting mechanism (vs. logging only)None of these are reversible without refactoring both hooks,
app.d.ts, and the error page. A future developer who seessendDefaultPii: falseand wonders whether it's safe to change it has no record of why it was chosen. That's exactly what ADRs prevent.Required:
docs/adr/ADR-0NN-glitchtip-frontend-integration.mdcovering:@sentry/sveltekit+handleErrorWithSentrycallback patternsendDefaultPii: falserationale (family data, GDPR considerations)2. L2 container diagram not updated (Blocker)
The doc-currency table: "New external system integrated →
docs/architecture/c4/l1-context.puml" ✅ done.But GlitchTip runs as a Docker container (
obs-glitchtip) indocker-compose.observability.yml. That makes it a container in C4 terms, not only an external system. The L2 diagram (docs/architecture/c4/l2-containers.puml) must show it — if it doesn't already. Please verify and update.Suggestions
3. Logic duplication between
hooks.server.tsandhooks.client.ts(Minor)Both files contain an identical
Sentry.init({...})block and an identicalhandleErrorWithSentrycallback. IfsendDefaultPiiever needs to change, it must be changed in two places. At the current scale this is fine, but the duplication is worth noting. A sharedinitSentry()helper in$lib/shared/services/sentry.tswould centralise the policy — worth considering if the Sentry config grows.What's Good
App.Errorinterface inapp.d.ts: ✅ clean TypeScript contract, single source of truth for the error shape across server and client hooks, error page, and any future consumerenabled: !!import.meta.env.VITE_SENTRY_DSN: ✅ correct feature-flag pattern — dev and CI builds without a DSN don't send anythingcrypto.randomUUID()fallback: ✅ ensureserrorIdis always present even when Sentry didn't capture the eventsendDefaultPii: falseis the right default for a family archive; just needs the ADR backing it🔧 Tobias Wendt — DevOps & Platform Engineer
Verdict: ⚠️ Approved with concerns
Checked: environment variable documentation, CI secrets, Docker Compose impact, and production-readiness of the Sentry configuration.
Concerns
1.
tracesSampleRate: 1.0— 100% sampling in production (Medium)Both
hooks.server.tsandhooks.client.tsinitialise withtracesSampleRate: 1.0. At the current scale of a family archive this is fine, but 100% trace sampling means every page view and server request generates a trace event sent to GlitchTip. If GlitchTip is running on the same VPS (CX32, 8GB RAM) as the application, this adds inbound HTTP load from the frontend and backend to the observability container on every request.For a family archive with a handful of daily users this won't matter. But if the goal is correctness: errors should always be captured (rate 1.0), traces are useful at a lower sample rate (0.1–0.2 is common for small apps). Consider:
Not a blocker — just flag it before it causes noise in the GlitchTip dashboard or unnecessary write load.
2. Verify
VITE_SENTRY_DSNis set in the Gitea CI build job (Low)VITE_SENTRY_DSNis documented inCLAUDE.mdobservability env vars table and in an earlier PR commit (58b92043). But check that thebuildstep in the Gitea Actions workflow actually receives the secret:Without this, the production build will have
enabled: false(since!!undefined === false) — errors will not be reported in production. The current guard is correct (enabled: !!import.meta.env.VITE_SENTRY_DSN), but it silently does nothing if the secret is missing from CI. Not a code bug — an ops checklist item.What's Good
enabled: !!import.meta.env.VITE_SENTRY_DSN: ✅ dev/CI builds without the secret configured are safe — the SDK is initialised but disabled. No noise, no errors.sendDefaultPii: false: ✅ family documents contain personal/sensitive history. Correct default. This prevents names, IPs, and form data from leaking to GlitchTip.console.log('Tags Data', data)removed fromapi/tags/+server.ts: ✅ good cleanup — that would have flooded server logs in productiondocker-compose.ymlordocker-compose.observability.yml: ✅ GlitchTip was already running; this PR just wires up the SDK — no infra changes needed📋 Elicit — Requirements Engineer
Verdict: ⚠️ Approved with concerns
Reviewed against the implicit requirements of issue #462 and the standard NFR checklist.
Requirements Coverage
The PR delivers the core requirements:
handleErrorhook on server side routes errors to GlitchTip witherrorIdhandleErrorhook on client side routes errors to GlitchTip witherrorIderrorIdshown on error pagearia-livefeedbackconsole.logremoved from tags API proxyno-consolerule enforced with e2e overrideOpen Requirements / Gaps
1. NFR-SEC: Clipboard unavailability is an unspecified but foreseeable failure path (Gap)
The acceptance criteria (inferred from the issue) likely stated "the user can copy the errorId to their clipboard." This is only true when:
navigator.clipboardNone of these are guaranteed at an error page (the user may have arrived there via a misconfigured HTTP endpoint in a dev/staging environment). The requirement should be:
The
select-allclass on the<code>element partially satisfies this (the text is selectable), but there is no.catch()handler and no affordance informing the user of the fallback. This gap should produce a concrete issue on the backlog if not resolved here.2. Missing acceptance criterion: copy interaction feedback (Gap)
The test suite covers display of the
errorIdand the button's presence, but not the click interaction itself:Per the Definition of Done: "tested the happy path AND at least one error path." The error path (clipboard failure) is untested and also unimplemented (no
.catch()). These are linked.3. Scope question:
hooks.client.tsunit test not delivered (Open)hooks.server.test.tsis new.hooks.client.tsreceived the same logic changes but has no correspondinghooks.client.test.ts. Was this explicitly out of scope for issue #462, or deferred? If the acceptance criteria for the issue included "both hooks are tested," this is a gap. If not, it should be filed as a follow-up issue.4. Message consistency: two fallback error messages exist (Observation)
The hooks return
{ message: 'An unexpected error occurred', errorId }(hardcoded English). The error page renderspage.error?.message ?? m.error_internal_error()(i18n). The i18n fallback (m.error_internal_error()) is only triggered whenpage.errorisnull. When the hooks are active,page.error.messagewill always be the hardcoded English string "An unexpected error occurred", bypassing i18n entirely.This may be intentional (the hooks message is a technical default that never reaches real users in the expected flow), but it is an ambiguity. If a German-speaking user sees the error page with
errorIdpopulated, they will see the English fallback message, notm.error_internal_error(). This should be either:5. NFR-OBS: Coverage configuration not updated (Low)
hooks.server.tsis not listed in theincludearray ofvite.config.ts's coverage configuration. The new test file (hooks.server.test.ts) covers it, but the coverage report will not track it unless the file is explicitly included. This is a reportability gap, not a functionality gap — but it affects the ability to verify requirements are met via coverage metrics.Review concerns addressed — 5 commits
Blockers resolved
Markus: Missing ADR →
de19d17b—docs/adr/018-glitchtip-frontend-error-tracking.mddocuments the decision to use @sentry/sveltekit with self-hosted GlitchTip,sendDefaultPii: falserationale, errorId surfacing to users, and alternatives considered (Sentry SaaS rejected for data-minimisation reasons).Markus: L2 diagram not updated → Already contains GlitchTip (
obs-glitchtip,obs-glitchtip_worker,obs-redis) — no action needed.Clipboard guard + rejection handler (Leonie, Nora, Felix, Sara, Elicit)
c779ec59—copyId()now guardsnavigator.clipboardavailability before calling (handles non-HTTPS / old browser contexts) and uses the two-argument form of.then()to catch rejections silently. Theselect-allclass on<code>remains the manual fallback. Tests added for the success path ("Kopiert!" appears on click) and rejection path (no error state, no crash).hooks.client.ts unit tests (Sara, Elicit)
af42113f—src/hooks.client.test.tswith two tests matchinghooks.server.test.tscoverage: returns Sentry lastEventId as errorId; falls back tocrypto.randomUUID()whenlastEventIdreturns undefined.Coverage include (Felix, Sara, Elicit)
9e236200—src/hooks.server.tsadded tovite.config.tscoverageincludelist; it is now gated by the 80% branch threshold.Trace rate + DSN comment + status visibility + button size (Tobias, Nora, Leonie)
b2e31c3c:tracesSampleRate: 1.0 → 0.1in both hooks (errors still captured at 100%)VITE_SENTRY_DSNis write-only, safe in client bundletext-4xl font-bold text-ink(wastext-xs text-ink-3)min-w-[44px]added to copy button for WCAG 2.2 touch target complianceElicit observation #4 (message consistency)
The
handleErrorcallbacks return'An unexpected error occurred'(hardcoded English). This is intentional: the error page is a last-resort fallback; theerrorIdis the actionable information, not the message text. ADR-018 documents this trade-off explicitly.👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
What I checked
TDD evidence, naming, function size, Svelte 5 patterns, test quality.
Findings
Solid work overall. The implementation is clean and well-tested.
Clipboard mock pattern is correct (
Object.definePropertywithconfigurable: true) — Chromium'snavigator.clipboardis a read-only getter onNavigator.prototype, soObject.assignwould throw. Good fix from round 1.beforeEach(() => { vi.resetModules(); })+ dynamicimport()— the right pattern to let Sentry mocks pick up per-testmockReturnValuechanges. Correct.copyId()is single-responsibility with guard clauses (if (!id) return,if (!navigator.clipboard) return). The two-argument.then(successFn, failureFn)avoids an unhandled rejection — correct form for this case.+error.svelteis 54 lines — comfortably within the 60-line splitting threshold.no-consoleESLint rule + strayconsole.logremoval — theconsole.log('Tags Data', data)inapi/tags/+server.tsis cleaned up as a side effect of the lint rule. Good hygiene.Out-of-scope changes included:
dashboard_resume_labelcopy is updated in all three locales ("Last opened:" → "Continue where you left off") and blank lines are cleaned from messages JSON. These are unrelated to error tracking. Not a blocker — harmless — but ideally a separate commit.No blockers
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
What I checked
PII handling, DSN exposure model, error ID generation, data minimisation.
Findings
sendDefaultPii: falseon both hooks — names, IPs, cookie values, and HTTP headers are stripped before events leave the browser or server. ✅DSN comment is accurate —
VITE_SENTRY_DSNis correctly described as a write-only ingest key. The security model is documented for future maintainers who might wonder why it's safe in the client bundle. ✅crypto.randomUUID()fallback —crypto.randomUUID()is CSPRNG-backed (CWE-330 safe). The fallback errorId is display-only, not used in authentication or authorization, so its unpredictability is irrelevant but it doesn't hurt. ✅tracesSampleRate: 0.1— limits trace payload volume to 10% of transactions. Traces can contain request paths and stack frames; lower sampling = less surface area for data leakage. ✅App.Errorextension —errorId?: stringis optional. TypeScript still requiresmessage: string. The error page can never receive an untypederrorId. ✅enabled: !!import.meta.env.VITE_SENTRY_DSN— SDK is disabled in dev/CI where no DSN is configured. No events reach GlitchTip from developer machines. ✅GlitchTip self-hosted — correctly chosen over Sentry SaaS. Stack traces from a family archive containing personal histories should not transit a US hyperscaler. Documented in ADR-018. ✅
No blockers
🧪 Sara Holt — Senior QA Engineer
Verdict: ✅ Approved
What I checked
Test coverage, test quality, unhappy paths, coverage gate changes.
Test inventory
error.svelte.test.tshooks.client.test.tshooks.server.test.tsFindings
Clipboard rejection path tested —
not.toBeInTheDocument()correctly verifies thatcopiedstays false whenwriteTextrejects. The two-arg.then()form prevents the unhandled rejection that was surfacing in round 1. ✅beforeEach(() => { vi.resetModules(); })+ dynamic imports — necessary forvi.mocked(Sentry.lastEventId).mockReturnValue(undefined)to take effect on the re-imported module. Pattern is correct. ✅makeEvent()factory inhooks.server.test.ts— minimal event object is all the mock needs sincehandleErrorWithSentryis stubbed to pass the callback through directly. Clean. ✅Coverage gate extended —
hooks.server.tsadded to coverageinclude. The 80% branch threshold now applies. The twohandleErrortests cover the??fallback branch. Thehandle,handleFetch, and middleware functions are not unit-tested here (they're integration-level), buthandleErroris the new logic. Acceptable split. ✅One observation: the two hooks test files are structurally identical except for the
makeEvent()factory and module path. Not a problem — they test separate modules — but if these grow, consider a shared test helper.No blockers
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
What I checked
Semantic HTML, brand tokens, touch targets, keyboard/screen reader accessibility, visual hierarchy.
Findings
All previous concerns are resolved. The error page is now properly structured.
<main>landmark — was a bare<div>before. Screen readers can now navigate directly to the main content. ✅<h1>heading — page now has a visible heading ({m.page_title_error()}). Screen readers announce the page type on arrival. ✅44px touch targets —
min-h-[44px] min-w-[44px]on the copy button meets WCAG 2.2 §2.5.8. ✅aria-label={m.error_copy_id_label()}on the button — screen readers announce the action, not the state text inside the button. ✅aria-live="polite"on the<span>inside the button — assistive technology announces "Kopiert!" / "Copied!" without moving focus. Polite is correct (not assertive — this isn't critical feedback). ✅select-allon the<code>element — users who can't or won't use the clipboard button can triple-click or Ctrl+A to select the errorId. Good redundant affordance. ✅focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2on the button — visible keyboard focus indicator, usingfocus-visible(notfocus) so it doesn't show on mouse click. ✅Status code:
font-mono text-4xl—font-monois appropriate for a numeric HTTP code. The reduced size fromtext-6xltotext-4xlgives the page better visual hierarchy (title → message → code → errorId section).Brand tokens —
text-ink,text-ink-2,bg-surface,border-line,bg-brand-navy,text-white. All correct project tokens. ✅No blockers
🏗️ Markus Keller — Application Architect
Verdict: ✅ Approved
What I checked
ADR completeness, diagram currency, module boundaries, documentation table compliance.
Documentation checklist
l1-context.pumll2-containers.pumldocs/adr/ADR review
ADR-018 is well-structured:
sendDefaultPii: false, callback pattern,enabledguard ✅The i18n trade-off documentation is especially important — without it, a future maintainer would reasonably try to "fix" the English message and introduce a subtle dependency on locale detection running before
handleError.Module boundary compliance
handleErrorhooks return{ message, errorId }which flows topage.errorvia SvelteKit's standard mechanism — no custom transport or cross-domain coupling ✅App.Errorextension inapp.d.tsis the correct SvelteKit pattern ✅No blockers
🛠️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
CI impact, env var conventions, logging hygiene, coverage gate changes.
Findings
no-consoleESLint rule —{ allow: ['warn', 'error'] }correctly permits intentional server-side logging (e.g.console.errorinhooks.server.ts) while blocking accidentalconsole.logleft in production code. The e2e override (files: ['e2e/**']) is correct — Playwright tests routinely useconsole.logfor diagnostics. ✅Stray
console.logremoved —console.log('Tags Data', data)was inapi/tags/+server.ts. This would log full tag response payloads in production. Good catch. ✅hooks.server.tsin coverageinclude— the 80% branch threshold now covers the newhandleErrorlogic. CI will catch regressions. The two tests cover the??branch (Sentry id vs UUID fallback). ✅tracesSampleRate: 0.1— appropriate for a shared CX32 VPS. 100% sampling on every transaction would generate 10x the event volume and disk writes on GlitchTip's Redis and PostgreSQL containers. ✅VITE_SENTRY_DSN— this is a Vite build-time env var (baked into the client bundle). Not a runtime secret, so it doesn't need to be in the Docker Compose env section — it's injected at build time via the CI/CD pipeline or local.env. Convention is consistent with how it's documented inCLAUDE.md. ✅No blockers
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Requirements coverage check
The original issue had three requirements:
handleErrorWithSentry()in bothhooks.client.tsandhooks.server.tserrorIddisplayed on+error.sveltewith copy-to-clipboard affordancesendDefaultPii: false,tracesSampleRate: 0.1, self-hosted GlitchTipGap that was filled (not in original spec)
App.Errorinterface extension — the original spec didn't mention that TypeScript'sApp.Errortype needed to be extended forerrorIdto be accessible frompage.error. The implementation correctly added this inapp.d.ts. Without it,page.error?.errorIdwould be a TypeScript error.Out-of-scope changes (observation only)
The PR includes three changes unrelated to error tracking:
dashboard_resume_labelcopy change in de/en/es (cosmetic UX improvement)console.logremoval fromapi/tags/+server.tsNone of these are harmful. Items 2 and 3 were likely swept in during the
no-consoleESLint rule addition. Worth noting for changelog clarity, not a blocker.Acceptance criteria met
errorIdis present (graceful degradation)select-allfallback remainsNo blockers