Flutter
Flutter plugin bridging Dart error capture into the native Android and iOS BugWatch SDKs: native crashes (including Android NDK), ANRs and app hangs, sessions, breadcrumbs and the offline queue come with it.
Requirementslink
- Dart 3.11+, Flutter 3.3+, iOS 14+ (Podfile must declare
platform :ios, '14.0'or higher), Android minSdk 24 with JDK 17 - Android setup is zero-config beyond
mavenCentral()in your repositories; autolinking does the rest
1 - Installlink
flutter pub add bugwatchiOS needs one manual Podfile line until the pod ships on the CocoaPods trunk. Add it above flutter_install_all_ios_pods:
pod 'BugWatch', :path => '../relative/path/to/bug-watch-ios'If CocoaPods complains about a bugwatch vs BugWatch name collision, run pod deintegrate, pod cache clean --all, then pod install.
2 - Initialise (before runApp)link
Get projectId and appSecret from the dashboard: your BugWatch project, then Mobile credentials, then Reveal (see the Mobile overview for how the on-device signing works).
import 'package:bugwatch/bugwatch.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await BugWatch.instance.init(const BugWatchOptions(
projectId: String.fromEnvironment('BUGWATCH_PROJECT_ID'),
appSecret: String.fromEnvironment('BUGWATCH_APP_SECRET'),
environment: 'production',
release: '1.4.2+318',
));
runApp(const MyApp());
}init() is idempotent: repeat calls before close() return immediately. Pass credentials with --dart-define or --dart-define-from-file so they stay out of source.
3 - Configurationlink
| Option | Default | What it does |
|---|---|---|
enabled | true | Master switch |
endpoint | https://api.newinstance.cloud | Override for dev or self-hosted backends |
environment / release | - / - | Labels (the Android native layer defaults environment to production when unset); release must match symbol uploads |
autoCaptureErrors | true | Hooks FlutterError.onError + PlatformDispatcher.onError, chaining your handlers |
sampleRate | 1.0 | Client-side sampling |
sensitiveFields | built-in list (password, token, authorization, cvv, bvn, nin, more), case-insensitive; yours merge in | Redaction |
batchSize / flushIntervalMs / maxQueueSize | 50 / 5000 / 1000 | Delivery and offline queue |
requestTimeoutMs | 15000 | Per-request timeout |
retry | 3 attempts, 500 ms to 10 s | Exponential backoff |
debug | false | Feeds the onDiagnostic stream |
ANR and hang thresholds are fixed by the native layer (Android 5000 ms, iOS 2000 ms) and are not configurable from Dart.
4 - Capture APIlink
final id = await BugWatch.instance.captureException(error, st);
await BugWatch.instance.captureMessage('Sync finished', level: Severity.info);
await BugWatch.instance.setUser(BugWatchUser(id: 'u_123', email: 'ada@example.com'));
await BugWatch.instance.setUser(null);
await BugWatch.instance.setTag('tenant', 'acme');
await BugWatch.instance.setContext('payment.provider', 'paystack');
await BugWatch.instance.addBreadcrumb(Breadcrumb(category: 'ui', message: 'Tapped checkout'));
await BugWatch.instance.setRelease('1.4.3+319');
await BugWatch.instance.flush();
await BugWatch.instance.close();- The stack trace argument to
captureExceptionis positional; capture methods return aFuture<String?>event id. Severitywire values: trace 10, debug 20, info 30, warn 40, error 50, fatal 60.setContext(key, value)takes String values;Breadcrumbrequirescategory(typedefaults todefault,leveltoSeverity.info,dataisMap<String, String>).close()tears down the native SDK and restores your original error handlers.- For code outside the Flutter zone use the static
BugWatch.runZonedGuarded(() => ...).
Automatic capturelink
- Dart errors via the two framework hooks; frames from
dart:andpackage:flutter*are marked not-in-app for cleaner grouping. - Events are tagged
platform: flutterso the backend applies Dart-frame handling rather than dSYM or R8 paths. - Native crashes, ANRs and hangs, sessions, lifecycle and network breadcrumbs, and offline persistence come from the underlying native SDKs with the same credentials.
5 - Symbolication (CI)link
flutter build apk --release --obfuscate --split-debug-info=build/symbols
(cd build/symbols && zip -r ../symbols.zip .)
npx @newinstance/bugwatch-cli artifacts upload build/symbols.zip \
--release "1.4.2+318" --platform flutter --type dart-symbols --token "$BUGWATCH_CI_KEY"artifacts upload takes a single file, so zip the --split-debug-info directory first as shown.
Obfuscated traces that cannot be parsed on device (they contain isolate_instructions) ship verbatim as rawStack and are resolved server-side. artifacts upload handles text inputs (Dart symbols, R8 mappings); binary dSYM and ELF files go through symbols upload. Add the Android R8 mapping and iOS dSYMs for native frames (see the Android and iOS & macOS pages).
What is captured, and at what levellink
| Crash or event class | Caught by | Level |
|---|---|---|
| Uncaught Dart / Flutter error | FlutterError.onError, PlatformDispatcher.onError | error |
| Uncaught JVM exception (Android) | Native JVM handler | fatal |
Uncaught NSException (iOS) | Native NSUncaughtExceptionHandler | fatal |
| Native signal crash, either platform | Native signal handlers | fatal |
| Crash in the Flutter engine or a native plugin | Native signal handlers | fatal |
| Stack overflow | Native handlers, on the alternate signal stack | fatal |
| ANR (Android) / app hang (iOS) | Native watchdogs | error |
Dart errors are recorded at error rather than fatal on purpose. Unlike React Native, Flutter does not terminate the process on an uncaught Dart error, and neither error callback carries a fatal flag. A failure that genuinely kills a Flutter app is a native crash, and the native handlers record that as fatal.
What is never capturedlink
Platform limits, not SDK limits.
| Not captured | Why |
|---|---|
| Out-of-memory kills, jetsam, watchdog terminations | The OS sends SIGKILL, which no handler can intercept. The prior session finalises as exited, so crash-free rate reads slightly optimistic |
| The user force-quitting the app | Indistinguishable from a clean exit |
Anything before BugWatch.init completes | Handlers are armed during init. Await it as early as possible in main() |
| A crash inside the crash handler itself | The signal is already being handled and the process dies |
| A stack overflow on a thread other than the one that started the SDK | sigaltstack is registered per-thread |
Delivery needs a relaunchlink
Nothing is uploaded from inside a crash handler. The handler writes an artifact to disk synchronously and the next launch uploads it. There is no background job that uploads after the process is gone, so a user who crashes and never returns is never counted. Queued events are delayed, never lost.
Upgradinglink
- 0.1.5 requires the native iOS pod at
~> 0.1.1so Flutter apps get stack-overflow capture; the previous~> 0.1.0pin permitted that fix without requiring it. - 0.1.1 added
autoCaptureErrors,BugWatch.runZonedGuardedand Android NDK capture. - 0.1.3 and 0.1.4 are maintenance releases with no option or API surface changes.
- 0.1.2 pinned the iOS pod to
BugWatch ~> 0.1.0.
Troubleshootinglink
- MissingPluginException: hot restart does not register native code after adding the plugin; do a full cold
flutter run. - Dart stacks unreadable in release: the build ran without
--split-debug-info, or the uploaded release string differs frominit's. - Silence in debug: subscribe to diagnostics:
BugWatch.instance.onDiagnostic.listen((d) => print('${d.event}: ${d.data}'));withdebug: true. - Wire endpoint the natives call:
POST https://api.newinstance.cloud/api/v1/bugwatch/ingest/mobile.