arm_tooling
arm_tooling is a Flutter package for low-overhead automated remote monitoring in deployed apps.
It captures handled and unhandled failures, fingerprints recurring issues, stores per-incident cases in Cloud Firestore, and can attach screenshots through Firebase Storage. It is designed for Flutter web and mobile apps that want production telemetry without introducing a custom server just for crash and incident intake.
Features
- Captures
FlutterError,PlatformDispatcher, and zone-level async failures. - Records breadcrumb history from nearby app events and
print()output. - Deduplicates related failures into stable issue IDs.
- Stores per-occurrence cases with context, tags, and optional recovery snapshots.
- Optionally uploads screenshots to Firebase Storage.
- Returns a case ID for moderate-or-higher incidents so apps can show a support reference to the user.
Documentation
Installation
Add the package to your app:
dependencies:
arm_tooling: ^0.1.0
If your app does not already use them, add the Firebase packages required by your chosen sink:
dependencies:
firebase_core: ^3.15.2
cloud_firestore: ^5.6.12
firebase_storage: ^12.4.10
Then fetch dependencies:
flutter pub get
Firebase setup
arm_tooling is intentionally storage-agnostic at the API layer, but the included FirebaseArmSink expects:
- Firebase to be initialized before use.
- Cloud Firestore to be enabled.
- Firebase Storage to be enabled only if you want screenshot uploads.
- Security rules in the consuming app that allow:
- case writes from the client
- issue-summary reads and writes for deduplication
- admin-only reads for full incident details
Recommended collections:
armIssues/{issueId}: lightweight deduplicated issue summary.armCases/{caseId}: full incident records with breadcrumbs, stack traces, snapshots, and optional screenshot metadata.
Quick start
Create a shared ArmClient:
import 'package:arm_tooling/arm_tooling.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
ArmClient createArmClient() {
return ArmClient(
sink: FirebaseArmSink(
firestore: FirebaseFirestore.instance,
storage: FirebaseStorage.instance,
),
appId: 'my_flutter_app',
environment: kReleaseMode ? 'production' : 'debug',
userIdProvider: () => FirebaseAuth.instance.currentUser?.uid,
userEmailProvider: () => FirebaseAuth.instance.currentUser?.email,
routeProvider: () => AppRouteContext.instance.currentRoute,
contextBuilder: () => AppRouteContext.instance.snapshot(),
);
}
Wrap app startup so unhandled failures are captured:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final armClient = createArmClient();
await ArmBootstrap.runGuarded(
client: armClient,
body: () async {
runApp(MyApp(armClient: armClient));
},
);
}
Expose the client through your app's dependency injection:
return Provider<ArmClient>.value(
value: armClient,
child: const MyRootView(),
);
Tracking a risky operation
Use runTracked() around writes, submissions, checkout steps, or any action where you want a case ID and recovery context if something fails:
final armClient = context.read<ArmClient>();
await armClient.runTracked<void>(
feature: 'lead_capture',
operation: 'submit_inquiry',
severity: ArmSeverity.moderate,
category: 'data_integrity',
tags: <String, dynamic>{
'projectSlug': projectSlug,
'channel': selectedChannel,
},
recoverySnapshotBuilder: () => <String, dynamic>{
'form': <String, dynamic>{
'name': nameController.text.trim(),
'email': emailController.text.trim(),
},
},
action: () async {
await repository.submitInquiry(...);
},
onReported: (result) {
if (result.caseIdExposed) {
debugPrint('Support reference: ${result.caseId}');
}
},
);
Adding screenshot capture
Wrap the part of the UI you want to capture in ArmCaptureBoundary and pass the controller's capturePng callback to runTracked() or captureException():
final boundaryController = ArmCaptureBoundaryController();
ArmCaptureBoundary(
controller: boundaryController,
child: YourScreenBody(),
);
await armClient.runTracked<void>(
feature: 'checkout',
operation: 'submit_payment',
severity: ArmSeverity.serious,
category: 'ui_failure',
screenshotCapture: boundaryController.capturePng,
action: () async {
await checkoutRepository.submit(...);
},
);
Screenshot uploads are best-effort. If Storage is not configured or the upload fails, the case is still recorded in Firestore.
Capturing handled exceptions directly
If you already have a catch block and want to record the exception explicitly:
try {
await syncJob.run();
} catch (error, stackTrace) {
final result = await armClient.captureException(
error: error,
stackTrace: stackTrace,
feature: 'background_sync',
operation: 'pull_remote_state',
severity: ArmSeverity.low,
category: 'sync',
handled: true,
);
debugPrint('ARM case: ${result.caseId}');
}
Data model
FirebaseArmSink writes:
- an issue summary document keyed by fingerprint hash
- a case document per occurrence
The issue summary is intentionally lightweight so the client can perform deduplication checks without exposing full stack traces or recovery payloads. Full diagnostic detail stays in the case document.
Example app
A publishable example lives in example/lib/main.dart. Replace the placeholder Firebase options with your own project values before running it.
Publishing notes
Before publishing, run:
dart pub publish --dry-run
Also make sure:
LICENSEmatches how you want other packages to consume this library.CHANGELOG.mdreflects the version you are publishing.homepage,repository, andissue_trackerpoint to the final public repo location.- Your package name is available on pub.dev.