Skip to content

feat(crash-reporting): opt-in crash reporting behind a build-time SENTRY_DSN gate#878

Open
TaprootFreak wants to merge 3 commits into
stagingfrom
feat/crash-reporting-dsn-gate
Open

feat(crash-reporting): opt-in crash reporting behind a build-time SENTRY_DSN gate#878
TaprootFreak wants to merge 3 commits into
stagingfrom
feat/crash-reporting-dsn-gate

Conversation

@TaprootFreak

@TaprootFreak TaprootFreak commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

Two customer cases this week reached support with zero diagnostic evidence: a BitBox02 USB connect crash/hang on Android and a registration-signing blocker (both 23.07.). Server-side request tracing covers everything that reaches the API — but a client-side crash before any request is invisible. The app has no crash reporter; every error thrown from a Future, Stream or Timer callback vanishes without trace (#866 documents this gap explicitly, and installErrorHandlers() was designed as the hook a reporter plugs into).

Change

  • sentry_flutter behind a compile-time gate. initCrashReporting() (new, lib/setup/error_handling/crash_reporting.dart) reads --dart-define=SENTRY_DSN. Without an injected DSN — i.e. in every local, test and current CI build — the SDK never starts and produces no network traffic. The app behaves exactly as before this PR.
  • Best-effort by contract: a malformed DSN or a failing native binding is logged and swallowed inside initCrashReporting() — reporting infrastructure can never keep the wallet from starting. Covered by a dedicated test.
  • Wiring order is load-bearing: main() calls initCrashReporting() after installErrorHandlers(), because installErrorHandlers() overwrites PlatformDispatcher.onError without chaining — the reverse order would silently drop the SDK's async-error hook. The SDK itself chains both handlers it wraps (FlutterError.onError: capture, then delegate; PlatformDispatcher.onError: delegate, then capture). In release mode the native splash is now preserved before this first await, so the added async gap cannot flash the splash to blank.
  • Pinned option surface — the guarantee is exactly this list: sendDefaultPii=false, attachScreenshot=false, enableAutoSessionTracking=false, tracesSampleRate=null — error events only, no session telemetry. Native crash handling and ANR detection deliberately stay on their SDK defaults: they produce precisely the error events this reporter exists for. View-hierarchy attachment stays off by SDK default; its option is experimental and deliberately not referenced. Environment defaults to production, overridable via --dart-define=SENTRY_ENVIRONMENT.
  • CONTRIBUTING § API Access: adds the one scoped exception for first-party crash reporting, and declares any widening of the reported data (breadcrumbs with request URLs, user context, attachments) a review-blocking change.

Tests

  • test/setup/error_handling/crash_reporting_test.dart: DSN gate (no-op without DSN, exactly one init with DSN), the full pinned option set, and the swallow-on-failure contract, via the injectable CrashReporterInit — no platform channels involved.
  • // @no-integration-test annotation on initCrashReporting per CONTRIBUTING: the native SDK only starts in builds that inject a DSN, which no test build does.

Open points before this becomes effective

  1. Release pipeline: the release workflow must inject SENTRY_DSN (repo/environment secret + --dart-define) — deliberately a separate PR, so this one stays inert and fully reviewable on its own.
  2. Native release build check: PR CI runs analyze + tests; the first tagged build after the pipeline change should be smoke-checked once (Android Gradle / iOS pods pull in the native SDK parts).
  3. Follow-up (separate PR): attach BitBox device context (product, firmware version) and connect-flow breadcrumbs after connect, so hardware-related crashes carry the device facts that today require asking the customer.

@TaprootFreak

Copy link
Copy Markdown
Contributor Author

3 review passes until zero findings.

Pass 1 fixes: made the reporter init best-effort (a failing or misconfigured reporter can no longer block app startup — with a dedicated test), moved the release-mode splash preserve ahead of the first await, corrected the handler-chaining description to match the pinned SDK version's actual behavior, additionally pinned session tracking off and narrowed the pinning claim to the exact option list, fixed the test-file import order, and removed an internal record reference from the PR description.

Pass 2 fix: restored installErrorHandlers() as the very first bootstrap step so its documented guarantee holds while still leaving no async gap before the splash preserve.

Pass 3: clean.

@TaprootFreak
TaprootFreak marked this pull request as ready for review July 24, 2026 16:11
TaprootFreak added a commit that referenced this pull request Jul 24, 2026
## Summary

- inject the repository `SENTRY_DSN` into Android and iOS store builds
with an explicit `production`/`internal` environment
- keep compile-time values out of Fastlane process arguments via a
mode-0600 temporary define file and fail release builds when the DSN is
missing
- upload Android split debug info, Dart obfuscation maps, and iOS
archive symbols to the self-hosted Sentry project
- identify uploads with the native release name
`swiss.realunit.app@<version>+<build>` and matching distribution

The Sentry project `realunitchapp` and the repository secrets
`SENTRY_DSN` and `SENTRY_AUTH_TOKEN` have been provisioned. Runtime
reporting activates together with #878. Related to DFXServer/server#958.

## Validation

- Ruby syntax checks for both Fastfiles
- workflow YAML parse and `git diff --check`
- `flutter analyze --no-fatal-infos`
- non-golden Flutter suite: 4,687 tests passed; the 16 subprocess cases
initially failed only because local `dart` was absent from PATH and all
16 passed when rerun with the Flutter SDK path
- DFX PR precheck: 3 rounds, final conformity and logic reviews both at
0 findings

## Follow-up verification

The first tagged native release should confirm the uploaded debug files
in Sentry and symbolicate one controlled test event.

---------

Co-authored-by: Daniel Padrino <danswarrior1@gmail.com>
…TRY_DSN gate

Customer-facing crashes currently leave no diagnostic trace: errors from
Future/Stream/Timer callbacks vanish, and support cases arrive with zero
evidence (#866 documents the gap). This wires the crash reporter into the
existing error-handler hook.

Without --dart-define=SENTRY_DSN (all local, test and current CI builds) the
SDK never starts and produces no network traffic. The option set is pinned to
error events only: no PII, no screenshots, no view hierarchy, no tracing.
initCrashReporting() runs after installErrorHandlers() on purpose - the SDK
chains the handlers installed there, the reverse order would drop its async
hook. CONTRIBUTING gains the one scoped API-access exception for first-party
crash reporting; widening the reported data is declared review-blocking.

The release pipeline change that injects the DSN is a deliberate follow-up so
this change stays inert and reviewable on its own.
…pinning

Review pass fixes:

- initCrashReporting is best-effort now: a malformed DSN or failing native
  binding is logged and swallowed instead of aborting app startup as an
  unhandled error before runApp.
- Preserve the native splash before the first await in main() so the new
  async gap cannot flash the splash to blank in release builds.
- Correct the handler-chaining doc: the SDK's PlatformDispatcher.onError
  hook delegates first and captures second (FlutterError.onError is the
  capture-first one); the ordering requirement itself is unchanged.
- Narrow the pinning claim to the exact option list and additionally pin
  enableAutoSessionTracking=false - error events only, no session pings.
- Test-file import order per CONTRIBUTING (package:other before
  package:realunit_wallet).
…erve again

installErrorHandlers() is fully synchronous, so running it ahead of the
release-mode splash preserve keeps the documented guarantee in
error_handlers.dart ('called before any other bootstrap step') true while
still leaving no async gap before FlutterNativeSplash.preserve().
@TaprootFreak
TaprootFreak force-pushed the feat/crash-reporting-dsn-gate branch from 9653f93 to 74cbb18 Compare July 24, 2026 19:53
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.

1 participant