errorgap (Flutter)

Flutter/Dart notifier for Errorgap. Reports errors, inline Dart source, APM transactions/jobs, query and HTTP spans, and structured logs. Flutter applications wire FlutterError.onError (framework-level Dart errors) and PlatformDispatcher.instance.onError (uncaught zone errors). Native crashes (Android JNI, iOS Objective-C) are out of scope for v1; pair with the native SDKs (errorgap-android, errorgap-swift) until a Pigeon bridge ships.

Requires Dart 3.0+.

Install

dependencies:
  errorgap: ^0.4.0

Configure

import 'package:errorgap/errorgap.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'dart:ui';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  Errorgap.init(ErrorgapConfiguration(
    endpoint:    'https://errorgap.example.com',
    projectSlug: 'your-project',
    apiKey:      const String.fromEnvironment('ERRORGAP_API_KEY'),
    environment: kReleaseMode ? 'production' : 'development',
    release:     const String.fromEnvironment('APP_VERSION'),
    apmEnabled:  true,
    logsEnabled: true,
    deviceInfo: <String, Object?>{
      'os_name': defaultTargetPlatform.toString(),
    },
  ));

  FlutterError.onError = (FlutterErrorDetails details) {
    Errorgap.notify(details.exception, stackTrace: details.stack);
    FlutterError.presentError(details);
  };

  PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
    Errorgap.notify(error, stackTrace: stack);
    return true;
  };

  runApp(const MyApp());
}

Manual notification

try {
  await risky();
} catch (error, stack) {
  await Errorgap.notify(error, stackTrace: stack, context: <String, Object?>{
    'component': 'checkout',
  });
  rethrow;
}

notify returns a DeliveryResult (status, body, error, queued). The SDK never throws.

Source excerpts

The SDK reads a bounded source window for file-backed Dart VM frames. Package URIs need an explicit source-root mapping so both application and dependency frames can ship source:

ErrorgapConfiguration(
  projectSlug: 'your-project',
  rootDirectory: '/app',
  applicationPackages: const ['my_flutter_app'],
  packageSourceRoots: const {
    'my_flutter_app': '/app/lib',
    'inventory_client': '/app/vendor/inventory_client/lib',
  },
);

Each excerpt is limited to six lines before and after the target and 400 characters per line. Source reading degrades safely on Flutter web and on mobile builds where original Dart sources are not deployed.

APM and jobs

await Errorgap.notifyTransaction(ErrorgapTransaction(
  method: 'POST',
  path: '/orders/{orderId}',
  pathRaw: '/orders/42',
  statusCode: 201,
  durationMs: 125,
  spans: [
    ErrorgapSpan.database(
      "SELECT * FROM orders WHERE id = 42",
      durationMs: 12,
      file: 'lib/orders.dart',
      line: 30,
      function: 'Orders.load',
    ),
    ErrorgapSpan.external(durationMs: 18),
  ],
));

await Errorgap.trackJob('ReceiptJob', (spans) async {
  spans.database('SELECT 1 FROM receipts WHERE id = 42', durationMs: 8);
  await generateReceipt();
}, queue: 'default');

trackJob reports a failed operation as both an error and a failed job transaction, then rethrows it to preserve application behavior. SQL literals are normalized to ? so equivalent queries aggregate together.

Trace a call to your API and errorgap links it to the server request that answered it (when the server's errorgap SDK records the x-errorgap-trace header — Rails, Laravel, Express, Django, Spring and the rest do):

final spans = ErrorgapSpanCollector();
final stopwatch = Stopwatch()..start();
final response = await spans.traceCall('GET /api/orders/7',
    (headers) => http.get(ordersUri, headers: headers));
await client.notifyTransaction(ErrorgapTransaction(
  path: 'OrderScreen',
  durationMs: stopwatch.elapsedMicroseconds / 1000,
  spans: spans.snapshot(),
));

traceCall records an http span carrying the trace id it sent; the app's trace lists each traced call with a link to its server trace, and the server trace shows how long the app waited. For manual timing use final call = spans.startCall(label), send call.headers, then call.finish().

Every ErrorgapTransaction has an id. Errors reported inside withErrorgapTransaction(id, () async { ... }) — and a failing trackJob — carry it as context.transaction_id, so errorgap shows the error an interaction raised on its trace:

final transaction = ErrorgapTransaction(path: '/checkout', durationMs: 0);
await withErrorgapTransaction(transaction.id, () => submitOrder());

The id is a zone value: async work started inside keeps it, and concurrent work never shares it.

Structured logs

final logger = Errorgap.logger(source: 'flutter.checkout');
await logger?.warning('payment gateway timeout');
await logger?.error('checkout failed');

Logs are disabled by default. Set logsEnabled: true and optionally minimumLogLevel (trace, debug, info, warn, error, or fatal).

Session replay

Session replay records a timeline of each app session, shown on error groups and incidents in Errorgap. It is timeline only: no screenshots and no widget snapshots.

Event Recorded when
page_load The app starts ("App started"), or a new session starts after 30 minutes in the background ("App opened")
navigation A screen is tracked, or the app returns from the background
api_error A call through ErrorgapHttpClient fails or answers 5xx
error An error is reported, linked to its error group
exit The app goes to the background

It is on by default in iOS and Android apps (off elsewhere, such as a Dart server; pass replay: true to record there), and records only what the project allows in Settings → Session replay: nothing while replay is off, and a sampled share of sessions while it is on.

Only sessions that hit an error are kept. Until an error is reported, the session's most recent 300 events are held in memory and nothing is sent; then everything held goes up with the error and the rest of the session follows. A session that ends without an error is never sent.

This package does not depend on Flutter, so wire the screens and the app lifecycle in your app:

import 'package:flutter/widgets.dart';
import 'package:errorgap/errorgap.dart';

class ErrorgapNavigatorObserver extends NavigatorObserver {
  void _track(Route<dynamic>? route) {
    final name = route?.settings.name;
    if (name != null) Errorgap.trackScreen(name);
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _track(route);
  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _track(previousRoute);
  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => _track(newRoute);
}

// In your root widget's State:
late final AppLifecycleListener _lifecycle = AppLifecycleListener(
  onHide: Errorgap.appPaused,
  onShow: Errorgap.appResumed,
);

MaterialApp(navigatorObservers: [ErrorgapNavigatorObserver()], /* ... */);

Send your API calls through ErrorgapHttpClient to record the ones that fail:

final api = ErrorgapHttpClient(http.Client());

Options: Errorgap.init(config, replay: false) turns replay off, and replayOptions: ErrorgapReplayOptions(appName: 'Acme', device: 'Tablet') names the app and device class in the session list.

Configuration reference

Field Default Notes
endpoint ERRORGAP_ENDPOINT or http://127.0.0.1:3030
projectSlug ERRORGAP_PROJECT_SLUG Required
projectId ERRORGAP_PROJECT_ID
apiKey ERRORGAP_API_KEY Sent as x-errorgap-project-key
environment ERRORGAP_ENVIRONMENT or production
release —
rootDirectory current directory on Dart VM Source path base
async true Background queue + worker
filterKeys [password, token, ...] Substring, case-insensitive
ignoreEnvironments ERRORGAP_IGNORE_ENVIRONMENTS Comma-separated when read from env
apmEnabled false Enables transaction delivery
apmSampleRate 1.0 Range 0.0...1.0
logsEnabled false Enables structured log delivery
minimumLogLevel info Lowest forwarded log level
sourceContextEnabled true Reads inline source when available
timeoutSeconds 5 HTTP request timeout
queueSize 100 Bounded queue
deviceInfo {} Caller-supplied device fingerprint
applicationPackages [] Packages classified as application code
packageSourceRoots {} Package URI to local source mappings

Graceful shutdown

await Errorgap.flush();
await Errorgap.shutdown();

Verify the ingestion path

curl -sS -X POST "$ERRORGAP_ENDPOINT/api/projects/$ERRORGAP_PROJECT_SLUG/notices" \
  -H "content-type: application/json" \
  -H "x-errorgap-project-key: $ERRORGAP_API_KEY" \
  -d '{"errors":[{"type":"ErrorgapInstallTest","message":"Errorgap install verification"}],"context":{"environment":"development"}}'

Expect HTTP 201 with a JSON body containing group_id.

Development

dart pub get
dart test

License

MIT.

Libraries

errorgap
Flutter / Dart notifier for Errorgap.