The question a mobile team asks after every release is the same, and it has a number: of the sessions on the new version, what fraction ended without a crash? Not "how many crashes" โ a popular release will have more of everything. The rate, per release, compared to the one before it.
Sentrinel's Flutter SDK answers that from the first session, and a few things around it that turned out to be as important.
Crash-free, per release
Every session the app starts is recorded against the version it is running, and every crash is recorded against the session it ended. Crash-free sessions and crash-free users per release fall out of that directly, and the release page shows each version against its predecessor.
The version comes from init:
Sentrinel.init(
endpoint: 'https://api.sentrinel.dev',
apiKey: 'snt_mobile_โฆ',
appName: 'shop-app',
env: 'prod',
release: '2.4.1+318',
);
The key is a mobile key. It ships inside the bundle, which anyone can unzip, so it was issued to send only what a phone sends โ a leaked one cannot post a replay, reach the database collector, or read anything.
The crash has to survive the crash
An uncaught Dart error ends the process. Anything the SDK has in memory ends with it, including the report of the error that just happened. So the report is written to disk first, synchronously, in the crash handler, and uploaded on the next launch. If the app never launches again, nothing is lost that could have been kept; if it does, the crash arrives with the breadcrumbs that led to it.
Those breadcrumbs โ the last twenty-five things that happened: screens, requests, log lines, taps you recorded โ are bounded on purpose. A crash report is the wrong place to discover an unbounded list.
Start-up and frames
Two performance numbers that users feel and dashboards usually miss. Start-to-first-frame is how long the app took to draw anything, measured from process start. Slow and frozen frames are the frames that missed their deadline and the ones that missed it badly enough to read as a hang. Both are recorded per session and shown per release, because a regression in either is the kind of thing that ships without anyone noticing.
What people did, not just what broke
Errors say what broke. Events say what people did, and they are the other half of the release question โ did the new checkout flow work, in the sense that people finished it?
Sentrinel.screen('Cart');
Sentrinel.track('checkout_started', properties: {'cart_value': 42, 'tier': 'pro'});
// When they sign in:
Sentrinel.identify('user_42');
Sentrinel.track('purchase_completed', properties: {'revenue': 129.0});
These feed funnels, retention and top screens. They are deliberately not logs: a log line is for a person debugging, an event is a row to be counted. The anonymous id is minted once and written to disk, so a user who has not signed in is still the same user across launches โ without that, retention reads flat and "daily actives" means "daily launches". identify() attaches the real id and both travel together, which is what makes a signup funnel measurable across the sign-in boundary.
One trace, phone to backend
Wrap the HTTP client and every request the app makes is recorded with its route template, host, status and timing โ and carries a traceparent header. The backend plugin picks it up, and the span that started on the phone continues into the server, through its database calls, and back. From the app's slow request you open a trace that shows where the time went on both sides.
That last part is the one nothing else does well, and it is the reason the mobile SDK and the backend plugin are one product rather than two.
Not covered yet
Native iOS and Android crashes โ the ones below the Dart layer โ and symbolication of obfuscated stacks. Both need an upload pipeline for symbols, which is the next piece. Everything above is in the mobile guide, with the full setup.