juice_observability 0.5.0 copy "juice_observability: ^0.5.0" to clipboard
juice_observability: ^0.5.0 copied to clipboard

Crash reporting and breadcrumbs as a Juice bloc, with global error-handler capture behind a fan-out reporter seam.

example/lib/main.dart

import 'dart:developer' as developer;

import 'package:juice/juice.dart';
import 'package:juice_observability/juice_observability.dart';

import 'telemetry_feed.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // The in-app telemetry feed — a Juice-pure consumer of the same events
  // the mirror posts to DevTools, so the demo shows the mirror working
  // without DevTools open (see telemetry_feed.dart for the self-loop guard).
  BlocScope.register<TelemetryFeedBloc>(() => TelemetryFeedBloc(),
      lifecycle: BlocLifecycle.permanent);

  // Mirror Juice's structured log entries (use-case runs + completions,
  // emissions, bloc lifecycle, leaks, errors) to the VM's extension-event
  // stream as `juice:<type>` events — live in DevTools and any VM-service
  // listener. A decorator: console logging keeps working underneath. The
  // `post` seam is injectable: here it TEES — the real VM post, plus the
  // in-app feed (filtered so the feed never consumes its own telemetry).
  JuiceLoggerConfig.configureLogger(DevtoolsJuiceLogger(
    post: (kind, data) {
      developer.postEvent(kind, data);
      if (!isSelfTelemetry(data)) {
        BlocScope.get<TelemetryFeedBloc>().append(telemetryLine(kind, data));
      }
    },
  ));

  // Console reporter so reports are visible with no backend. A real app adds a
  // Sentry/Crashlytics reporter to the list.
  BlocScope.register<ObservabilityBloc>(
    () => ObservabilityBloc.withConfig(
      ObservabilityConfig(
        reporters: [ConsoleCrashReporter()],
        maxBreadcrumbs: 10,
      ),
    ),
    lifecycle: BlocLifecycle.permanent,
  );

  runApp(const DemoApp());
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'juice_observability demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
        useMaterial3: true,
      ),
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessJuiceWidget<ObservabilityBloc> {
  HomeScreen({super.key}) : super(groups: {ObservabilityGroups.status});

  @override
  Widget onBuild(BuildContext context, StreamStatus status) {
    final s = bloc.state;
    return Scaffold(
      appBar: AppBar(title: const Text('juice_observability demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('${s.errorCount} errors · ${s.breadcrumbs.length} breadcrumbs'),
            if (s.lastError != null)
              Padding(
                padding: const EdgeInsets.all(8),
                child: Text('last: ${s.lastError}',
                    style: const TextStyle(color: Colors.red)),
              ),
            const SizedBox(height: 24),
            OutlinedButton(
              onPressed: () =>
                  bloc.breadcrumb('tapped at ${s.breadcrumbs.length}', category: 'ui'),
              child: const Text('Drop a breadcrumb'),
            ),
            const SizedBox(height: 8),
            FilledButton(
              onPressed: () =>
                  bloc.recordError(StateError('demo error'), StackTrace.current),
              child: const Text('Record an error'),
            ),
            const SizedBox(height: 8),
            // An *uncaught* error — the installed FlutterError/PlatformDispatcher
            // handlers capture this automatically.
            TextButton(
              onPressed: () => Future<void>.error(StateError('uncaught async')),
              child: const Text('Throw uncaught (auto-captured)'),
            ),
            const SizedBox(height: 24),
            // Every tap above runs a use case; its start/end pair — same
            // executionId, elapsed on the end — lands here within a frame.
            Expanded(child: TelemetryPanel()),
          ],
        ),
      ),
    );
  }
}

/// The rolling feed of `juice:<type>` events, newest at the bottom.
class TelemetryPanel extends StatelessJuiceWidget<TelemetryFeedBloc> {
  TelemetryPanel({super.key}) : super(groups: TelemetryFeedGroups.all);

  @override
  Widget onBuild(BuildContext context, StreamStatus status) {
    final lines = bloc.state.lines;
    return Container(
      width: double.infinity,
      margin: const EdgeInsets.symmetric(horizontal: 16),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('DevTools mirror — live juice: events',
              style: Theme.of(context).textTheme.labelSmall),
          const SizedBox(height: 6),
          Expanded(
            child: lines.isEmpty
                ? const Text('tap a button above')
                : ListView(
                    children: [
                      for (final l in lines)
                        Text(l,
                            style: const TextStyle(
                                fontFamily: 'monospace', fontSize: 11)),
                    ],
                  ),
          ),
        ],
      ),
    );
  }
}
0
likes
150
points
148
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Crash reporting and breadcrumbs as a Juice bloc, with global error-handler capture behind a fan-out reporter seam.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#crash-reporting #observability #sentry #bloc #state-management

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter, juice

More

Packages that depend on juice_observability