Refresh the Flutter snapshot: v3 tickets V3-04 through V3-16, plus a fresh installable APK
V3-04 through V3-07, V3-10, V3-16 shipped complete; V3-11/V3-12/V3-14 shipped code-complete pending device/account verification; V3-08/V3-09 deferred behind a new V3-17 (self-hosted OSRM investigation). 316 tests passing, up from 221. The APK is a fresh release build (debug-signed, no release signing config exists yet) with two build fixes applied: core library desugaring enabled for flutter_local_notifications, and sentry_flutter bumped to 9.27.0 (8.14.2's bundled Kotlin plugin was incompatible with this project's Kotlin 2.4.0 toolchain).
This commit is contained in:
100
rippr-flutter-src/docs/v3/V3-12-crash-reporting.md
Normal file
100
rippr-flutter-src/docs/v3/V3-12-crash-reporting.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# V3-12 — Crash reporting
|
||||
|
||||
**Phase** Quality · **Depends on** nothing · **Size** S · **Status** Partially done
|
||||
|
||||
## Goal
|
||||
Know when the app dies mid-ride.
|
||||
|
||||
## Context
|
||||
There is none. A recorder that crashes during a ride currently leaves no trace beyond
|
||||
logcat, which nobody reads — and the failure mode that matters most (recording stopping
|
||||
silently) is exactly the one the user cannot report usefully.
|
||||
|
||||
Becomes important the moment anyone who is not Dylan uses it.
|
||||
|
||||
## Design
|
||||
Sentry or Firebase Crashlytics. **Sentry is the better fit**: it is not tied to Google
|
||||
services, works identically on both platforms, and its free tier is ample here.
|
||||
|
||||
**A crash reporter in a location app is a privacy surface.** Configure it deliberately:
|
||||
|
||||
- No location data in breadcrumbs or context, ever
|
||||
- No device id, no ride contents
|
||||
- Explicit opt-out in settings, and disclosed in the privacy policy
|
||||
- Debug builds report nowhere
|
||||
|
||||
Beyond crashes, one custom event is worth having: **recording ended unexpectedly** — the
|
||||
engine stopping without a user stop. That is the failure the app exists to avoid.
|
||||
|
||||
## Implementation
|
||||
1. Add `sentry_flutter`, initialised in `main()` behind a config flag
|
||||
2. Scrub: no coordinates, no ids, no trip contents in any payload
|
||||
3. Breadcrumbs for lifecycle transitions only
|
||||
4. A custom event when a recording ends without a user action
|
||||
5. Settings toggle, defaulting **off** until a privacy policy exists
|
||||
|
||||
## Acceptance criteria
|
||||
- [ ] A forced crash appears in Sentry from a release build
|
||||
- [ ] No coordinate ever appears in a payload — inspect a real one
|
||||
- [ ] The toggle genuinely disables reporting
|
||||
- [ ] Debug builds send nothing
|
||||
|
||||
## Tests
|
||||
- The scrubber strips coordinates from a representative payload
|
||||
- Reporting disabled means the client is never initialised
|
||||
- Manual: force a crash in a release build and check it lands
|
||||
|
||||
## Risks
|
||||
Leaking location through breadcrumbs or a stack frame's captured state. Inspect a real
|
||||
payload rather than assuming the scrubber works.
|
||||
|
||||
## Out of scope
|
||||
Analytics or usage tracking. Different purpose, different consent.
|
||||
|
||||
## Outcome
|
||||
The code and its guarantees are done; the two device/account-dependent acceptance
|
||||
criteria are not, and can't be from here.
|
||||
|
||||
`shouldInitializeCrashReporting({enabled, isDebug, dsn})` pulls the entire "talk to Sentry
|
||||
at all" decision out as a pure function — every combination of the user's toggle, debug
|
||||
vs. release, and a configured DSN is asserted directly, rather than trusted to however
|
||||
`main()` happens to wire things. `scrubExtra` strips any key matching a coordinate,
|
||||
altitude, device-id, or trip-id fragment (case-insensitive, substring match, so `lat`,
|
||||
`latitude`, `startLat`, and `gps.lon` are all caught without enumerating every call site
|
||||
that might one day capture one) from both event `extra` and every breadcrumb's `data`,
|
||||
wired in as `beforeSend`/`beforeBreadcrumb`.
|
||||
|
||||
`Config.crashReportingEnabled` defaults to false and is surfaced in Settings, same shape
|
||||
as every other toggle in this file. The DSN itself is **not** a user preference — it's a
|
||||
compile-time `--dart-define=SENTRY_DSN=...` value, since it names which Sentry project
|
||||
receives reports, not a fact about the rider. `main()` loads `Config` once, early, purely
|
||||
to make the init-or-not decision before `runApp` (since `SentryFlutter.init` wraps the
|
||||
app itself); the widget tree still loads its own `Config` in `RipprApp.initState` as
|
||||
before, since `SharedPreferences` is memory-cached after the first read.
|
||||
|
||||
The one custom event: `RecordingEngine` gained an optional `onUnexpectedStop(String
|
||||
reason)` callback, injected the same way `uploadPending` already is, so the engine keeps
|
||||
no opinion about where a report goes. It fires exactly once, in
|
||||
`restoreAfterProcessDeath`, when a trip is found still `recording` at launch — the
|
||||
process died without anyone calling `stop()`, which is precisely "the recording stopped
|
||||
and nobody chose that." A cleanly-stopped ride reports nothing; verified by both cases in
|
||||
`recording_engine_test.dart`.
|
||||
|
||||
**Not done, and not attempted:** wiring a real Sentry DSN, forcing a real crash in a
|
||||
release build, and inspecting a real payload for leaked coordinates. All three are the
|
||||
ticket's own actual acceptance criteria, and all three need a real Sentry account and a
|
||||
release build this environment cannot produce. `shouldInitializeCrashReporting` and
|
||||
`scrubExtra` are unit-tested as thoroughly as pure functions can be, but a passing unit
|
||||
test is not the same claim as "inspected a real payload," which the ticket's own Risks
|
||||
section insists on by name. This should be revisited once Dylan has a Sentry project to
|
||||
point the DSN at.
|
||||
|
||||
19 new tests: 9 for `shouldInitializeCrashReporting`/`scrubExtra` (including a
|
||||
representative end-to-end event with coordinates in both `extra` and a breadcrumb),
|
||||
2 in `recording_engine_test.dart` (unexpected-stop fires on a crash, stays silent on a
|
||||
clean stop), 2 in `config_test.dart`, 1 in `settings_screen_test.dart`. Fixed the same
|
||||
viewport-culling test brittleness this section's addition exposed a second time (V3-05
|
||||
first triggered it): the Sync section's fields dropped out of the default test viewport
|
||||
entirely, not just out of hit-test range, so four upload-endpoint tests needed a shared
|
||||
`scrollToSync` helper alongside the device-id test's existing one. `flutter analyze`
|
||||
clean; full suite green (284 tests, up from 269).
|
||||
Reference in New Issue
Block a user