Skip to main content
API preview: names may change before 1.0. apsio_flutter is a thin Dart layer over the iOS and Android SDKs, which do the work: native crashes, hangs and ANRs, app start, sessions, the store on the device and delivery. The package adds what only Dart sees: Dart errors with their Dart stacks, screen names from Flutter’s navigation, and the API to Dart. This first version records no session replay: see What this version does not do.

Requirements

  • Flutter 3.44 or later (Dart 3.10 or later). Flutter uses Swift Package Manager by default from 3.44, and the Apple SDK is a Swift package only: there is no podspec.
  • iOS 15 or later; Android 7.0 (API level 24) or later.
  • No other platform: on web and desktop every call does nothing.

Install

The package is not on pub.dev. It installs from its GitHub repository as a git dependency, in pubspec.yaml:
That works once the native SDK versions the package pins are released: the Apple SDK 0.1.0, and the Android SDK 0.1.0 in Apsio’s Maven repository. Neither is released yet, so the package cannot be installed yet. Until then, try it with the example app in apsio-flutter-sdk, which builds the native SDKs itself.
  • iOS. The plugin is a Swift package that pulls the Apple SDK from GitHub. The Apple SDK links KSCrash through Swift Package Manager: an app that also pulls KSCrash through CocoaPods (some crash reporters vendor it) gets duplicate symbols.
  • Android. The plugin adds Apsio’s Maven repository to the app’s projects and depends on io.apsio:apsio-android. An app with minSdk under 26 turns on core library desugaring, as for the Android SDK.

Start

Start Apsio first thing in main, and add the navigator observer:
An iOS app and an Android app are two apps in Apsio, each with its own key: ApsioKey.perPlatform takes both, ApsioKey('a-…') one key for whichever platform the build runs on. The app key is not a secret: it ships inside your app and only routes data to your project. A start from Dart comes after the launch, so it records no app start and cannot catch a crash that happens before Dart runs. To have both, start the native SDK first, natively, as on the iOS and Android pages, with screens off (screens come from the observer). Apsio.start then adds the Dart handlers to it, and the native start’s options are the ones in use: give the options to the native start only. A debug build warns that Apsio.start’s options are not applied when the native SDK was already started. Set FlutterError.onError and PlatformDispatcher.instance.onError before Apsio.start: a handler set after it replaces Apsio’s.

Options

Apsio.start(key, options: ApsioOptions(...)). Every automatic signal is on unless noted. There is no replay option. After a native start, these options belong to the native start.

What is recorded

Everything the native SDKs record (see iOS and Android) except replay, and from Dart:
  • Errors the framework catches (FlutterError.onError), such as an exception in a build method, and uncaught Dart errors (PlatformDispatcher.onError), as handled errors with their Dart stacks. A Dart error does not end the process, so it is not a crash; native crashes are crashes. The app’s own handlers still run.
  • Screens from ApsioNavigatorObserver, or from Apsio.trackScreen.
  • Network requests on iOS that go through URLSession, such as with package:cupertino_http, as client spans recorded by the Apple SDK. Requests made with dart:io’s HttpClient, and so with package:http on its default client, are not recorded.
On iOS the plugin ships its own privacy manifest beside the Apple SDK’s; your App Store privacy label is the same as for a native app (App Store privacy labels).

Users, logs, errors and spans

The same API as the native SDKs:
  • The user hash follows the native SDKs’ rules: see The user hash.
  • recordError takes the stack from the catch; without one, an Error’s own stack is used, else the stack where it is called. fingerprint replaces the grouping.
  • Spans. A span’s parent is parent when given, otherwise the enclosing withSpan, which stays the parent across awaits, otherwise the innermost span still open; root: true makes a root span. withSpan ends its span when the operation settles, with an error status if it throws. Spans started before start completes are not recorded, and Dart spans never parent native work, such as network requests.
  • Calls before and during start. Every call is safe at any time: before start it does nothing, calls made while start runs are applied once it completes, in order, up to 256, and a failure inside the SDK is never thrown into the app. setConsent called before start applies from the start.
  • Attribute values are strings, booleans, numbers and lists of strings; anything else is dropped, never sent as its string form.

Screens

ApsioNavigatorObserver tracks the top route of a Navigator as the current screen. A route pushed is a screen view and a screen load; a route shown again after a pop is a view only. Only page routes are screens by default, so dialogs and bottom sheets are not. Name your routes: a route with no RouteSettings.name has no screen. A path segment that holds a value becomes {id}, so Navigator.pushNamed(context, '/orders/8127') is the screen /orders/{id}, and a query or fragment is left out. To name routes yourself, give screenName; return null to skip a route, and never put an id in a name:
Apsio.trackScreen(name) names a screen by hand, and Apsio.reportFullyDrawn() records the current screen’s time to full display (see Screens).

Dart errors

A Dart error is a log record at severity ERROR with exception.type, exception.message and exception.stacktrace, plus apsio.dart.runtime (aot in release and profile builds, jit in debug) and apsio.dart.mechanism (manual, flutter_error or uncaught).
  • Redaction works as for a native handled error: redact covers its attributes, and listing exception.message covers the message and the record’s body. The type and the stack are never redacted: a stack holds function names and file names, with no local paths.
  • Messages. A value thrown that is neither an Error, an Exception nor a string is described by its type, never serialized. A FormatException leaves out its source, a FlutterError sends its one-line summary, and a URL in a message loses its query and fragment.
  • Isolates. Errors in an isolate you spawn are not caught: send them to Apsio.recordError.

Symbols for Dart stacks

How a Dart stack reads depends on how the app was built:
  • Without --split-debug-info, the stack names its functions, files and lines (#0 Checkout.pay (package:shop/checkout.dart:12:3)). Apsio reads it as it is, with nothing to upload.
  • With --split-debug-info, which --obfuscate requires, the stack holds the build’s build_id and an address per frame instead. Apsio resolves those addresses with the build’s debug file, the app.<platform>-<arch>.symbols file Flutter writes into the --split-debug-info directory: on Android an ELF file whose GNU build id is the stack’s build_id, on iOS a Mach-O file whose UUID is the build_id. The names in that file stay readable under --obfuscate, so frames need no obfuscation map.
The apsio CLI cannot upload Flutter’s .symbols files yet, so a stack from a build with --split-debug-info keeps its addresses for now. It is grouped on its frames without exception.type, which --obfuscate renames in every build, and the release’s missing symbols list the build’s Dart code as libapp.so (Android) or App.framework (iOS), with its debug id. In your app’s code are the frames outside the Dart SDK, Flutter itself, the packages in the pub cache and apsio_flutter. Native crashes in a Flutter app are native: upload dSYMs and Android mappings as for a native app. The native SDKs keep the data and apply consent, so everything in Privacy and consent holds for a Flutter app. Apsio.setConsent(false) stops collection and drops what is queued, and the choice is kept across launches; called before start, it applies from the start. If your app must ask before anything is collected, call start only after the user agrees. Keys listed in redact never leave the device, from Dart records and native ones alike, since the native SDK redacts every record. Never put an email, a name or an id in a screen name, a span name or a log message.

What this version does not do

  • No session replay, and no option for it. Flutter draws its own pixels, so the native view tree shows one flat surface, and a wireframe replay needs a walk of the widget tree that does not exist yet. Masking fails closed, so there is no replay rather than a partial one. If you start the native SDK yourself, leave its replay option off too.
  • No live inspection, for the same reason: its wireframes need that walk.
  • No spans for Dart’s HTTP requests. dart:io’s HttpClient has its own sockets, so the native SDKs do not see it. package:cupertino_http goes through URLSession, which the Apple SDK does see.
  • Slow and frozen frames are not Flutter’s. The native SDKs count what their platform reports for the window; Flutter’s frame timings do not reach them yet.
  • Dart stacks from a build with --split-debug-info are not symbolicated yet: their debug files cannot be uploaded (see Symbols for Dart stacks). On iOS, a release stack’s build_id has not yet been compared with the Mach-O UUID of its debug file on a real device, so that match is unproved.
  • No log bridge for print or debugPrint: use Apsio.log.
  • Errors in isolates you spawn are not caught: send them to Apsio.recordError.

Next