Skip to content
Back to archipelago.banuacoder.com

Infrastructure SDKs

monitoring_sdk Free ​

Tier
Freeversion 2.0.7
Command
archipelago generate monitoring_sdk
Generates
  • infrastructure/monitoring_api
  • infrastructure/monitoring_impl
  • infrastructure/monitoring_noop
Generates first
shared_consentwhen needed and missing, so there is nothing to install first
Depends on
app_utilitiesgenerate these first

Vendor-agnostic crash reporting and performance-monitoring abstraction (AppMonitoring) with consent gating, PII scrubbing, and a system-logger implementation. It ships in release builds; monitoring_noop exists for tests or an explicit opt-out. Vendors (Crashlytics, Sentry, Grafana, New Relic) are separate bricks that plug into the fanout.

Variables ​

VariableTypeDefaultDescription
appNamestring—The name of your application
isForMonorepobooleantrueWhether this is being generated as part of a monorepo

monitoringVendors (the multi-select that decides which vendor bricks get generated) is not a variable of this brick — it's asked once during flutter_modular_monorepo generation, and each selected vendor is generated as its own independent brick (e.g. crashlytics_monitoring_impl).

Usage ​

Interactive ​

bash
archipelago generate monitoring_sdk

Non-interactive (CI) ​

bash
archipelago generate monitoring_sdk --config my_config.json

Generated Structure ​

text
infrastructure/
├── monitoring_api/
│   └── lib/src/
│       ├── monitoring/
│       │   ├── app_monitoring.dart                 # AppMonitoring interface
│       │   ├── app_monitoring_vendor.dart          # AppMonitoringVendor interface
│       │   ├── flutter_aware_monitoring_vendor.dart
│       │   └── monitoring_level.dart               # debug/info/warning/error/fatal
│       └── utils/
│           └── monitoring_injector_key.dart        # @Named keys per vendor
├── monitoring_impl/
│   └── lib/src/
│       ├── monitoring_fanout_impl.dart             # Fans out to all registered vendors
│       ├── consent_aware_monitoring.dart           # Consent gate + PII scrubber decorator
│       └── di/
│           ├── injector.dart / injector.module.dart
│           └── register_module.dart                # Collects installed vendors by @Named key
└── monitoring_noop/
    └── lib/src/
        ├── monitoring_fanout_impl.dart             # Inert stub (tests / opt-out)
        └── di/injector.dart

API ​

dart
abstract interface class AppMonitoring {
  bool get isEnabled;
  Future<void> initialize();
  Future<void> dispose();
  Future<void> setUserContext(String userId, {Map<String, dynamic>? extras});
  Future<void> clearUserContext();
  void log(String message, {String? tag, MonitoringLevel level = MonitoringLevel.debug});
  Future<void> recordError(Object exception, StackTrace? stackTrace, {bool fatal = false, Map<String, dynamic>? context});
  Future<T> recordTransaction<T>(String name, Future<T> Function() action, {String? operation, Map<String, dynamic>? data});
  void breadcrumb(String message, {String? category, Map<String, dynamic>? data, MonitoringLevel level = MonitoringLevel.debug});
}
dart
final monitoring = getIt<AppMonitoring>();
await monitoring.initialize();

monitoring.log('Payment started', tag: 'PAY', level: MonitoringLevel.info);

try {
  await processPayment();
} catch (e, stack) {
  await monitoring.recordError(e, stack, fatal: false);
}

final result = await monitoring.recordTransaction(
  'checkout',
  () => processCheckout(),
  operation: 'http.post',
  data: {'items': 3},
);

Each vendor implements a second, internal contract so the fanout can dispatch to it:

dart
abstract interface class AppMonitoringVendor {
  String get vendorName;
  bool get isEnabled;
  Future<void> initialize();
  Future<void> dispose();
  Future<void> setUserContext(String userId, {Map<String, dynamic>? extras});
  Future<void> clearUserContext();
  void log(String message, {String? tag, MonitoringLevel level = MonitoringLevel.debug});
  Future<void> recordError(Object exception, StackTrace? stackTrace, {bool fatal = false, Map<String, dynamic>? context});
  Future<T> recordTransaction<T>(String name, Future<T> Function() action, {String? operation, Map<String, dynamic>? data});
  void breadcrumb(String message, {String? category, Map<String, dynamic>? data, MonitoringLevel level = MonitoringLevel.debug});
}

A vendor that holds data of its own (buffers, offline caches, a collection switch) can also implement ConsentWithdrawalAware, whose single method onConsentWithdrawn() must stop collecting and discard what is buffered without sending it. Vendors without it get dispose() instead, which may flush.

A vendor that implements FlutterAwareMonitoringVendor in addition (Crashlytics does) gets FlutterError.onError routed to its dedicated recordFlutterError API instead of the generic recordError.

getIt<AppMonitoring>() never resolves MonitoringFanoutImpl directly — it resolves ConsentAwareMonitoring, a decorator registered @LazySingleton(as: AppMonitoring) that wraps the fanout:

  • log, breadcrumb, recordError, setUserContext, recordTransaction all require ConsentPurpose.diagnostics, checked against the ambient ConsentGateRegistry on every call (not injected once, so a runtime consent change takes effect immediately).
  • Denied consent does not suppress the work being measured: recordTransaction still runs and returns action()'s result when consent is denied — only the vendor-side report is skipped.
  • clearUserContext and dispose are never gated — both reduce vendor-held data.
  • initialize() defers actually starting the vendors until consent is granted, listening for the consent state to flip, so a vendor SDK never starts collecting before consent exists.
  • The decorator keeps listening after the first grant. When diagnostics is withdrawn it calls onConsentWithdrawn() on every enabled vendor (or dispose() for a vendor without the hook), so vendors stop collecting and drop what they buffered. A later grant runs initialize() again.
  • Withdrawal per vendor: Crashlytics disables collection, calls deleteUnsentReports() and clears the user id. Grafana stops its timers and clears the log and trace buffers unsent. Sentry calls Sentry.close(), which stops capturing and sending; Sentry has no API to discard envelopes already cached on disk. New Relic calls shutDown(), which stops the agent; the native agent may not restart before the next app launch.
  • recordTransaction keeps a duration_ms you pass in data (launch phase spans rely on this) and measures one only when it is missing. Failures reach vendors as the error type only, not the message.
  • Exception messages are scrubbed by the ambient TelemetryScrubberRegistry before leaving the device; if anything was redacted, the vendor sees a ScrubbedTelemetryError (original runtime type preserved, message redacted) instead of the raw exception.

Release builds ​

Since 1.2.0, monitoring_impl ships in release builds. Crash reporting matters most in production, and ConsentAwareMonitoring is the privacy control: nothing reaches a vendor until the user grants ConsentPurpose.diagnostics. flutter_modular_monorepo's build_prepare.yaml has no monitoring mapping. Debug-only tooling (app_debugger, app_blackbox) still swaps to noop. The app launch tracker is not swapped either: it ships in release and its spans pass through ConsentAwareMonitoring.

To opt out and strip monitoring from release anyway, add this mapping by hand:

yaml
build_prepare:
  mappings:
    - debug: monitoring_impl
      release: monitoring_noop

Fanout and global error handlers ​

The fanout installs no global handlers. bootstrap.dart owns FlutterError.onError and PlatformDispatcher.instance.onError (installGlobalErrorHandlers()): it drops localization-delegate errors, calls safeReportFatalCrash, then reports through the AppMonitoring binding, so ConsentAwareMonitoring gates every uncaught error. A framework error goes to AppMonitoring.recordFlutterError(details), which the fanout sends to every enabled vendor once: recordFlutterError for a FlutterAwareMonitoringVendor, recordError(..., fatal: true) for the others. Earlier versions installed the handlers in MonitoringFanoutImpl.initialize(), which replaced bootstrap's handlers and bypassed consent. One vendor throwing during fanout is caught and logged (debugPrint) rather than breaking the others.

Dependency injection ​

MonitoringRegisterModule.monitoringVendors builds the vendor list the fanout dispatches to:

dart
@lazySingleton
List<AppMonitoringVendor> get monitoringVendors {
  final getIt = GetIt.instance;
  final vendors = <AppMonitoringVendor>[];
  for (final name in const [
    MonitoringInjectorKey.crashlytics,
    MonitoringInjectorKey.sentry,
    MonitoringInjectorKey.grafana,
    MonitoringInjectorKey.newrelic,
  ]) {
    if (!getIt.isRegistered<AppMonitoringVendor>(instanceName: name)) {
      continue;
    }
    try {
      vendors.add(getIt<AppMonitoringVendor>(instanceName: name));
    } catch (error) {
      debugPrint('[monitoring] vendor "$name" disabled: $error');
    }
  }
  return vendors;
}

A vendor that cannot be built (for example Crashlytics before flutterfire configure, so no Firebase app exists) is skipped with a debug message, not fatal. It checks isRegistered per known @Named key rather than getIt.getAll<AppMonitoringVendor>() — the pinned get_it version throws a StateError from getAll<T>() when no registration of T exists at all, which is exactly the default (monitoringVendors: []) case. A project with zero, one, or all four vendor bricks installed all work correctly.

monitoring_impl's pubspec.yaml declares its archipelago: manifest so monorepo_toolkit generate host-registry can regenerate the host injector's marker regions:

yaml
archipelago:
  di_modules:
    - class: MonitoringImplPackageModule

post_gen.dart wires MonitoringImplPackageModule into the app's lib/di/injector.dart and adds the monitoring_impl dependency to the app's pubspec.yaml. monitoring_noop is never wired directly. It declares the same names as the impl, MonitoringImplPackageModule and ConsentAwareMonitoring (the class bound as AppMonitoring), so a build_prepare swap only has to change the import path.

AppMonitoring is resolved eagerly by starter features such as auth_sdk whenever isUsingMonitoring: true, so both the impl and noop registrations must always produce a real (even if inert) AppMonitoring instance — neither ever leaves it unregistered.

Testing ​

monitoring_impl needs no test prerequisite (see test_prereq). It does no platform I/O during DI init. Under plain flutter test no Firebase app exists, so FirebaseCrashlytics.instance throws [core/no-app] when the Crashlytics vendor is built. monitoringVendors catches that and skips the vendor, so getIt<AppMonitoring>() and initializeInjector() still complete. The vendor is simply absent in tests. test/src/di/monitoring_impl_package_module_test.dart covers this case ("a vendor that throws on construction is skipped, not fatal").

If a test needs a working Crashlytics vendor, fake Firebase in that test with setupFirebaseCoreMocks() from package:firebase_core_platform_interface/test.dart, then await Firebase.initializeApp().

Dependencies ​

  • consent_api — ConsentGateRegistry / TelemetryScrubberRegistry ambient lookups for the consent decorator. pre_gen generates it (the shared_consent brick) when the project lacks it
  • app_utilities, dependencies (shared workspace packages)
  • Vendor bricks (crashlytics_monitoring_impl, sentry_monitoring_impl, grafana_monitoring_impl, newrelic_monitoring_impl) are optional, separately-generated add-ons — this brick never depends on them, they depend on it

Known Limitations ​

  • No vendor implementation ships with this brick — without at least one vendor brick installed, AppMonitoring is fully functional but has nothing to report to (isEnabled is false, every call is a no-op fanout over an empty vendor list).
  • Consent gating requires a ConsentInitializer to have installed the ambient ConsentGateRegistry/TelemetryScrubberRegistry during bootstrap; until that happens, ConsentAwareMonitoring reads whatever default the registry singleton starts with.

Changelog ​

Run archipelago brick info monitoring_sdk to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.