observatory 3.0.1
observatory: ^3.0.1 copied to clipboard
Isolate-aware Flutter logs, bounded history, safe HTTP logging and Sentry incident capture.
observatory #

Isolate-aware Flutter logs, bounded history, safe HTTP logging and Sentry incident capture.
Every connected source uses the same thread(zoneName): message format in console output,
the log screen, exported text and breadcrumbs. Multiline entries prefix every line.
Start an isolate #
dependencies:
observatory: ^3.0.0
Future<void> main() => Observatory.run<void>(
config: const Config(),
zoneName: 'main',
body: () {
final dio = Dio();
Observatory.attachTo(dio);
runApp(
ObservatoryWidget(
child: MaterialApp(
navigatorObservers: Observatory.navigatorObservers,
home: const HomePage(),
),
),
);
},
);
Observatory creates the Flutter binding and runs body in the same zone. Call it before
creating a binding yourself. isStarted means local logging is ready; isSentryEnabled
reports whether remote capture initialized successfully. Failed Sentry initialization
leaves local logging available and still runs the application.
Observatory.run initializes launch_mode automatically. Native and web root isolates use
foreground; computational isolates use isolate. The detected mode remains the external
thread value in logs and Sentry data.
The result of body becomes the result of run. An exception in body is captured and
returned with its original stack. The runtime stays active after body returns, including
a runApp call. A second or concurrent run in the same isolate fails with StateError.
Log sources and zones #

Observatory.record(LogLevel.info, 'Session restored');
Observatory.talker.info('Direct Talker call');
print('Console message');
debugPrint('Flutter diagnostic');
await Observatory.runInZone('profile', () async {
await loadProfile();
Observatory.record(LogLevel.info, 'Profile loaded');
});
print and debugPrint become info records, with console output, history and eligibility
for breadcrumbs. They do not create incidents. Context is captured when a log is received,
including across asynchronous continuations. Their own console output is not recaptured.
Zone names are trimmed, empty names become unspecified, and names are limited to 20 characters.
Direct calls to the managed Talker outside the runtime zone use the isolate's startup context.
Pass Observatory.talker to third-party Talker integrations. Standard methods, handle
and logCustom use the common preparation path. Observatory owns the logger, history and
error preparation; replacing those with talker.configure is unsupported. Observers can still be configured; use Config.filter for filtering. Console output can be disabled with
talker.configure(settings: TalkerSettings(useConsoleLogs: false)).
Each background isolate needs its own entry point:
Future<void> worker() => LaunchMode.withBackgroundMode(
() => Observatory.run<void>(
config: const Config(),
zoneName: 'downloads',
body: () async {
print('Worker ready');
await downloadFiles();
},
),
);
Wrap a background root engine handler with LaunchMode.withBackgroundMode. The scoped mode
survives await without changing neighboring foreground work. runInZone changes only
zoneName.
print interception covers the runtime zone and its descendants. Logs before startup,
other Talker instances, direct stdout, arbitrary dart:developer.log calls and native
platform logs are outside that interception.
Incidents and Sentry #

try {
await syncProfile();
} on Object catch (error, stack) {
await Observatory.capture(
LogLevel.error,
'Profile sync failed',
error: error,
stackTrace: stack,
);
}
await Observatory.capture(LogLevel.warning, 'Profile is incomplete');
record writes an ordinary log. capture writes one local incident and submits a Sentry
event when remote capture is enabled. An error is optional. Messages, timestamps, levels,
isolate context and exception chains are retained. Derived error locations are added as
context without replacing exception types, values or the transaction name.
Configure Config.sentry with SentrySpec; the complete example is in
example/observatory_example.dart. Sentry is disabled by
default. Observatory owns its Sentry initialization; do not initialize a second Sentry
runtime in the same isolate. SDK screenshot masking defaults remain enabled.
Dedupe uses error identity, location and operation message within dedupeTtl, bounded by
dedupeMaxEntries. Message-only events also include isolate context. SDK object-identity
deduplication is disabled. An SDK call completing does not guarantee server acceptance.
await Observatory.bindUser(id: user.id, email: user.email);
await Observatory.bindDevice(connectedDeviceId: device.id, platformDeviceId: platformId);
await Observatory.clearUser();
await Observatory.clearDevice();
Privacy, filters and limits #
Config.redaction controls the common RedactionPolicy. Sensitive headers, query parameters,
URI credentials, JSON and FormData fields are masked before storage. The same policy cleans
Sentry requests, breadcrumbs, contexts, extra data and spans. Original HTTP objects remain
unchanged. Recognizable key/value text is scrubbed; arbitrary free text cannot be guaranteed
free of unknown secrets. RedactionPolicy.disabled() is an explicit opt-out from masking.
HttpLogSpec() omits headers and bodies; HttpLogSpec.detailed() includes sanitized values.
attachTo is idempotent for each Dio. Bloc errors are captured once; other Bloc callbacks
and navigation use the common history.
Configure bloc_effects logging through the same Talker:
Config(
blocEffects: TalkerBlocEffectsSettings(
printEffectFullData: false,
effectFilter: (bloc, effect) => effect is! HeartbeatEffect,
),
)
Effects and full data are enabled by default. excludedBlocTypes runs first for Bloc/Cubit
sources, followed by effectFilter, then message formatting. Effect records use the
bloc-effect key and retain the originating launch mode and zone. Create Bloc, Cubit and
standalone effect sources after Observatory.run installs its observer.
ObservationFilter filters ordinary records by message substring, HTTP URL regular expression
and exact Bloc type name. It does not suppress explicit or uncaught incidents. Bloc name
filters must account for obfuscation in the host application.
Config.historyLimit: 1000 records by default; zero disables retention, negative is invalid.- Formatting: maximum depth 8, 1000 visited elements, 16,384 characters per rendered record. Cycles and truncation have explicit markers. These bounds also apply when masking is disabled.
- Breadcrumbs: filter by
logsLevel, then takelogsMaxBreadcrumbs; combined SDK and history breadcrumbs are capped bymaxBreadcrumbs. Collection reads do not expose mutable payloads. - Logging failures use a guarded console fallback and do not block HTTP processing or a separate attempt to capture the incident remotely.
Log screen and shutdown #
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => const ObservatoryLogScreen(appBarTitle: 'Logs'),
));
await Observatory.close();
close waits for pending capture operations, closes the owned Sentry runtime, detaches Dio
wrappers, clears history and restores previous Flutter/platform/debugPrint/Bloc handlers.
Handlers replaced by the host after startup are preserved. The host still owns its Dio
clients. Close the runtime when an isolate's work is complete or during test cleanup;
a later run can initialize it again.
Migrate from 2.x #
- Remove the
threadargument fromObservatory.run; launch mode is detected automatically. - Replace
ObservatoryThreadandIsolateContext.threadreferences withLaunchModeTypeandIsolateContext.launchMode. - Wrap background root engine handlers with
LaunchMode.withBackgroundMode. - Configure effect logging with
Config.blocEffects;Confignow extendsPackageConfig.
Migrate from 1.x #
- Replace
initPackage,Observatory.startandObservatory.runZonedwithObservatory.run. - Remove
Dependencies(talker: ...)and obtain the shared instance fromObservatory.talker. - Move
HttpLogSpec.redactiontoConfig.redaction. - Replace
disabledBlocLogswith exact names inObservationFilter.excludedBlocTypes. - Use
ObservatoryLogScreenandObservatoryWidgetinstead of facade widget builders. - Replace test resets with
await Observatory.close(); initialize every background isolate.
Development #
make get
make analyze
make format-check
make test
make publish-dry-run
CI uses Flutter 3.47.2 and runs these checks on pushes and pull requests. Sentry tests use a local transport; they do not contact a Sentry server or establish physical-device behavior.
License: MIT.