Flutter
The Flutter plugin bridges 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 | none / none | 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.