Example walkthrough
example/lib/main.dart is the smallest correct wiring, in one file, in the order it has to happen. This page walks through it.
Run it
The repository carries the example's platform code that matters — MainActivity, AppDelegate, the crash harness, Debug.xcconfig — but not the generated scaffolding (the Gradle wrapper, the iOS runner project). Generate it first, as the README and CI do:
cd example
flutter pub get
flutter create --platforms=android . # and/or --platforms=ios
flutter run --dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=http://<your collector>:4318Without a collector the app still runs: the screen says "No collector — logging locally only".
1. The OtelZone, at the top level
final OtelZone observability = OtelZone(
OtelZoneConfig(
serviceName: 'otel-zone-example',
endpoint: const String.fromEnvironment(
'OTEL_EXPORTER_OTLP_ENDPOINT',
defaultValue: 'http://localhost:4318',
),
deploymentEnvironmentName: 'development',
spoolDirectory: () => Directory('${Directory.systemTemp.path}/otel-zone'),
exportFloor: ExportFloor.parse(
const String.fromEnvironment(
'OTEL_LOG_EXPORT_LEVEL',
defaultValue: 'warning',
),
),
),
);- The endpoint comes from
--dart-define, defaulting to a local collector over plain HTTP.secureis not set, so it follows the scheme:http://here means no TLS. deploymentEnvironmentNamebecomesdeployment.environment.nameon the resource. OneserviceNameper service, not per flavour; the environment is what tells flavours apart.- The spool is on. A recovered native crash is acknowledged once it is on disk there and delivered from there, so it survives the app being killed or relaunched before the collector answers. The example uses the temp directory only to stay free of
path_provider; a real app passesgetApplicationSupportDirectory(or better, a non-backed-up directory — see Where the spool lives). - The floor is read from somewhere untyped, which is exactly what
ExportFloor.parseis for:--dart-define=OTEL_LOG_EXPORT_LEVEL=loudfalls back towarningand says so in the start-up line, rather than quietly changing what the build sends.
2. The zone, the binding, the screen
Future<void> main() => observability.runGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
observability.setCrashScreen('/home');WidgetsFlutterBinding.ensureInitialized() runs inside runGuarded: Flutter records the zone that created the binding and warns if runApp arrives from another one.
setCrashScreen('/home') comes next. A real app calls it on every navigation, with the route pattern. It needs the binding (the platform channel throws without one) but not the SDK, so it is safe before start() — and it comes first here so that a slow start cannot let the crash harness's crash beat it. See Which screen it died on.
3. start()
await observability.start(
serviceVersion: const String.fromEnvironment(
'APP_VERSION',
defaultValue: '1.0.0',
),
buildId: const String.fromEnvironment('APP_BUILD', defaultValue: '1'),
resourceAttributes: const <String, String>{'os.type': 'android'},
);
runApp(const ExampleApp());
});In a real app serviceVersion and buildId come from package_info_plus and the resource attributes from device_info_plus, read off the installed artifact. Here --dart-define stands in for that read, so the crash harness can install two builds and tell their identities apart.
4. The app
MaterialApp(
navigatorObservers: <NavigatorObserver>[?observability.routeObserver()],
home: Scaffold(
// ...
Text(
observability.isReady
? 'Exporting to ${observability.config.endpoint}'
: 'No collector — logging locally only',
),
ElevatedButton(
onPressed: () => observability.talker.warning('The button was pressed'),
child: const Text('Log a warning'),
),
ElevatedButton(
// Nothing catches this: the zone does.
onPressed: () => throw StateError('Something impossible'),
child: const Text('Throw'),
),
const CrashHarnessPanel(),routeObserver()is spread null-aware: it isnullwhen the SDK never came up.isReadysays whetherstart()succeeded.- "Log a warning" writes to the one
Talker. At the defaultwarningfloor it is exported. - "Throw" throws from a gesture handler with nothing to catch it. The zone's handlers report it as an error, and the error brings the records before it along as a breadcrumb record.
CrashHarnessPanelshows one "Crash: kind" button per kind the platform accepts —jvm,native,anron Android;nsexception,signalon iOS — in debug builds only. It is how a person verifies native capture on a real device with no tooling attached. See Crash harness.
The integration test
example/integration_test/crash-channel_test.dart proves the half of the harness a process can outlive: the crash channel is registered, and refuses an unknown kind with a PlatformException whose code is unknown-kind. It never sends a valid kind — that would kill the test process.
cd example && flutter test integration_test -d <device>