Crash reporting

day-piece-break saves crash reports on the device. On the next launch, the app can show the report and offer to send it by HTTP, GitHub issue, or email. Reports are sent only through an explicit user action. The steps below show how to capture a crash, display the report after restarting, and let the user decide whether to send it.

Works on: Rust panics are captured on every native target. Native faults (SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT, SIGTRAP) are caught on the Unix targets (macOS, iOS, Linux, Android, and HarmonyOS), and Android also records uncaught Java exceptions. Windows records panics but not native faults yet, and on the web init is a no-op. The full matrix is in the break reference.

1. Initialize reporting before launch

Add the crate and call init as early as possible: before day::launch, so a crash during startup is still recorded:

[dependencies]
day-piece-break = { git = "https://github.com/daybrite/day-piece-break" }

The showcase wraps it in a helper called from every entry point:

/// Arm crash reporting. Idempotent (day-piece-break's `init` is single-shot); safe to call from
/// every entry point.
pub fn install_crash_reporting() {
    let _ = day_piece_break::Config::new()
        // "Send report" opens a prefilled email to the developer (no server needed).
        .reporter(day_piece_break::EmailReporter::new("crashdemo@daybrite.dev"))
        .init();
}

Crash capture is process-global, so init is single-shot: a second call returns InitError::AlreadyInitialized. That’s why the helper ignores the result: calling it from both main and a mobile entry point is safe.

The configuration also controls which reports are kept and how they are redacted: .max_reports(n) caps the report rotation (5), .keep_contained(false) drops reports for panics day-core contained (kept by default), .signals(false) turns off the native signal handlers (on by default), and .redact(|msg| …) scrubs secrets from panic messages before they are persisted, displayed, or uploaded. App identity (id, version, build) is baked in by day build from Day.toml; .app_id(…), .app_version(…), and .app_build(…) override it.

2. What gets captured

Reports come from a Rust panic (the panic hook), a native fault or abort (the signal handlers), and, on Android, an uncaught Java exception. A panic that day-core contains at its trampoline boundaries (the app survives) is recorded too, as a distinct non-fatal report, so you can investigate failures that did not terminate the app.

A report is versioned JSON: app id, version, and build; the day version and backend; OS, device model, and locale; the session id and uptime; the panic message and source location, or the signal’s number and addresses; and a backtrace. The reference lists every field. Panic messages may contain user data supplied by your app; use .redact(…) to remove sensitive values before storage. The signal handlers chain to the previous disposition, so the platform’s crash reporter (Android tombstones, HarmonyOS faultlogs) still runs alongside.

3. Show the report on the next launch

init reconciles the previous session before your UI mounts, so by the time it builds you can ask what happened:

match day_piece_break::last_session() {
    day_piece_break::SessionEnd::Crashed { .. } => show_crash_prompt(), // your UI, or consent_banner()
    day_piece_break::SessionEnd::Unknown => {} // an OS kill — not a crash; usually ignore
    day_piece_break::SessionEnd::Clean => {}
}

Use day_piece_break::consent_banner() from the ui feature (on by default): a piece that appears while reports are pending, shows the full report text, and offers send and discard. To build your own (the showcase’s Crash Reporting page does), compose the queries: pending() is a reactive Signal<Vec<ReportMeta>>, newest first; report_text(&meta) is the full text the transport sends; reporter_description() is the transport’s one-line disclosure; send(&meta, |result| …) uploads; discard(&meta) deletes. The showcase keeps its viewer current with one effect:

let report = Signal::new(String::new());
let pending = day_piece_break::pending();
Effect::new(move || {
    pending.get(); // track
    report.set(day_piece_break::latest_report_text().unwrap_or_default());
});

4. Pick a reporter

Three transports ship with the crate:

  • RestReporter::new(url) POSTs the report JSON via day-part-http, off the UI thread; .named("our crash server") sets the name shown to the user. It’s also the shape a GitHub-issue proxy takes: a small server accepts the JSON and opens the issue with your repo token held server-side, never on the device.
  • GithubIssueReporter::new(owner, repo) opens a prefilled new-issue page in the browser; the user reviews and submits it themselves, and GitHub is the only server involved.
  • EmailReporter::new(to) opens a prefilled mailto: compose (.subject_prefix(…) tags the subject); the user sends the mail.

Or implement the trait yourself:

pub trait Reporter: Send + Sync {
    fn name(&self) -> &str;          // shown on the consent surface
    fn describe(&self) -> String;    // one-line disclosure: where the report goes
    fn send(&self, report: &Report, done: Box<dyn FnOnce(Result<(), SendError>) + Send>);
}

The browser and email transports finish with SendError::HandedOff: they handed the report to the platform and can’t confirm delivery. It is reported so your UI can say so, and it is not a failure.

Pitfalls

  • Arm first. The hook can’t record a crash that happens before init runs, and init also reconciles the previous session, so call it at the top of the app entry, before day::launch.
  • Release backtraces carry symbols, not lines. The release profile ships no debug info by default. For file:line in release reports, add [profile.release] debug = "line-tables-only" in your own workspace; day-piece-break doesn’t change the global profile. For native faults, signal.pc - signal.slide is the module-relative address to symbolize offline.
  • Unknown is not a crash. A leftover session with no crash artifact (an OS kill, power loss) reconciles as SessionEnd::Unknown, never Crashed. Don’t show crash UI for it.
  • Not every crash class is caught everywhere. Windows native faults, the iOS Objective-C exception handler, and HarmonyOS errorManager are deferred to a later version; on iOS an uncaught ObjC exception ends in abort(), which the SIGABRT handler does record.

Reference

break — the full design: the report schema, signal-handler discipline, the session sentinel, app identity and symbolication, transports, and how it’s tested.