forge_ops_tracker (Dart)
Dart error reporting client for a private, self-hosted ForgeOps tracker instance.
Requires Dart 3.0+. A from-scratch port of gems/forge_ops_tracker
(the Rails client) -- see that gem's README for the shared design rationale; this document only
covers what's Dart-specific.
This directory holds two packages:
forge_ops_tracker(this directory) -- the core client. Works in any Dart VM environment: server apps, CLI tools, and Flutter apps on mobile/desktop.flutter_forge_ops_tracker-- a separate, small package adding Flutter-specific automatic capture (FlutterError.onError,PlatformDispatcher.onError) on top of the core client. Kept separate specifically so a plain Dart (non-Flutter) consumer of the core package never has to pull in the Flutter SDK -- the same reasoning behind Go's Gin integration living in its own nested module elsewhere in this repo.
Installation
Not yet published to pub.flutter-io.cn -- add it as a local path dependency:
# pubspec.yaml
dependencies:
forge_ops_tracker:
path: /path/to/forge_ops/sdks/dart
# Flutter apps only:
flutter_forge_ops_tracker:
path: /path/to/forge_ops/sdks/dart/flutter_forge_ops_tracker
Dependencies
The core package has zero runtime dependencies: dart:io's HttpClient and dart:convert's
jsonEncode cover HTTP delivery and JSON encoding, the same "reach for the language's own standard
library first" choice every other client in this repo makes where its language's stdlib actually
has the needed piece.
One real platform limitation this implies: dart:io is not available on Flutter Web. This
client covers server/CLI Dart and Flutter on mobile/desktop, not Flutter Web. Extending it to Web
would mean swapping dart:io's HttpClient for package:http (or dart:html's fetch-equivalent)
behind a conditional import -- a real, known gap, not silently ignored, just out of scope for this
round.
Configuration
Set a DSN (from a project's settings page in ForgeOps), either via the FORGE_OPS_DSN environment
variable or explicitly:
import 'package:forge_ops_tracker/forge_ops_tracker.dart' as forge_ops_tracker;
forge_ops_tracker.init((config) {
config.dsn = 'https://<api_key>@your-forgeops-host/api/v1/events'; // or leave unset to read FORGE_OPS_DSN
config.release = '...';
config.environment = 'production';
});
Call init once at startup.
What gets reported automatically, and what doesn't
Plain Dart (server apps, CLI tools): wrap your entry point in runGuarded:
void main() {
forge_ops_tracker.runGuarded(() {
// your app
});
}
This is the closest Dart equivalent to Python's sys.excepthook or Go's global panic hook: it
reports any error that escapes body -- a synchronous throw, or an unawaited Future's error --
then re-throws, so the same crash still happens exactly as it would without this wrapper. Verified
directly (not assumed): both a synchronous throw and an unawaited Future's uncaught error crash a
plain Dart program by default, with no zone at all, so runGuarded genuinely changes nothing about
program behavior besides also reporting first.
Flutter apps: use the separate flutter_forge_ops_tracker package instead, which wires the two
Flutter-specific error paths runGuarded doesn't cover on its own:
import 'package:flutter_forge_ops_tracker/flutter_forge_ops_tracker.dart';
void main() {
forge_ops_tracker.init((config) {
config.dsn = 'https://<api_key>@your-forgeops-host/api/v1/events';
});
installFlutterErrorHandlers();
runApp(MyApp());
}
FlutterError.onError-- an error the framework itself catches while building, laying out, or painting a widget (what would otherwise render Flutter's own red "error" screen in debug mode).PlatformDispatcher.onError-- anything that escapes the root zone instead: an async callback's error, an uncaughtFutureerror, anything that never went through a widget build.
Both chain to whatever handler was already installed rather than replacing it, so installing this package changes nothing observable about how an error is presented besides also reporting it first -- the same "report, then don't change program behavior" rule every other client in this repo follows for its own automatic-capture path.
An error your own code catches and handles is different in both cases -- report it explicitly, right at the catch site:
try {
chargeCard(order);
} catch (error, stackTrace) {
forge_ops_tracker.captureException(error, stackTrace, {'order_id': order.id});
}
Delivery happens on an async drain loop with a bounded queue and a short per-request HTTP timeout
(Configuration.timeout, 2s default) -- see DeliveryQueue's own comment for why Dart's
single-threaded, cooperative concurrency model makes this safe without any locking, unlike the
thread-based delivery queues in most of this repo's other clients. Every failure mode -- network
errors, timeouts, a full queue, a malformed DSN -- is caught and dropped rather than thrown, so a
broken or unreachable tracker can never take down the host app.
One real limitation worth knowing: delivery is in-memory only, with no durable on-disk queue the
way the Objective-C/Swift crash reporters in this repo have. A crash reported via runGuarded or
the Flutter handlers may still terminate the isolate before that delivery finishes. A
caught-and-handled captureException call, made while the program is still healthy, doesn't have
this limitation.
in_app backtrace frames
A Dart StackTrace has no structured frame API at all -- the only thing available is
StackTrace.toString(), a multi-line format like:
#0 ChargeService.chargeCard (package:my_app/charge_service.dart:42:7)
#1 main (package:my_app/main.dart:10:3)
so this client parses that format with a regex, the same situation the Ruby/PHP/Node clients are
in for their own languages (verified directly against real caught-and-rethrown stack traces before
relying on this shape). Set Configuration.packageName (e.g. "my_app", matching your
pubspec.yaml's own name:) to mark a frame in_app when its location is
"package:<packageName>/..."; unset by default, meaning every frame reports as not-in-app until
you set it, the safe default. A dart:... location (the Dart SDK itself) and any other
package:... location (a third-party pub dependency) are never in_app, regardless of
configuration.
PII scrubbing
Same behavior as every other client in this repo: the message, backtrace, and any context you
attach are scanned for likely personal data -- email addresses, formatted SSNs/credit cards, known
API key/token formats, and anything under a suspiciously-named key (password, api_key, ssn,
and similar) -- and redacted before the payload ever leaves this process. ForgeOps itself scrubs
again on arrival regardless, so this is a second, earlier layer, not the only one.
To disable it:
forge_ops_tracker.init((config) => config.scrubPii = false);
Running the tests
cd sdks/dart
dart pub get
dart test
dart analyze
cd flutter_forge_ops_tracker
flutter pub get
flutter test
flutter analyze
Libraries
- forge_ops_tracker
- Dart error reporting client for a private, self-hosted ForgeOps tracker instance: