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, inpubspec.yaml:
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 withminSdkunder 26 turns on core library desugaring, as for the Android SDK.
Start
Start Apsio first thing inmain, and add the navigator observer:
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 fromApsio.trackScreen. - Network requests on iOS that go through
URLSession, such as withpackage:cupertino_http, as client spans recorded by the Apple SDK. Requests made withdart:io’sHttpClient, and so withpackage:httpon its default client, are not recorded.
Users, logs, errors and spans
The same API as the native SDKs:- The user hash follows the native SDKs’ rules: see The user hash.
recordErrortakes the stack from thecatch; without one, anError’s own stack is used, else the stack where it is called.fingerprintreplaces the grouping.- Spans. A span’s parent is
parentwhen given, otherwise the enclosingwithSpan, which stays the parent acrossawaits, otherwise the innermost span still open;root: truemakes a root span.withSpanends its span when the operation settles, with an error status if it throws. Spans started beforestartcompletes 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: beforestartit does nothing, calls made whilestartruns are applied once it completes, in order, up to 256, and a failure inside the SDK is never thrown into the app.setConsentcalled beforestartapplies 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 severityERROR 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:
redactcovers its attributes, and listingexception.messagecovers 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, anExceptionnor a string is described by its type, never serialized. AFormatExceptionleaves out its source, aFlutterErrorsends 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--obfuscaterequires, the stack holds the build’sbuild_idand an address per frame instead. Apsio resolves those addresses with the build’s debug file, theapp.<platform>-<arch>.symbolsfile Flutter writes into the--split-debug-infodirectory: on Android an ELF file whose GNU build id is the stack’sbuild_id, on iOS a Mach-O file whose UUID is thebuild_id. The names in that file stay readable under--obfuscate, so frames need no obfuscation map.
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.
Privacy and consent
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’sHttpClienthas its own sockets, so the native SDKs do not see it.package:cupertino_httpgoes throughURLSession, 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-infoare not symbolicated yet: their debug files cannot be uploaded (see Symbols for Dart stacks). On iOS, a release stack’sbuild_idhas 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
printordebugPrint: useApsio.log. - Errors in isolates you spawn are not caught: send them to
Apsio.recordError.
Next
- Upload your dSYMs and Android mappings, for native frames.
- Remote configuration: sampling and capture without a release.
- The API reference of the package.