# 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.

## Requirements

- 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. Install

```bash
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`:

```ruby
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)

Get `projectId` and `appSecret` from the dashboard: your BugWatch project, then **Mobile credentials**, then *Reveal* (see the [Mobile](/mobile) overview for how the on-device signing works).

```dart
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. Configuration

| 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 API

```dart
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 capture

- 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)

```bash
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](/android) and [iOS & macOS](/ios-macos) pages).

## 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 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 captured

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 relaunch

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.

## Upgrading

- 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`.

## Troubleshooting

- **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`.
