Skip to content

Add opt-in error reporting via GlitchTip (Sentry-compatible)#109

Merged
krazyjakee merged 2 commits into
masterfrom
claude/vigilant-brown-fk3t8y
Jun 10, 2026
Merged

Add opt-in error reporting via GlitchTip (Sentry-compatible)#109
krazyjakee merged 2 commits into
masterfrom
claude/vigilant-brown-fk3t8y

Conversation

@krazyjakee

Copy link
Copy Markdown
Contributor

Summary

Implements anonymous, opt-in error reporting to a self-hosted GlitchTip instance, mirroring the reference Godot client's error reporting feature. Nothing is sent until the user explicitly consents via a first-launch dialog or the Settings toggle. All personally identifiable information (tokens, URLs, message content) is scrubbed before transmission.

Key Changes

  • GlitchTipClient (lib/features/error_reporting/repositories/glitchtip_client.dart): Pure Dart HTTP client for the Sentry-compatible store API. Handles DSN parsing, event building, breadcrumb management, and stack frame parsing. Deliberately avoids native SDKs for cross-platform consistency (web, mobile, desktop).

  • ErrorReportingController (lib/features/error_reporting/controllers/error_reporting.dart): Riverpod-based state management for error reporting. Wraps global FlutterError.onError and PlatformDispatcher.onError hooks once, capturing unhandled exceptions and async errors. Provides breadcrumb recording for navigation, messaging, and voice events. All text is scrubbed via scrubPiiText() before leaving the device.

  • PII Scrubbing (scrubPiiText): Redacts Bearer tokens, token= query parameters, dk_ hex tokens, bare 64-char hex strings, and URLs with explicit ports — matching the reference client's patterns.

  • Settings Integration: Added errorReportingEnabled and errorReportingConsentShown flags to AccordSettings. First-launch consent dialog shown in main.dart (never reshown once answered). Settings screen includes toggle and "Report a Problem" dialog for user-initiated feedback.

  • Release Build Integration (.github/workflows/release.yml): Injects SENTRY_DSN secret at build time via --dart-define. Falls back to in-repo dev DSN when unset, mirroring the reference client's project.godot setup.

  • Comprehensive Tests (test/features/error_reporting/): Unit tests for DSN parsing, PII scrubbing, event sending, breadcrumb capping, and settings round-trip serialization.

Implementation Details

  • No-op until consent: All guarded methods (addBreadcrumb, captureMessage, captureError, reportProblem) return immediately if reporting is disabled, so toggling consent requires no re-initialization.

  • Global hooks installed once: FlutterError.onError and PlatformDispatcher.onError are wrapped exactly once and chain to any previously installed handlers (e.g., LiveKit noise filters in main.dart).

  • Breadcrumb ring buffer: Capped at 100 entries; oldest are dropped when exceeded. Attached to every subsequent event.

  • Stack frame parsing: Lenient regex-based parser (matching the reference client) handles both well-formed Dart frames and anomalies like <asynchronous suspension>.

  • Event ID tracking: UUID v4 (32 hex chars, no dashes) generated per event; lastEventId exposed for correlation in the UI.

  • Transport resilience: HTTP errors never throw back into the app; all exceptions are silently caught.

https://claude.ai/code/session_01BWiFwfBbVEzQr9Tdgq33F6

claude added 2 commits June 10, 2026 23:17
Port the legacy-godot client's error reporting stack to Flutter:

- GlitchTipClient: pure Dart Sentry store-API client (DSN parsing,
  tags, breadcrumb ring buffer, message/error capture), like the
  reference's glitchtip_client.gd — no native SDK dependency.
- ErrorReportingController: consent-gated Riverpod service that wires
  PII scrubbing (tokens, token= params, URLs with ports), global
  FlutterError/PlatformDispatcher capture hooks, context tags
  (server count, truncated space/channel ids), and breadcrumbs for
  navigation, space/channel switches, and message sends.
- First-launch consent dialog (asked once, stamped in settings),
  settings toggle, and a "Report a problem" feedback dialog that
  sends a type=user-feedback tagged event.
- Settings model/controller: errorReportingEnabled +
  errorReportingConsentShown, off by default; nothing is sent
  without consent.
- release.yml injects the SENTRY_DSN secret at build time; the dev
  DSN stays in-repo for local testing, as before.
- Tests: DSN parsing, event payloads, breadcrumb cap, PII scrubbing
  (ported from test_error_reporting.gd), consent gating.

https://claude.ai/code/session_01BWiFwfBbVEzQr9Tdgq33F6
- reportProblem permanently stamped type=user-feedback on the client;
  all subsequent auto-captured errors inherited it. Fixed with a
  try/finally that calls client.removeTag('type') after each send.
- GlitchTipClient._http was never closed; toggling consent on/off
  leaked a connection pool each time. Added close() and call it from
  ref.onDispose alongside the _active nulling.
- Random() replaced with Random.secure() for UUID v4 event IDs.
- while loop for breadcrumb capping replaced with if (one added at a
  time, so while never loops more than once).
- Added test for removeTag to prevent regression.

https://claude.ai/code/session_011DSV18kWfoLYNyUgStp9JP

Copy link
Copy Markdown
Contributor Author

Code Review — PR #109 (opt-in error reporting)

Fixes pushed in commit cde6643 on this branch. Findings ranked by severity:


🔴 HIGH — Tag pollution bug in reportProblem

File: lib/features/error_reporting/controllers/error_reporting.dart

reportProblem called client.setTag('type', 'user-feedback') but never removed it. Every auto-captured Flutter/async error after the first user-submitted report would also be tagged type=user-feedback in GlitchTip, making automatic crashes look like user feedback and corrupting dashboards.

Fix: Wrapped the captureMessage call in a try/finally that calls client.removeTag('type'). Added GlitchTipClient.removeTag(String key) and a regression test.


🟠 MEDIUM — http.Client connection-pool leak

File: lib/features/error_reporting/repositories/glitchtip_client.dart

GlitchTipClient creates (or accepts) an http.Client but had no close() method, and the controller never closed it on dispose. Toggling error reporting off then on (e.g., visiting Settings twice) created a new GlitchTipClient each time while the old HTTP connection pool was silently leaked.

Fix: Added void close() => _http.close() to GlitchTipClient and called it from ref.onDispose in the controller alongside the existing _active = null cleanup.


🟠 MEDIUM — Non-cryptographic Random for UUID event IDs

File: lib/features/error_reporting/repositories/glitchtip_client.dart

_generateEventId() used Random() (a seeded PRNG) rather than Random.secure(). With a weak seed the 32-hex-char event IDs can collide, breaking GlitchTip's deduplication and event correlation (it uses event_id as a primary key).

Fix: Changed final Random _random = Random() to Random.secure().


🟡 LOW — while loop for breadcrumb capping

File: lib/features/error_reporting/repositories/glitchtip_client.dart

addBreadcrumb used while (_breadcrumbs.length > maxBreadcrumbs) to trim the list after each insertion. Since exactly one item is added per call, the loop body executes at most once — a while disguised as an if, which surprises future readers.

Fix: Changed to if.


✅ No action needed

  • File sizes — all new files are well under 1000 lines (201 / 175 / 149 / 136).
  • Test coverage — existing tests cover DSN parsing, PII scrubbing, breadcrumb cap, transport resilience, settings round-trip, and controller lifecycle. The regression test for removeTag has been added.
  • No 3D/visual changes — not applicable to this PR.
  • No Discord/Firebase regressions — the feature is self-contained in lib/features/error_reporting/ with no references to Discord APIs.
  • PII scrubbing — patterns match the reference client; Bearer, token=, dk_*, 64-char hex, and port-qualified URLs are all redacted before leaving the device.
  • Consent model — first-launch dialog, settings toggle, and markErrorReportingConsentShown separation all look correct.
  • Global hook install-once logic_hooksInstalled guard and identical(_active, created) check in onDispose correctly prevent double-wrap and stale-client capture.

Generated by Claude Code

@krazyjakee krazyjakee merged commit 2c27471 into master Jun 10, 2026
1 check failed
@krazyjakee krazyjakee deleted the claude/vigilant-brown-fk3t8y branch June 10, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants