Mobile: Flutter and Dart

Two packages, because they answer to different constraints.

Package Depends on Use it when
sentrinel http only Any Dart: Flutter, a CLI, a server-side job
sentrinel_flutter flutter, path_provider, sentrinel A Flutter app

The core deliberately takes no Flutter dependency โ€” adding one would stop it working outside Flutter, which is where a good part of its use is. Everything needing the framework lives in the integration package instead. Sentry splits the same way, for the same reason.


Install

dependencies:
  sentrinel_flutter:
    git:
      url: https://github.com/Zaga-ltd/sentinel_packages
      path: packages/sentrinel_flutter_integration

The whole setup

void main() => SentrinelFlutter.run(
      options: SentrinelOptions(
        serverUrl: 'https://api.sentrinel.dev',
        appName: 'mobile-app',
        env: 'prod',
        release: '1.4.2',
        apiKey: const String.fromEnvironment('SENTRINEL_API_KEY'),
      ),
      app: () => runApp(const MyApp()),
    );

That single call starts a guarded zone, finds a storage directory that survives a restart, installs both error handlers, and begins tracking frames and app start.

runApp goes inside the app callback rather than after the call. Asynchronous errors are only caught within the zone, so calling runApp outside it silently misses most of them.

Add the navigator observer for screen context and navigation breadcrumbs:

MaterialApp(navigatorObservers: [SentrinelNavigatorObserver()])

Two values worth getting right

release โ€” crash-free rate is per release. Without it every build is unknown and a regression is invisible, which is the entire point of the page.

env โ€” must match the environment the API key was issued for. Ingest checks appName + env against the key and answers 403 on a mismatch, which the SDK reports once at startup and then goes quiet. A wrong value looks exactly like "monitoring is fine, we just have no traffic."


What gets captured

Crashes

Uncaught Dart errors are captured automatically and written to disk before the process dies, then delivered on the next launch. That last part is what makes it crash reporting rather than error reporting: the ordinary buffer flushes on a 30-second timer, which a crashing app never reaches.

Source Mechanism Covered
Async errors escaping to the zone runZonedGuarded โœ…
Flutter build/layout/paint errors FlutterError.onError โœ…
Errors reaching the engine PlatformDispatcher.onError โœ…
Errors on other isolates Isolate.onError โœ…
Native iOS/Android crashes, ANRs โ€” โŒ

Every report carries the OS and version, locale, core count, the session id, the screen, and the last 25 breadcrumbs. The Issues page renders all of it: a fatal badge, the mechanism that caught it, the device, and the trail as an expandable list, oldest first so it reads as the sequence that led there.

Breadcrumbs fill themselves from HTTP requests and navigation. Add your own for taps and business events:

Sentrinel.addBreadcrumb('tapped pay', category: 'ui', data: {'amount': 42});

Release health

One session per app launch, which is the denominator crash-free rate needs. See GUIDE.md for how to read the page.

Sessions end up in one of four states:

Status Meaning
ok Running, or ended cleanly
crashed A fatal error was written before the process died
abnormal Ended without a clean shutdown and without a crash report โ€” force-quit, OOM kill, flat battery
errored Survived, but reported a handled error

abnormal is kept apart from crashed deliberately. Counting a user swiping the app away as a crash against your release would make the number worthless.

Performance

Both arrive as structured logs with category: performance, so they are searchable and available in the field explorer.

Requests, and the trace that joins them

SentrinelHttpClient records every call and sends traceparent, so the backend plugin continues the same trace rather than starting a new one โ€” a tap and the server work it caused land on one timeline. That join is the thing most tools do not do well.

final client = Sentrinel.httpClient();
await client.get(Uri.parse('https://api.example.com/orders'));

Not covered

These are one project rather than two: native traces without symbolication are unreadable hex.


Seeing it without building an app

API=https://api.sentrinel.dev KEY=snt_live_โ€ฆ APP=mobile-app \
  bun run examples/mobile-telemetry.ts

Sends the same shapes the SDK does โ€” two releases where one is measurably worse, crashes with breadcrumbs and device context, and frame/app-start records.

Troubleshooting

TROUBLESHOOTING.md has a mobile section covering the usual causes: a missing storagePath, guard() without the Flutter handlers, a missing release, and sessions that all report as abnormal.