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 bugwatch

iOS 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

OptionDefaultWhat it does
enabledtrueMaster switch
endpointhttps://api.newinstance.cloudOverride for dev or self-hosted backends
environment / release- / -Labels (the Android native layer defaults environment to production when unset); release must match symbol uploads
autoCaptureErrorstrueHooks FlutterError.onError + PlatformDispatcher.onError, chaining your handlers
sampleRate1.0Client-side sampling
sensitiveFieldsbuilt-in list (password, token, authorization, cvv, bvn, nin, more), case-insensitive; yours merge inRedaction
batchSize / flushIntervalMs / maxQueueSize50 / 5000 / 1000Delivery and offline queue
requestTimeoutMs15000Per-request timeout
retry3 attempts, 500 ms to 10 sExponential backoff
debugfalseFeeds 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 captureException is positional; capture methods return a Future<String?> event id.
  • Severity wire values: trace 10, debug 20, info 30, warn 40, error 50, fatal 60.
  • setContext(key, value) takes String values; Breadcrumb requires category (type defaults to default, level to Severity.info, data is Map<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: and package:flutter* are marked not-in-app for cleaner grouping.
  • Events are tagged platform: flutter so 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 classCaught byLevel
Uncaught Dart / Flutter errorFlutterError.onError, PlatformDispatcher.onErrorerror
Uncaught JVM exception (Android)Native JVM handlerfatal
Uncaught NSException (iOS)Native NSUncaughtExceptionHandlerfatal
Native signal crash, either platformNative signal handlersfatal
Crash in the Flutter engine or a native pluginNative signal handlersfatal
Stack overflowNative handlers, on the alternate signal stackfatal
ANR (Android) / app hang (iOS)Native watchdogserror

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 capturedWhy
Out-of-memory kills, jetsam, watchdog terminationsThe 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 appIndistinguishable from a clean exit
Anything before BugWatch.init completesHandlers are armed during init. Await it as early as possible in main()
A crash inside the crash handler itselfThe signal is already being handled and the process dies
A stack overflow on a thread other than the one that started the SDKsigaltstack 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.1 so Flutter apps get stack-overflow capture; the previous ~> 0.1.0 pin permitted that fix without requiring it.
  • 0.1.1 added autoCaptureErrors, BugWatch.runZonedGuarded and 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 from init's.
  • Silence in debug: subscribe to diagnostics: BugWatch.instance.onDiagnostic.listen((d) => print('${d.event}: ${d.data}')); with debug: true.
  • Wire endpoint the natives call: POST https://api.newinstance.cloud/api/v1/bugwatch/ingest/mobile.