bugwatch 0.1.6 copy "bugwatch: ^0.1.6" to clipboard
bugwatch: ^0.1.6 copied to clipboard

BugWatch — crash, error, and log observability for Flutter. Capture exceptions, messages, breadcrumbs, and scope from your app via native iOS/Android bridges.

bugwatch #

BugWatch Flutter SDK — crash, error, and log observability for Flutter apps. Capture unhandled and handled exceptions, log messages, breadcrumbs, and user context, and ship them to newinstance.cloud.


How it works (architecture) #

The Dart BugWatch API is a thin bridge. Every method call — init, capture, scope, flush — is forwarded across a MethodChannel (channel name: bugwatch) to a native Flutter plugin, which delegates to the real native BugWatch SDK:

  • iOS: the BugWatch CocoaPod (bug-watch-ios)
  • Android: the cloud.newinstance:bugwatch Maven artifact (bug-watch-android)

The native SDKs own everything below the Dart layer: delivery over HTTPS (with batching, retry, and persistent queuing), signal-based native crash capture, ANR/app-hang detection, release-health session tracking, device metadata collection, and automatic breadcrumbs. The Dart layer translates your Dart exceptions and messages into the format the native SDKs expect, then hands off.

An EventChannel (bugwatch/events) streams native diagnostic lines back to Dart when debug: true, accessible via BugWatch.instance.onDiagnostic.


Requirements #

Requirement Minimum
Dart SDK ^3.11
Flutter ≥ 3.3
iOS deployment target 14.0
Android minSdkVersion 24
Android JDK 17

Installation #

1. Add the Dart package #

Add the package from pub.dev:

# pubspec.yaml
dependencies:
  bugwatch: ^0.1.2

For local development, use a path dependency instead:

dependencies:
  bugwatch:
    path: ../bug-watch-flutter

Then run:

flutter pub get

2. Android native setup #

The Flutter plugin depends on cloud.newinstance:bugwatch:0.1.3. The plugin's android/build.gradle.kts uses Maven Central by default, with mavenLocal() kept as a development fallback.

The cloud.newinstance:bugwatch:0.1.3 artifact is published to Maven Central. Add mavenCentral() to your app's android/build.gradle repositories block if it is not already present (it usually is by default in new Flutter projects):

// android/build.gradle  (or settings.gradle dependencyResolutionManagement block)
repositories {
    google()
    mavenCentral()
}

No other Gradle changes are needed. Flutter autolink registers the plugin.

For local native SDK development, publish from bug-watch-android first:

./gradlew :sdk:publishToMavenLocal

3. iOS native setup (required manual step) #

The plugin's podspec declares s.dependency 'BugWatch', '~> 0.1.0', referring to the native BugWatch iOS SDK pod.

Until BugWatch is published to the CocoaPods trunk spec repo, you must add a local path override to your app's ios/Podfile above the flutter_install_all_ios_pods call:

# ios/Podfile
target 'Runner' do
  use_frameworks!
  use_modular_headers!

  # Local path to the native BugWatch iOS SDK.
  # Adjust the relative path to match your checkout layout.
  pod 'BugWatch', :path => '../../bug-watch-ios'

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

Then run:

cd ios && pod install

Known issue — pod name case collision: CocoaPods is case-sensitive in some contexts. The plugin's podspec is named bugwatch (all lowercase), while the native dependency is BugWatch (TitleCase). If pod install fails with a case-related conflict, try cleaning the Pods cache:

cd ios
pod deintegrate
pod cache clean --all
pod install

The iOS deployment target in ios/Podfile must be 14.0 or higher:

platform :ios, '14.0'

Getting your project credentials #

Log in to your merchant dashboard at newinstance.cloud, open the BugWatch section, create or select a project, then go to Settings → API Keys.

  • Project ID — starts with bwp_, e.g. bwp_a1b2c3d4
  • App secret — a long random string

The app secret is used only on-device to sign a short-lived ingest token (HMAC-SHA256) before each batch is delivered. The secret itself is never transmitted to the server.


Initialisation #

Call BugWatch.instance.init() once in main(), after WidgetsFlutterBinding.ensureInitialized() and before runApp().

import 'package:bugwatch/bugwatch.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await BugWatch.instance.init(const BugWatchOptions(
    projectId: 'bwp_<your-project-id>',
    appSecret: '<your-app-secret>',
    environment: 'production',
    release: '1.4.2+318',         // semver or build string — your choice
  ));

  runApp(const MyApp());
}

init() is idempotent — subsequent calls before close() return immediately without reconfiguring.

All configuration options #

BugWatchOptions(
  // Required
  projectId: 'bwp_…',             // BugWatch project public id
  appSecret: '…',                  // Signing secret (never transmitted)

  // Strongly recommended
  environment: 'production',       // Informational tag
  release: '1.4.2+318',           // Release / build identifier

  // Optional — defaults shown
  endpoint: 'https://api.newinstance.cloud',  // Override only for self-hosted / dev
  enabled: true,                   // Master switch; false = no collection, no delivery
  debug: false,                    // Emit internal diagnostics to BugWatch.instance.onDiagnostic
  sampleRate: 1.0,                 // 0.0–1.0 fraction of events to keep
  maxQueueSize: 1000,              // Max pending events before oldest are dropped
  batchSize: 50,                   // Events per NDJSON ingest request
  flushIntervalMs: 5000,           // Auto-flush cadence in ms (0 = timer disabled)
  requestTimeoutMs: 15000,         // Per-request network timeout in ms
  autoCaptureErrors: true,         // Install global error handlers automatically (see below)
  sensitiveFields: [...],          // Keys whose values are redacted (case-insensitive)
  retry: RetryPolicy(              // Retry policy for failed ingest requests
    maxRetries: 3,
    baseDelayMs: 500,
    maxDelayMs: 10000,
  ),
)

sensitiveFields defaults cover common credential keys: password, passwd, pwd, token, accesstoken, refreshtoken, idtoken, authorization, auth, cookie, setcookie, secret, clientsecret, apikey, privatekey, sessionid, ssn, creditcard, cardnumber, cvv, pin, nin, bvn.


Automatic error capture (implemented) #

When autoCaptureErrors: true (the default), init() installs two global error handlers automatically:

  • FlutterError.onError — catches errors reported by the Flutter framework: widget build failures, image-load errors, rendering exceptions, etc.
  • PlatformDispatcher.instance.onError — catches uncaught async errors that escape the Flutter framework's own zone.

Both handlers chain any previously-installed callback, so any handler your app or another SDK installed before BugWatch.instance.init() keeps running. The originals are restored when BugWatch.instance.close() is called.

With autoCaptureErrors: true you do not need try/catch or runZonedGuarded for most unhandled errors — they are captured automatically.

Set autoCaptureErrors: false if you need full manual control.

runZonedGuarded helper #

For errors that escape both Flutter framework zones and PlatformDispatcher, wrap your runApp() call with the provided helper:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await BugWatch.instance.init(const BugWatchOptions(
    projectId: 'bwp_…',
    appSecret: '…',
  ));

  await BugWatch.runZonedGuarded(() async {
    runApp(const MyApp());
  });
}

BugWatch.runZonedGuarded wraps dart:async's runZonedGuarded and forwards any zone-escaping error to captureException. It can be combined with autoCaptureErrors: true for maximum coverage.


Capturing events manually #

Handled exceptions #

try {
  await doRiskyThing();
} catch (error, stack) {
  await BugWatch.instance.captureException(error, stack);
}

captureException parses the Dart StackTrace into structured frames (filename, function, line, column) and forwards them — along with the error runtime type and message — to the native SDK via captureWrapperException(..., platform: "flutter"). The platform: "flutter" tag tells the BugWatch worker to route the frames to Dart-frame handling rather than the dSYM / R8 symbolicators.

Log messages #

await BugWatch.instance.captureMessage('Checkout started', level: Severity.info);

Severity values and their wire integers:

Constant Value
Severity.trace 10
Severity.debug 20
Severity.info 30
Severity.warn 40
Severity.error 50
Severity.fatal 60

Scope #

Scope is sticky — it is attached to every subsequent event until changed or cleared.

User identity #

// Set user
await BugWatch.instance.setUser(const BugWatchUser(
  id: 'u_123',
  email: 'ada@example.com',
  username: 'ada',
  // ip: '…',   // optional
));

// Clear user (e.g. on logout)
await BugWatch.instance.setUser(null);

Tags and context #

await BugWatch.instance.setTag('screen', 'checkout');
await BugWatch.instance.setContext('cart_id', 'c_987');

Release #

await BugWatch.instance.setRelease('1.4.3+319');

Prefer setting release in BugWatchOptions at init time. Call setRelease only if the release identifier changes at runtime (unusual).

The SDK retains a bounded history of breadcrumbs and attaches them to every outgoing event so you can see what led up to a problem.

await BugWatch.instance.addBreadcrumb(Breadcrumb(
  category: 'navigation',
  message: 'Opened checkout',
  level: Severity.info,
  // type: 'default',     // optional, defaults to 'default'
  // data: {'key': 'val'} // optional string key/value map
  // timestamp: DateTime.now()  // optional, defaults to now
));

Native crash capture, ANR detection, and session tracking #

These features are fully implemented in the underlying native SDKs and work automatically once the Flutter plugin is initialized:

  • Native crash capture: on iOS, POSIX signal handlers (SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE, SIGTRAP, SIGSYS) plus an NSUncaughtExceptionHandler, with binary-image collection for dSYM symbolication; on Android: JVM uncaught exception handler + NDK C++ signal handler (4 ABIs) for ProGuard/R8 and ELF symbolication. Both platforms run their handlers on a dedicated alternate signal stack, which is what makes a stack overflow reportable, and both chain whatever handler was installed before them so Crashlytics and the OS crash report still work.
  • ANR / app-hang detection — on Android: watchdog thread fires at 5 000 ms; on iOS: app-hang tracking fires at 2 000 ms (both configurable via BugWatchOptions fields passed from the native option objects, not currently exposed in BugWatchOptions Dart API — native defaults apply).
  • Release-health sessions — the native SDK tracks session start / end / crash state and reports to the ingest API automatically.

You do not need to call any Dart method to enable these.

What is captured, and at what level #

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 FlutterError.onError nor PlatformDispatcher.onError 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 captured #

Platform limits, not SDK limits. No crash reporter on either platform captures these.

Not captured Why
Out-of-memory kills, jetsam, watchdog terminations The OS sends SIGKILL, which no handler can intercept. The prior session is finalised exited, so crash-free rate reads slightly optimistic. There is no abnormal session status
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 relaunch #

Nothing is uploaded from inside a crash handler: the process is dying and networking is not safe there. The handler writes a small artifact to disk synchronously, and the next launch turns it into a fatal event and 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.

Device context on every event #

Collected automatically by the native SDK, with nothing to configure: device model, manufacturer and brand (Android), OS name and version, locale, timezone, whether it is a simulator or emulator, app version and build, and the package or bundle identifier. Alongside it every event carries an install id (stable per install), a session id, the release, the environment, your tags, contexts, user and the breadcrumb ring.


Symbol upload and obfuscated stack traces #

Dart obfuscation (--split-debug-info / --obfuscate) #

When you build with --obfuscate --split-debug-info=<dir>, Dart strips symbol names from the binary. The Flutter SDK detects this automatically (by checking for isolate-instruction address markers in the raw stack string) and ships the raw address-form trace as a rawStack field, which the BugWatch worker can resolve using flutter symbolize:

flutter symbolize \
  --debug-info=<dir>/app.android-arm64.symbols \
  --input=<raw-stack.txt>

Upload the --split-debug-info output to BugWatch so the server can resolve Dart stacks automatically. Dart symbols are a text artifact, not a debug-symbol archive, so use artifacts upload:

npx @newinstance/bugwatch-cli artifacts upload build/symbols.zip \
  --token <keyId>:<secret> \
  --platform flutter \
  --type dart-symbols \
  --release 1.4.2+318

Native crash symbols #

Native crash frames (iOS, Android NDK) go through the respective native symbol pipelines:

  • iOS dSYM — upload the .dSYM bundle produced by Xcode with npx @newinstance/bugwatch-cli symbols upload --platform ios ….
  • Android native .so (NDK/ELF) — zip the .so tree first, then npx @newinstance/bugwatch-cli symbols upload native-symbols.zip --platform android … (bare .so files and directories are rejected).
  • Android R8/ProGuard mapping — a text file, so it uses the artifacts command: npx @newinstance/bugwatch-cli artifacts upload mapping.txt --platform android --type r8 --release <v>.

Lifecycle #

// Drain the pending queue through the native delivery worker.
await BugWatch.instance.flush();

// Tear down the native SDK instance and restore saved error handlers.
await BugWatch.instance.close();

Call flush() before the app exits (e.g. in a dispose or shutdown hook) to ensure in-flight events are delivered.


Diagnostics #

When debug: true, the native SDK emits internal log lines. Listen to them from Dart:

BugWatch.instance.onDiagnostic.listen((event) {
  debugPrint('[BugWatch] ${event['event']}: ${event['data']}');
});

Complete examples #

Minimal #

import 'package:bugwatch/bugwatch.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await BugWatch.instance.init(const BugWatchOptions(
    projectId: 'bwp_a1b2c3d4',
    appSecret: 'your_app_secret_here',
    environment: 'production',
    release: '1.0.0+1',
  ));

  runApp(const MyApp());
}

autoCaptureErrors defaults to true, so unhandled Flutter and Dart errors are captured automatically without any additional code.


Realistic (zone guard + user + scope + breadcrumbs) #

import 'package:bugwatch/bugwatch.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await BugWatch.instance.init(const BugWatchOptions(
    projectId: 'bwp_a1b2c3d4',
    appSecret: 'your_app_secret_here',
    environment: 'production',
    release: '1.4.2+318',
    debug: false,
    sampleRate: 1.0,
  ));

  // Optional: wrap runApp in a zone guard for maximum error coverage.
  await BugWatch.runZonedGuarded(() async {
    runApp(const MyApp());
  });
}

// ─── Somewhere in your app ───────────────────────────────────────────

Future<void> onUserLoggedIn(User user) async {
  await BugWatch.instance.setUser(BugWatchUser(
    id: user.id,
    email: user.email,
    username: user.displayName,
  ));
}

Future<void> onUserLoggedOut() async {
  await BugWatch.instance.setUser(null);
}

Future<void> navigateTo(String screen) async {
  await BugWatch.instance.addBreadcrumb(Breadcrumb(
    category: 'navigation',
    message: 'Navigated to $screen',
    level: Severity.info,
  ));
  await BugWatch.instance.setTag('screen', screen);
}

Future<void> processOrder(String orderId) async {
  try {
    await BugWatch.instance.setContext('order_id', orderId);
    await submitOrder(orderId);
    await BugWatch.instance.captureMessage(
      'Order submitted: $orderId',
      level: Severity.info,
    );
  } catch (error, stack) {
    await BugWatch.instance.captureException(error, stack);
    rethrow;
  }
}

Production checklist #

  • projectId and appSecret are set from build-time environment variables or a secrets manager — not hardcoded in source.
  • environment is 'production' in your production build; use a different value (e.g. 'staging') for pre-production.
  • release matches the version string your team uses to track deployments (e.g. '1.4.2+318').
  • debug: false in production builds.
  • sampleRate: 1.0 unless you intentionally want partial coverage.
  • Android: cloud.newinstance:bugwatch:0.1.3 resolves from Maven Central or from mavenLocal during local native SDK development.
  • iOS: pod install ran successfully and the BugWatch pod resolves.
  • iOS deployment target is 14.0 or higher in ios/Podfile.
  • For obfuscated builds (--split-debug-info/--obfuscate): Dart symbol files are uploaded to BugWatch after each release.
  • For native crash symbolication: dSYM (iOS) and mapping.txt (Android) are uploaded to BugWatch after each release.

Known limitations #

Stated plainly so you can plan around them rather than discover them during an incident.

  • Delivery needs a relaunch. Crash reports upload the next time the app starts. There is no background upload job on either platform, so a user who crashes and never returns is never counted.
  • Out-of-memory kills are invisible. SIGKILL cannot be caught, so those runs finalise as exited and crash-free rate reads slightly better than reality.
  • Dart errors are never fatal. See the level table above; this is correct for Flutter, but it means a Dart error and a native crash are not comparable by level alone.
  • ANR and hang thresholds are not exposed in the Dart API. The native defaults apply (5000 ms on Android, 2000 ms on iOS).
  • Stack overflow is covered only on the thread that started the SDK. sigaltstack is registered per-thread.
  • A Flutter crash needs two sets of symbols. Dart frames need the --split-debug-info output; native frames need dSYMs on iOS and the R8 mapping plus ELF symbols on Android. Uploading only one leaves half the stack unreadable.
  • No automatic screenshot, widget tree, or profiling capture. The SDK collects crashes, errors, logs, breadcrumbs, device context and release-health sessions only.

Troubleshooting #

Pod install fails: Unable to find a specification for 'BugWatch' #

The BugWatch pod is not available from your configured CocoaPods spec repos. Add a local path override in ios/Podfile:

pod 'BugWatch', :path => '../../bug-watch-ios'

Pod install fails with a case-collision error #

The plugin's podspec is bugwatch (lowercase); the native pod is BugWatch (TitleCase). CocoaPods can treat them as the same spec and refuse. Clean and retry:

cd ios
pod deintegrate
pod cache clean --all
pod install

Android build fails: Could not find cloud.newinstance:bugwatch:0.1.3 #

Make sure mavenCentral() is present in your Android repositories. If you are testing a local native SDK build, run ./gradlew :sdk:publishToMavenLocal in the bug-watch-android directory first.

Events are not appearing in the BugWatch dashboard #

  1. Set debug: true and listen to BugWatch.instance.onDiagnostic to see internal SDK logs.
  2. Confirm enabled: true (the default).
  3. Confirm projectId and appSecret are correct (from the dashboard).
  4. Verify network connectivity — the SDK delivers to https://api.newinstance.cloud/api/v1/bugwatch/ingest/mobile.
  5. Call await BugWatch.instance.flush() to force immediate delivery (the auto-flush timer runs every 5 000 ms by default).

BugWatch.instance.init() throws a PlatformException #

The native plugin could not initialize. Common causes:

  • Android: cloud.newinstance:bugwatch was not found (see above).
  • iOS: BugWatch pod was not installed or the Podfile path is wrong.
  • projectId or appSecret is empty.

Dart stack traces are not symbolicated in the dashboard #

For obfuscated release builds (built with --obfuscate --split-debug-info), you must upload the Dart symbol file to BugWatch using the CLI. Without the upload, the worker receives raw addresses it cannot resolve.


Upgrading #

0.1.0 → 0.1.1 #

  • autoCaptureErrors option added (default true) — no code change needed unless you want to opt out.
  • BugWatch.runZonedGuarded static helper added — optional.
  • Android native dep bumped to cloud.newinstance:bugwatch:0.1.1 (adds NDK native-crash capture).


License #

MIT — see LICENSE.

0
likes
150
points
42
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

BugWatch — crash, error, and log observability for Flutter. Capture exceptions, messages, breadcrumbs, and scope from your app via native iOS/Android bridges.

Homepage

License

MIT (license)

Dependencies

flutter

More

Packages that depend on bugwatch

Packages that implement bugwatch