pulse_ops

pub version pub points pub likes pub popularity

CI GitHub stars GitHub issues GitHub last commit PRs welcome Sponsor

Flutter โ‰ฅ3.10 Dart โ‰ฅ3.0 platforms license: MIT

A modern, Flutter-native developer toolkit for in-app network inspection, performance monitoring, and crash diagnostics โ€” designed as a lightweight alternative to Chucker / Pulse / Stetho, with a beautiful dark Material 3 UI.

PulseOps in action โ€” floating launcher, expandable inspector, cURL export, and log sharing

PulseOps ships with three focused capabilities:

  1. ๐ŸŒ Network Inspector โ€” a Dio interceptor that records every request, pretty-prints JSON, exports cURL, retries calls, and presents it all in a developer-grade dark inspector.
  2. โšก Performance Monitoring โ€” real-time FPS tracking, frame drop & jank detection, startup time measurement, and API latency charts โ€” all with zero extra dependencies.
  3. ๐Ÿ’ฅ Crash Diagnostics โ€” pluggable bridge to Firebase Crashlytics (or any backend) with rich breadcrumbs and automatic attachment of recent API activity to every crash report.

โœจ Highlights

  • ๐ŸŽจ Beautiful dark, Material 3 inspector with monospace JSON viewer and syntax highlighting
  • ๐Ÿ”Œ One-line Dio integration โ€” works with GET, POST, PUT, PATCH, DELETE, and multipart/form-data
  • ๐Ÿ” Search, filter by method / status family / slow / failed โ€” live and composable
  • ๐Ÿ“‹ Copy buttons everywhere โ€” headers, body, full cURL
  • โ†ป Retry requests from the inspector with your real Dio client
  • โšก Real-time FPS monitor โ€” sparkline chart, dropped-frame list, startup time, and API latency bar chart in one screen
  • ๐Ÿ“ณ Shake to open โ€” shake the device to launch the inspector without touching the overlay
  • ๐Ÿ”’ Sanitization for secrets / tokens / passwords before storage or upload
  • ๐Ÿงญ Breadcrumb trail with bounded ring buffer
  • ๐Ÿ’ฅ Backend-agnostic crash reporter โ€” wire Crashlytics, Sentry, or your own logger via a thin PulseCrashReporter interface
  • ๐Ÿ›ก๏ธ Production-safe โ€” disabled in release builds by default
  • ๐Ÿชถ Lightweight โ€” no Firebase or Isar at runtime; pure Dart + Dio + Riverpod core

๐Ÿš€ Quick start

1. Add the dependency

dependencies:
  pulse_ops: ^1.2.0
  dio: ^5.4.0

2. Initialize in main()

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:pulse_ops/pulse_ops.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await PulseOps.initialize(
    crashlytics: true,
    enableInRelease: false,
    sanitizeKeys: ['token', 'password', 'authorization'],
  );

  final dio = Dio()..interceptors.add(PulseOps.instance.dioInterceptor);

  runApp(PulseOps.instance.wrap(retryDio: dio, child: const MyApp()));
}

That's it. A draggable floating button appears in debug builds; tap it to open the inspector.

3. (Optional) Open the inspector imperatively

PulseOps.instance.openInspector(context, retryDio: dio);

๐ŸŒ Network Inspector

Every Dio call routed through PulseOps.instance.dioInterceptor is captured as a NetworkRecord and pushed into an in-memory ring buffer (configurable via PulseOpsConfig.maxRecords).

The inspector UI provides:

Surface Contents
Timeline list Newest-first list of requests with method chip, host, path, timestamp, duration, status chip
Overview tab Status, timing, request/response sizes, error details
Headers tab Sanitized request + response headers with copy-all
Request tab Query params and request body with syntax-highlighted JSON
Response tab Highlighted response body and error banner
cURL tab One-tap copy of the full curl command

The list supports:

  • ๐Ÿ”Ž Live search across URL, method, status, host, and error message
  • ๐ŸŽฏ Filter by HTTP method (GET / POST / PUT / PATCH / DELETE)
  • โš ๏ธ Failed only toggle โ€” show only errored requests
  • ๐Ÿข Slow only toggle โ€” requests exceeding slowRequestThresholdMs
  • ๐Ÿ”ข Status-family chips โ€” 2xx / 3xx / 4xx / 5xx

Retrying a request

Pass your authenticated Dio instance to wrap(retryDio:) or openInspector(retryDio:). The retry button in the app bar reissues the captured request via that client.

Multipart support

FormData payloads are described (field names, file names, sizes) rather than serialized โ€” useful for inspecting uploads without breaking streams.


โšก Performance Monitoring

The performance screen is available from the speed icon in the inspector toolbar. It requires no additional dependencies โ€” everything uses Flutter's built-in WidgetsBinding.addTimingsCallback.

What's tracked

Metric Description
Startup time Wall-clock time from PulseOps.initialize() to the first rendered frame
Current FPS Rolling average over the last 15 frames
Dropped frames Frames taking > 16 ms (one refresh period at 60 Hz)
Severe jank Frames taking > 33 ms (two full refresh periods)
API latency Per-request bar chart for the last 40 completed calls

FPS chart

A gradient-filled sparkline shows FPS over the last N frames (default 300). The line turns green (โ‰ฅ 55 fps), yellow (โ‰ฅ 40 fps), or red (< 40 fps). Reference grid lines are drawn at 60 fps and 30 fps.

Latency chart

Each bar represents one completed request, coloured:

  • ๐ŸŸข Green โ€” within the slow-request threshold
  • ๐ŸŸก Yellow โ€” exceeds slowRequestThresholdMs
  • ๐Ÿ”ด Red โ€” the request failed

A dashed threshold line marks the slow boundary.

Configuration

const PulseOpsConfig(
  enableFpsMonitor: true,           // default: true
  fpsFrameBufferSize: 300,          // frames kept in memory
  slowRequestThresholdMs: 2000,     // ms before a request is "slow"
)

๐Ÿ’ฅ Crash Diagnostics

PulseOps decouples itself from any specific crash backend via the PulseCrashReporter interface, so the package itself does not depend on firebase_crashlytics. You wire that up in your app.

Example adapter for Firebase Crashlytics

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:pulse_ops/pulse_ops.dart';

class FirebaseCrashReporterAdapter implements PulseCrashReporter {
  FirebaseCrashReporterAdapter(this._c);
  final FirebaseCrashlytics _c;

  @override
  Future<void> recordNonFatal(Object error,
      {StackTrace? stackTrace, String? reason, Map<String, dynamic>? context}) async {
    await _attach(context);
    await _c.recordError(error, stackTrace, reason: reason, fatal: false);
  }

  @override
  Future<void> recordFatal(Object error,
      {StackTrace? stackTrace, Map<String, dynamic>? context}) async {
    await _attach(context);
    await _c.recordError(error, stackTrace, fatal: true);
  }

  @override
  Future<void> attachBreadcrumbs(List<Breadcrumb> breadcrumbs) async {
    for (final b in breadcrumbs) {
      await _c.log(b.toString());
    }
  }

  @override
  Future<void> attachNetworkHistory(List<NetworkRecord> records) async {
    final summary = records.take(20).map((r) =>
        '${r.method} ${r.endpoint} -> ${r.statusCode ?? r.status.name}').join('\n');
    await _c.setCustomKey('pulse_ops_recent_requests', summary);
  }

  @override
  Future<void> setCustomKey(String key, Object value) =>
      _c.setCustomKey(key, value);

  Future<void> _attach(Map<String, dynamic>? context) async {
    if (context == null) return;
    for (final e in context.entries) {
      await _c.setCustomKey(e.key, e.value.toString());
    }
  }
}

Then pass it in:

await PulseOps.initialize(
  crashReporter: FirebaseCrashReporterAdapter(FirebaseCrashlytics.instance),
);

Example adapter for Sentry

import 'package:pulse_ops/pulse_ops.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

class SentryCrashReporterAdapter implements PulseCrashReporter {
  const SentryCrashReporterAdapter();

  @override
  Future<void> recordNonFatal(Object error,
      {StackTrace? stackTrace, String? reason, Map<String, dynamic>? context}) async {
    await Sentry.captureException(
      error,
      stackTrace: stackTrace,
      hint: Hint.withMap({
        if (reason != null) 'reason': reason,
        if (context != null) ...context,
      }),
    );
  }

  @override
  Future<void> recordFatal(Object error,
      {StackTrace? stackTrace, Map<String, dynamic>? context}) async {
    await Sentry.captureException(
      error,
      stackTrace: stackTrace,
      withScope: (scope) => scope.setTag('fatal', 'true'),
    );
  }

  @override
  Future<void> attachBreadcrumbs(List<Breadcrumb> breadcrumbs) async {
    for (final b in breadcrumbs) {
      await Sentry.addBreadcrumb(
        SentryBreadcrumb(
          message: b.message,
          level: _sentryLevel(b.level),
          timestamp: b.timestamp,
          data: b.data,
        ),
      );
    }
  }

  @override
  Future<void> attachNetworkHistory(List<NetworkRecord> records) async {
    final summary = records.take(20).map((r) =>
        '${r.method} ${r.endpoint} -> ${r.statusCode ?? r.status.name}').join('\n');
    await Sentry.configureScope(
      (scope) => scope.setContexts('pulse_ops_recent_requests', {'log': summary}),
    );
  }

  @override
  Future<void> setCustomKey(String key, Object value) async {
    await Sentry.configureScope((scope) => scope.setTag(key, value.toString()));
  }

  SentryLevel _sentryLevel(BreadcrumbLevel level) {
    switch (level) {
      case BreadcrumbLevel.debug:   return SentryLevel.debug;
      case BreadcrumbLevel.info:    return SentryLevel.info;
      case BreadcrumbLevel.warning: return SentryLevel.warning;
      case BreadcrumbLevel.error:   return SentryLevel.error;
    }
  }
}

Then initialize Sentry first, then PulseOps:

await SentryFlutter.init(
  (options) => options.dsn = 'YOUR_DSN',
  appRunner: () async {
    await PulseOps.initialize(
      crashReporter: const SentryCrashReporterAdapter(),
    );
    runApp(PulseOps.instance.wrap(child: MyApp()));
  },
);

What gets attached to crashes

Whenever an error is reported through PulseOps โ€” automatically for failed HTTP requests, or manually via PulseOps.instance.recordError(...):

  • The breadcrumb trail (default 50 entries) is forwarded.
  • The last 20 network records are summarized and attached as context.
  • Any additional extra map you pass is merged in.

Adding your own breadcrumbs

PulseOps.instance.log('User opened checkout', data: {'cart_size': 4});

Reporting errors manually

try {
  await doRiskyThing();
} catch (e, st) {
  await PulseOps.instance.recordError(e, st, reason: 'checkout pipeline');
}

Failed requests

When PulseOpsConfig.captureFailedRequestsAsCrashEvents is true (the default), every Dio exception is forwarded to the configured reporter as a non-fatal โ€” already enriched with the request summary, e.g.:

GET /profile        200 OK
POST /login         timeout
PUT /settings       500

This timeline rides along to Crashlytics so triage starts with full context.


โš™๏ธ Configuration

const PulseOpsConfig(
  // โ€” General โ€”
  enableInRelease: false,                  // keep disabled in prod builds
  showOverlay: true,                       // floating launcher button

  // โ€” Network โ€”
  maxRecords: 200,                         // request ring-buffer capacity
  sanitizeKeys: ['authorization', ...],    // keys redacted before storage
  slowRequestThresholdMs: 2000,            // ms to flag a request as slow
  captureFailedRequestsAsCrashEvents: true,
  attachNetworkHistoryToCrashes: true,

  // โ€” Performance โ€”
  enableFpsMonitor: true,                  // frame-timing subscriber
  fpsFrameBufferSize: 300,                 // frames kept in memory

  // โ€” Overlay / UX โ€”
  enableShakeToOpen: true,                 // shake gesture to open inspector
  shakeThreshold: 22.0,                    // m/sยฒ to trigger a shake
  inspectorPresentation: InspectorPresentation.bottomSheet, // or .fullScreen

  // โ€” Crash โ€”
  maxBreadcrumbs: 50,
)

You can pass it directly to PulseOps.initialize(config: ...), or use the shorthand named args enableInRelease, sanitizeKeys, crashlytics.


๐Ÿ— Architecture

lib/
โ”œโ”€โ”€ pulse_ops.dart                         # public exports
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ core/                              # facade + config
    โ”œโ”€โ”€ network/
    โ”‚   โ”œโ”€โ”€ interceptor/                   # PulseDioInterceptor
    โ”‚   โ”œโ”€โ”€ models/                        # NetworkRecord
    โ”‚   โ”œโ”€โ”€ store/                         # NetworkStore (in-memory)
    โ”‚   โ””โ”€โ”€ utils/                         # CurlBuilder, Sanitizer, LogExporter
    โ”œโ”€โ”€ performance/
    โ”‚   โ”œโ”€โ”€ frame_metric.dart              # FrameMetric value type
    โ”‚   โ”œโ”€โ”€ performance_store.dart         # ring-buffer + stream
    โ”‚   โ””โ”€โ”€ fps_tracker.dart              # WidgetsBinding timing subscriber
    โ”œโ”€โ”€ crash/                             # breadcrumbs + reporter + bridge
    โ”œโ”€โ”€ ui/
    โ”‚   โ”œโ”€โ”€ inspector/                     # screens, tabs, widgets
    โ”‚   โ”œโ”€โ”€ performance/                   # PerformanceScreen + charts
    โ”‚   โ”œโ”€โ”€ overlay/                       # draggable launcher + shake detector
    โ”‚   โ””โ”€โ”€ theme/                         # dark Material 3 theme
    โ””โ”€โ”€ providers/                         # Riverpod scope

The design follows clean architecture principles: the network layer is plain Dart with no Flutter imports, the UI consumes data only through Riverpod providers, and the crash backend is injected via an interface. This makes it trivial to:

  • swap the in-memory store for an Isar/Hive-backed store
  • substitute the crash reporter for Sentry, Bugsnag, or a custom sink
  • embed the inspector inside a debug menu without using the overlay

๐Ÿงช Testing

The package ships with a full test suite covering the sanitizer, cURL builder, in-memory store, breadcrumb trail, Dio interceptor (success / failure / sanitization paths), and the facade.

flutter test

๐Ÿ›ฃ Roadmap

  • x Real-time FPS monitor, frame drop detection, API latency chart (v1.2)
  • x Shake-to-open, expandable bottom sheet, log export (v1.1)
  • Isar-backed persistent network store
  • HTTP/2 + http package interceptor adapter
  • Log inspector (debugPrint / Logger)
  • Per-host throttling visualizer
  • Memory & GC pressure charts

๐Ÿ“„ License

MIT โ€” see LICENSE.

Libraries

pulse_ops
PulseOps โ€” a modern Flutter-native developer toolkit for in-app network inspection and crash diagnostics.