Tauri

The Rust crate, with a disk-backed queue that survives being offline or killed.

Every call hands an event to a background worker and returns: nothing panics into your app, nothing blocks the UI thread, and if firstrun is unreachable your app is unaffected.

Add the crate

src-tauri/Cargo.toml
[dependencies]
firstrun-sdk = { path = "../../sdk/tauri" }

Not published to crates.io yet, so it is a path or a git dependency.

Start the client

src-tauri/src/main.rs
use firstrun_sdk::{Analytics, Config};

let analytics = Analytics::start(Config {
    source_key: "fr_xxxxxxxxxxxxxxxx".into(),
    host: "https://app.firstrun.app".into(),
    service_name: "YourApp".into(),
    service_version: Some(env!("CARGO_PKG_VERSION").into()),
    ..Config::default()
});

tauri::Builder::default()
    .manage(analytics)
    .run(tauri::generate_context!())?;

start returns an Analytics rather than a Result, so a bad key or an unwritable disk gives you a client that accepts every call and sends nothing instead of a failure on your startup path. app_install on the first run ever and app_launch on every run are queued for you. service_name names the directory holding the anonymous id and the queue, so set it and both survive a key rotation.

Write events

Rust
analytics.event("exported_project", attrs! { "format" => "pdf", "pages" => 12 });

analytics.error(&e, attrs! {});

analytics.log(Entry {
    name: "render_stalled".into(),
    severity: Severity::Warn,
    attributes: attrs! { "frames_dropped" => 41 },
    ..Entry::now()
});

analytics.identify(Some("acct_8812"));   // your own id, when they sign in
analytics.flush(Duration::from_secs(2)); // on exit. Optional: dropping it flushes too

event writes at INFO and error at ERROR, both filling in the conventional attributes; log takes any name, any severity and any attributes. Values go on the wire as JSON, so a number stays a number, and an event is stamped with the time it happened rather than the time it is sent.

The queue is on disk. Events are appended to an NDJSON file beside the anonymous id before they go anywhere else, bounded at 5,000 events and 2 MB, dropping the oldest and counting them in stats(). That is what makes a desktop number trustworthy: laptops are offline, processes get killed, and a launch on Friday that uploads on Monday is still a Friday launch. The anonymous id lives in per-user local application data (%LOCALAPPDATA% on Windows, not the roaming profile) because it names one installation.