Skip to content

feat: error reporting (crashes and handled errors) - #38

Merged
cristipufu merged 1 commit into
mainfrom
feat/error-reporting
Oct 16, 2026
Merged

cristipufu merged 1 commit into
mainfrom
feat/error-reporting

Conversation

@cristipufu

@cristipufu cristipufu commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Adds error reporting to the error endpoint (/api/v0/error), matching the React Native, MAUI, Unity and Unreal SDKs. Reports appear on the Errors page of the dashboard.

Handled errors (try/catch)

Rust, through a new ErrorTracker trait on App, AppHandle and Window:

use tauri_plugin_aptabase::{ErrorReport, ErrorTracker};

if let Err(err) = std::fs::read_to_string("settings.json") {
    app.track_error(&err); // any std::error::Error: type name, message and its causes
}
app.track_error_report(ErrorReport::from_error(&err).with_fatal(true)); // severity: fatal

JavaScript, behind the new aptabase:allow-track-error permission:

import { trackError } from "@aptabase/tauri";

try { await saveSettings() } catch (error) { trackError(error) }
trackError(error, { fatal: true });

Crashes

tauri_plugin_aptabase::Builder::new("<APP_KEY>").with_crash_reporting().build()
What happened Reported as Delivery
Panic on the main thread (the app dies) fatal / crash Sent straight away; the hook waits up to 3 s before the app exits
Panic in an async command or a task spawned on an async runtime (only the task dies), on whatever thread it runs error / taskException In the background
Panic on another thread (only that thread dies) error / unhandled In the background
Uncaught error in the webview error / unhandled Through an init script; skips cross-origin "Script error." and ResizeObserver loop noise
Unhandled promise rejection in the webview error / taskException Same init script

A hook set with with_panic_hook still runs first. With panic = "abort", every panic is a crash.

A crash's report goes straight out, never through the queue. It is sent from a thread and a runtime of its own, on a connection of its own, so it doesn't wait for a send already in flight, which may be stuck on the crashing runtime: an app can hand Tauri a single-thread runtime with tauri::async_runtime::set. A full queue can't drop it either. The reports queued before it follow, and the hook waits at most 3 s in all. What a custom with_panic_hook tracked is flushed after the crash's report, within the same limit.

Delivery

  • One report per request, sent right away rather than on the next flush.
  • Fields are cut to the endpoint's limits in UTF-16 units, the way .NET counts a string's length, so a report is never refused for length. An empty type or message is never sent.
  • Network errors, 408, 429 and 5xx are retried in order on the next flush. A 403 (quota used up) and any other 4xx are dropped, as the server's comments ask. At most 25 reports wait in the queue; a fatal report the app tracks itself takes the place of the oldest one that isn't fatal.

Fix included

flush_events_blocking(), and the flush after a custom panic hook, panicked with there is no reactor running when events were queued and the calling thread was outside the async runtime. That includes the main thread handling RunEvent::Exit, which is the pattern the README shows. From such a thread the flush now runs on Tauri's runtime while the thread waits; inside a runtime it runs in place, as before. The crash path needed this, and it fixes the exit flush too.

Tests

The crate had no tests before; it now has 21, run with cargo test. Among the unit tests: a crash sent first with the queue full, a crash that doesn't wait for a flush in progress, and a fatal report evicting the oldest non-fatal one.

  • Report contents: every field, severity and kind for each way an error is captured, never an empty type or message, and truncation in UTF-16 units (emoji included, never splitting a character).
  • Delivery against a local HTTP server: one report per request with the App-Key; 503 kept and resent in order; 403 dropped; the 25-report cap; a blocking flush from a thread outside the runtime (this test fails on the old code); a bounded flush that gives up at its timeout.
  • tests/crash_reporting.rs: the plugin built into a mock Tauri app. A panic on a thread named main arrives as a fatal crash before the hook returns, and a panic in a tauri::async_runtime task arrives as a taskException.
  • tests/crash_on_single_thread_runtime.rs: a crash on the only thread of a single-thread runtime handed to Tauri, while another report's send is stuck in flight, is delivered at once. The task panic before it is reported as a taskException, not as a crash.
  • tests/ipc.rs: the webview's track_error arguments as JavaScript names them (errorType, stackTrace: null, kind), through Tauri's IPC.

The example app turns on crash reporting and adds two buttons, Report an error and Panic in Rust.

Tokio's rt, time and net features are now declared rather than relied on through Tauri's: tokio::time::sleep was already used without time.

No version bump: the changelog has an [Unreleased] entry.

Release

This PR also releases 1.3.0 (crate) and 0.7.0 (@aptabase/tauri on npm): Cargo.toml and package.json are bumped, the changelog section is [1.3.0] - 2026-10-16 (it includes #40), and the install snippets in the README and llms.txt point at 1.3. Publishing a 1.3.0 GitHub release after merging runs release.yml for both registries. cargo publish --dry-run and npm pack --dry-run pass locally, and the committed webview-dist matches a fresh build.

🤖 Generated with Claude Code

@cristipufu
cristipufu requested review from reatlat and a balanced review from Copilot October 16, 2026 10:13

This comment was marked as off-topic.

This comment was marked as outdated.

@cristipufu
cristipufu force-pushed the feat/error-reporting branch 3 times, most recently from 68c9f1b to 1387ba6 Compare October 16, 2026 12:47
Reports errors to the error endpoint (/api/v0/error), the same way the React
Native, MAUI, Unity and Unreal SDKs do; they appear on the Errors page.

- Handled errors: `ErrorTracker` on App/AppHandle/Window (`track_error(&err)`
  for any std::error::Error, with its type name and causes;
  `track_error_report(ErrorReport)` for full control, `with_fatal(true)` for
  severity fatal), and `trackError(error, { fatal })` in the JS bindings
  behind the new `aptabase:allow-track-error` permission.
- Crashes: `Builder::with_crash_reporting()`. A panic in a spawned task is a
  `taskException` on whatever thread it runs (only the task dies; a
  single-thread runtime runs its tasks on the main thread); otherwise a panic
  on the main thread is a fatal `crash`, and one on another thread is
  `unhandled`. An init script reports the webview's uncaught errors
  (`unhandled`) and unhandled rejections (`taskException`), skipping "Script
  error." and ResizeObserver loop noise. A custom `with_panic_hook` still runs
  first; what it tracked is flushed after the crash's report, within the same
  limit.
- A crash's report goes straight out, never through the queue: from a thread
  and a runtime of its own, on a connection of its own, without waiting for a
  send in flight (which may be stuck on the very runtime the crashing thread
  is on, e.g. a single-thread runtime handed to Tauri with
  `tauri::async_runtime::set`), and even with the queue full. The reports
  queued before it follow; the hook waits at most 3 s in all.
- Delivery: one report per request, sent right away; fields cut to the
  endpoint's limits in UTF-16 units (as .NET counts them, so a report is never
  refused for length); never an empty type or message; network errors, 408,
  429 and 5xx retried in order, 403 (quota) and other 4xx dropped; at most 25
  queued, a fatal report taking the place of the oldest one that isn't.
- Tokio's `rt`, `time` and `net` features are declared rather than relied on
  through Tauri's (`tokio::time::sleep` was already used without `time`).

Fix: `flush_events_blocking()` (and the flush after a custom panic hook)
panicked with "there is no reactor running" when events were queued and the
thread was outside the async runtime, e.g. the main thread handling
RunEvent::Exit as the README shows. Outside the runtime the flush now runs on
it while the thread waits; inside one it runs in place, as before.

Tests: report fields and limits, retry rules, the queue cap and its fatal
eviction, a crash sent first with the queue full and without waiting for a
flush in progress, a blocking flush from outside the runtime (fails on the old
code), and end to end through a mock Tauri app: panics reported as what they
ended, a crash on the only thread of a single-thread runtime delivered while
another report's send is stuck in flight (lost before, and the task's panic
misreported as a crash), and the webview's track_error arguments over IPC.

Release 1.3.0 (crate) and 0.7.0 (@aptabase/tauri on npm).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cristipufu
cristipufu force-pushed the feat/error-reporting branch from 1387ba6 to 72e612e Compare October 16, 2026 13:53
@cristipufu
cristipufu merged commit 1ba2181 into main Oct 16, 2026
1 check passed
@cristipufu
cristipufu deleted the feat/error-reporting branch October 16, 2026 14:17
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