bugwatch 0.1.4
bugwatch: ^0.1.4 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
BugWatchCocoaPod (bug-watch-ios) - Android: the
cloud.newinstance:bugwatchMaven 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.1. 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.1 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).
Breadcrumbs #
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: Mach exception + UNIX signal handlers with binary-image collection for dSYM symbolication; on Android: JVM uncaught exception handler + NDK C++ signal handler (4 ABIs) for ProGuard/R8 symbolication.
- 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
BugWatchOptionsfields passed from the native option objects, not currently exposed inBugWatchOptionsDart 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.
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
.dSYMbundle produced by Xcode withnpx @newinstance/bugwatch-cli symbols upload --platform ios …. - Android native
.so(NDK/ELF) — zip the.sotree first, thennpx @newinstance/bugwatch-cli symbols upload native-symbols.zip --platform android …(bare.sofiles 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 #
projectIdandappSecretare set from build-time environment variables or a secrets manager — not hardcoded in source.environmentis'production'in your production build; use a different value (e.g.'staging') for pre-production.releasematches the version string your team uses to track deployments (e.g.'1.4.2+318').debug: falsein production builds.sampleRate: 1.0unless you intentionally want partial coverage.- Android:
cloud.newinstance:bugwatch:0.1.1resolves from Maven Central or frommavenLocalduring local native SDK development. - iOS:
pod installran successfully and theBugWatchpod resolves. - iOS deployment target is
14.0or higher inios/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.
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.1 #
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 #
- Set
debug: trueand listen toBugWatch.instance.onDiagnosticto see internal SDK logs. - Confirm
enabled: true(the default). - Confirm
projectIdandappSecretare correct (from the dashboard). - Verify network connectivity — the SDK delivers to
https://api.newinstance.cloud/api/v1/bugwatch/ingest/mobile. - 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:bugwatchwas not found (see above). - iOS:
BugWatchpod was not installed or the Podfile path is wrong. projectIdorappSecretis 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 #
autoCaptureErrorsoption added (defaulttrue) — no code change needed unless you want to opt out.BugWatch.runZonedGuardedstatic helper added — optional.- Android native dep bumped to
cloud.newinstance:bugwatch:0.1.1(adds NDK native-crash capture).
Links #
- BugWatch dashboard
- REST API reference / Swagger
- BugWatch CLI (
@newinstance/bugwatch-cli) - BugWatch iOS SDK (
bug-watch-ios) - BugWatch Android SDK (
bug-watch-android)
License #
MIT — see LICENSE.