In-app debugging toolkit for Flutter

pub version License GitHub stars

Pub likes Pub points Pub downloads

What is ISpect?

In-app debugging tool for Flutter. Inspect network requests, logs, and UI during development and testing.

Features:

  • Network monitoring (Dio, http)
  • Structured logging with filtering
  • UI inspector
  • Observer pattern for external integrations
  • Removed from production builds via tree-shaking

Table of Contents

Interface Preview

Web Demo

Live Web Demo: https://yelmuratoff.github.io/ispect/

Drag and drop exported log files to explore them in the browser.

Architecture

Modular designβ€”add only what you need:

Package Role Version
ispect Core panel + inspectors pub
ispectify Logging backbone pub
ispectify_dio Dio HTTP capture pub
ispectify_http http package capture pub
ispectify_ws WebSocket traffic pub
ispectify_db Database operations pub
ispectify_bloc BLoC events/states pub

Features

Network Monitoring

Inspect HTTP requests and responses with headers, bodies, timing, and errors.

Database Operations

Track queries with execution time, result counts, and errors.

Logging System

  • Log levels (info, warning, error, debug)
  • Category-based filtering
  • Configurable history retention
  • Export and sharing
  • Observer pattern for third-party integrations

UI Inspector

Widget tree inspection, layout measurements, color picker.

Performance Tracking

Frame rates, memory usage, performance metrics.

Device Information

Device details, app version, platform info.

Feedback Collection

In-app feedback with screenshot capture and log attachment.

Observer Pattern

Observers receive log events in real-time for integration with error tracking services.

import 'dart:developer';
import 'package:ispect/ispect.dart';

// Observer that sends errors to Sentry
class SentryISpectObserver implements ISpectObserver {
  @override
  void onError(ISpectLogData err) {
    // Send to Sentry
    log('SentryISpectObserver - onError: ${err.message}');
    // Sentry.captureException(err.exception, stackTrace: err.stackTrace);
  }

  @override
  void onException(ISpectLogData err) {
    log('SentryISpectObserver - onException: ${err.message}');
    // Sentry.captureException(err.exception, stackTrace: err.stackTrace);
  }

  @override
  void onLog(ISpectLogData data) {
    // Optionally send high-priority logs to Sentry as breadcrumbs
    log('SentryISpectObserver - onLog: ${data.message}');
  }
}

// Observer that sends data to your backend
class BackendISpectObserver implements ISpectObserver {
  @override
  void onError(ISpectLogData err) {
    log('BackendISpectObserver - onError: ${err.message}');
    // Send error to your analytics/logging backend
  }

  @override
  void onException(ISpectLogData err) {
    log('BackendISpectObserver - onException: ${err.message}');
  }

  @override
  void onLog(ISpectLogData data) {
    log('BackendISpectObserver - onLog: ${data.message}');
  }
}

void main() {
  final logger = ISpectFlutter.init();

  // Add multiple observers
  logger.addObserver(SentryISpectObserver());
  logger.addObserver(BackendISpectObserver());

  ISpect.run(logger: logger, () => runApp(const MyApp()));
}

Observers receive all logs, errors, and exceptions. Use them to forward events to Sentry, Crashlytics, or custom analytics endpoints.

Getting Started

Installation

dependencies:
  ispect: ^4.7.0-dev04

Quick Start

ISpect uses automatic tree-shaking via kISpectEnabled. By default, ISpect is disabled and completely removed from production builds.

import 'package:flutter/material.dart';
import 'package:ispect/ispect.dart';

final observer = ISpectNavigatorObserver();

void main() {
  ISpect.run(() => runApp(const MyApp()));
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: ISpectLocalizations.delegates(),
      navigatorObservers: [observer],
      builder: (context, child) => ISpectBuilder(
        options: ISpectOptions(observer: observer),
        child: child ?? const SizedBox.shrink(),
      ),
      home: const HomePage(),
    );
  }
}

Build Commands:

# Development (ISpect enabled)
flutter run --dart-define=ISPECT_ENABLED=true

# Production (ISpect automatically removed via tree-shaking)
flutter build apk

Note: When ISPECT_ENABLED is not set (default), ISpect.run(), ISpectBuilder, and ISpectLocalizations.delegates() automatically become no-ops, allowing Dart's tree-shaking to remove all ISpect code from your production build.

Logger Configuration

Default Setup

void main() {
  ISpect.run(() => runApp(const MyApp()));
}

Custom Logger Options

Configure the logger during initialization:

void main() {
  final logger = ISpectFlutter.init(
    options: ISpectLoggerOptions(
      enabled: true,
      useHistory: true,              // Store logs in memory
      useConsoleLogs: kDebugMode,    // Print to console in debug mode
      maxHistoryItems: 5000,         // Keep last 5000 log entries
      logTruncateLength: 4000,       // Truncate long messages
    ),
  );

  ISpect.run(logger: logger, () => runApp(const MyApp()));
}

Quiet Mode (Disable Console Output)

If console logs are too noisy, disable them:

final logger = ISpectFlutter.init(
  options: const ISpectLoggerOptions(useConsoleLogs: false),
);
ISpect.run(logger: logger, () => runApp(const MyApp()));

You can also change this at runtime:

ISpect.logger.configure(
  options: ISpect.logger.options.copyWith(useConsoleLogs: false),
);

Stateless Mode (No History)

If you don't need log history (e.g., for real-time streaming only):

final logger = ISpectFlutter.init(
  options: const ISpectLoggerOptions(useHistory: false),
);
ISpect.run(logger: logger, () => runApp(const MyApp()));

Logs will still be sent to observers and console, but won't be stored in memory.

Filtering

Filter logs by priority or custom criteria:

// Only capture warnings and errors
class WarningsAndAbove implements ISpectFilter {
  @override
  bool apply(ISpectLogData data) {
    return (data.logLevel?.priority ?? 0) >= LogLevel.warning.priority;
  }
}

void main() {
  final logger = ISpectFlutter.init(filter: WarningsAndAbove());
  ISpect.run(logger: logger, () => runApp(const MyApp()));
}

For advanced configuration options (redaction, dynamic reconfiguration, performance tuning), see the ISpectLogger documentation.

Internationalization

Supported languages: en, ru, kk, zh, es, fr, de, pt, ar, ko, ja, hi

MaterialApp(
  localizationsDelegates: ISpectLocalizations.delegates(
    delegates: [
      // Add your own localization delegates here
    ],
  ),
  // ...
)

You can extend or override translations using the ISpectLocalizations delegate.

Production Safety

Security Best Practice: Debug and logging tools should not be included in production builds. They can expose sensitive data (API keys, tokens, user data, network traffic) and increase app size.

ISpect uses kISpectEnabled compile-time constant for automatic tree-shaking. When disabled (default), all ISpect code is removed from production builds.

Zero-Conditional API

ISpect provides factory methods that handle the kISpectEnabled check internally, eliminating conditional logic in your code:

void main() {
  ISpect.run(() => runApp(const MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      localizationsDelegates: ISpectLocalizations.delegates(),
      navigatorObservers: ISpectNavigatorObserver.observers(),
      builder: (_, child) => ISpectBuilder.wrap(child: child!),
      home: const MyHomePage(),
    );
  }
}

Build Commands:

# Development (ISpect enabled)
flutter run --dart-define=ISPECT_ENABLED=true

# Production (ISpect removed via tree-shaking)
flutter build apk

Security recommendations

1. Maximum Security Build

flutter build apk --release \
  --obfuscate \
  --split-debug-info=debug-info/

This enables:

  • Tree-shaking: Removes unused code (including disabled ISpect)
  • Obfuscation: Mangles class/method names
  • Debug symbol stripping: Removes debug information

2. Verified Tree-Shaking Results

Build APK Size libapp.so "ispect" strings
Obfuscated Production 42.4 MB - 6
Non-obfuscated Production 44.5 MB 3.8 MB 34
Development 51.0 MB 5.8 MB 276

Benefits:

  • No data exposure in production
  • Zero runtime overhead
  • Smaller production builds
  • Easier security audits

Environment-Based Configuration

For more complex setups (dev/staging/prod environments), you can create a configuration file:

// lib/config/ispect_config.dart
import 'package:flutter/foundation.dart';

class ISpectConfig {
  static const bool isEnabled = bool.fromEnvironment(
    'ISPECT_ENABLED',
    defaultValue: kDebugMode, // Enable in debug mode by default
  );

  static const String environment = String.fromEnvironment(
    'ENVIRONMENT',
    defaultValue: 'development',
  );

  // Only enable in non-production environments
  static bool get shouldInitialize => isEnabled && environment != 'production';
}

Then use it in your main.dart:

void main() {
  if (ISpectConfig.shouldInitialize) {
    ISpect.run(() => runApp(const MyApp()));
  } else {
    runApp(const MyApp());
  }
}

Build with environment flags:

flutter build apk \
  --dart-define=ISPECT_ENABLED=true \
  --dart-define=ENVIRONMENT=staging

Customization

Theming

final observer = ISpectNavigatorObserver();

ISpectBuilder(
  options: ISpectOptions(observer: observer),
  theme: ISpectTheme(
    pageTitle: 'Debug Panel',

    // Unified color theming with ISpectDynamicColor
    background: ISpectDynamicColor(
      light: Colors.white,
      dark: Colors.black,
    ),
    divider: ISpectDynamicColor(
      light: Colors.grey.shade300,
      dark: Colors.grey.shade800,
    ),

    // Custom colors for log types
    logColors: {
      'error': Colors.red,
      'warning': Colors.orange,
      'info': Colors.blue,
      'debug': Colors.grey,
    },

    // Custom icons for log types
    logIcons: {
      'error': Icons.error,
      'warning': Icons.warning,
      'info': Icons.info,
      'debug': Icons.bug_report,
    },

    // Custom descriptions for log types
    logDescriptions: {
      'error': 'Critical application errors',
      'info': 'Informational messages',
    },

    // Disable specific log categories
    disabledLogTypes: {
      'riverpod-add',
      'riverpod-update',
      'riverpod-dispose',
      'riverpod-fail',
    },
  ),
  child: child ?? const SizedBox.shrink(),
)

Panel Actions

final observer = ISpectNavigatorObserver();

ISpectBuilder(
  options: ISpectOptions(
    observer: observer,
    locale: const Locale('en'),

    // Custom action items in the menu
    actionItems: [
      ISpectActionItem(
        onTap: (context) {
          // Clear cache, reset state, etc.
        },
        title: 'Clear All Data',
        icon: Icons.delete_sweep,
      ),
      ISpectActionItem(
        onTap: (context) {
          // Switch to a test environment
        },
        title: 'Switch Environment',
        icon: Icons.swap_horiz,
      ),
    ],

    // Custom panel items (icons)
    panelItems: [
      DraggablePanelItem(
        enableBadge: false,
        icon: Icons.settings,
        onTap: (context) {
          // Open settings
        },
      ),
    ],

    // Custom panel buttons (labeled)
    panelButtons: [
      DraggablePanelButtonItem(
        icon: Icons.info,
        label: 'App Info',
        onTap: (context) {
          // Show app version, build number, etc.
        },
      ),
    ],
  ),
  child: child ?? const SizedBox.shrink(),
)

Settings Persistence

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

final observer = ISpectNavigatorObserver();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Load persisted settings
  final prefs = await SharedPreferences.getInstance();
  final settingsJson = prefs.getString('ispect_settings');
  final initialSettings = settingsJson != null
      ? ISpectSettingsState.fromJson(jsonDecode(settingsJson))
      : null;

  final logger = ISpectFlutter.init();
  ISpect.run(logger: logger, () => runApp(MyApp(initialSettings: initialSettings)));
}

class MyApp extends StatelessWidget {
  final ISpectSettingsState? initialSettings;

  const MyApp({super.key, this.initialSettings});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorObservers: [observer],
      builder: (context, child) => ISpectBuilder(
        options: ISpectOptions(
          observer: observer,
          initialSettings: initialSettings ?? const ISpectSettingsState(
            disabledLogTypes: {'warning'},
            enabled: true,
            useConsoleLogs: true,
            useHistory: true,
          ),
          onSettingsChanged: (settings) async {
            // Save settings when they change
            final prefs = await SharedPreferences.getInstance();
            await prefs.setString('ispect_settings', jsonEncode(settings.toJson()));
          },
        ),
        child: child ?? const SizedBox.shrink(),
      ),
      home: const MyHomePage(),
    );
  }
}

Custom Callbacks

import 'package:open_filex/open_filex.dart';
import 'package:share_plus/share_plus.dart';

final observer = ISpectNavigatorObserver();

ISpectBuilder(
  options: ISpectOptions(
    observer: observer,

    // Load log content from external source
    onLoadLogContent: (context) async {
      // Use file_picker to let users select a log file
      // final result = await FilePicker.platform.pickFiles();
      // if (result != null) {
      //   return File(result.files.single.path!).readAsStringSync();
      // }
      return 'Loaded log content from file';
    },

    // Handle file opening
    onOpenFile: (path) async {
      await OpenFilex.open(path);
    },

    // Handle sharing
    onShare: (ISpectShareRequest request) async {
      final files = request.filePaths.map((path) => XFile(path)).toList();
      await Share.shareXFiles(
        files,
        text: request.text,
        subject: request.subject,
      );
    },
  ),
  child: child ?? const SizedBox.shrink(),
)

Available callbacks:

  • onLoadLogContent – Load log files from storage
  • onOpenFile – Open exported files with system viewers
  • onShare – Share logs via system share sheet

Useful packages: file_picker, open_filex, share_plus


Integrations

ISpect provides companion packages for common Flutter libraries.

Available Packages

dependencies:
  ispect: ^4.7.0-dev04              # Core package (required)
  ispectify_dio: ^4.7.0-dev04       # Dio HTTP client
  ispectify_http: ^4.7.0-dev04      # Standard http package
  ispectify_db: ^4.7.0-dev04        # Database operations
  ispectify_ws: ^4.7.0-dev04        # WebSocket traffic
  ispectify_bloc: ^4.7.0-dev04      # BLoC/Cubit integration

🌐 HTTP Monitoring

Dio

import 'package:dio/dio.dart';
import 'package:ispectify_dio/ispectify_dio.dart';

final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

ISpect.run(
  () => runApp(MyApp()),
  logger: logger,
  onInit: () {
    dio.interceptors.add(
      ISpectDioInterceptor(
        logger: logger,
        settings: const ISpectDioInterceptorSettings(
          printRequestHeaders: true,
          printResponseHeaders: true,
          printRequestData: true,
          printResponseData: true,
        ),
      ),
    );
  },
);

HTTP Package

import 'package:http_interceptor/http_interceptor.dart' as http_interceptor;
import 'package:ispectify_http/ispectify_http.dart';

final http_interceptor.InterceptedClient client =
    http_interceptor.InterceptedClient.build(interceptors: []);

ISpect.run(
  () => runApp(MyApp()),
  logger: logger,
  onInit: () {
    client.interceptors.add(
      ISpectHttpInterceptor(
        logger: logger,
        settings: const ISpectHttpInterceptorSettings(
          printRequestHeaders: true,
          printResponseHeaders: true,
        ),
      ),
    );
  },
);

Multiple Clients

final Dio mainDio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final Dio uploadDio = Dio(BaseOptions(baseUrl: 'https://upload.example.com'));

ISpect.run(
  () => runApp(MyApp()),
  logger: logger,
  onInit: () {
    mainDio.interceptors.add(ISpectDioInterceptor(logger: logger));
    uploadDio.interceptors.add(ISpectDioInterceptor(logger: logger));
  },
);

πŸ—„οΈ Database Integration

import 'package:sqflite/sqflite.dart';
import 'package:ispectify_db/ispectify_db.dart';

// Configure database logging
ISpectDbCore.config = const ISpectDbConfig(
  sampleRate: 1.0,
  redact: true,
  attachStackOnError: true,
  enableTransactionMarkers: false,
  slowQueryThreshold: Duration(milliseconds: 400),
);

// Log database operations
final rows = await ISpect.logger.dbTrace<List<Map<String, Object?>>>(
  source: 'sqflite',
  operation: 'query',
  statement: 'SELECT * FROM users WHERE id = ?',
  args: [userId],
  table: 'users',
  run: () => db.rawQuery('SELECT * FROM users WHERE id = ?', [userId]),
  projectResult: (rows) => {'rows': rows.length},
);

πŸ”Œ WebSocket Integration

import 'package:ws/ws.dart';
import 'package:ispectify_ws/ispectify_ws.dart';

final interceptor = ISpectWSInterceptor(
  logger: logger,
  settings: const ISpectWSInterceptorSettings(
    enabled: true,
    printSentData: true,
    printReceivedData: true,
    printReceivedMessage: true,
    printErrorData: true,
    printErrorMessage: true,
  ),
);

final client = WebSocketClient(
  WebSocketOptions.common(
    interceptors: [interceptor],
  ),
);

interceptor.setClient(client);

🎯 BLoC Integration

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ispectify_bloc/ispectify_bloc.dart';

ISpect.run(
  () => runApp(MyApp()),
  logger: logger,
  onInit: () {
    Bloc.observer = ISpectBlocObserver(
      logger: logger,
    );
  },
);

Filter specific BLoC logs:

final observer = ISpectNavigatorObserver();

ISpectBuilder(
  options: ISpectOptions(observer: observer),
  theme: const ISpectTheme(
    disabledLogTypes: {
      'bloc-event',
      'bloc-transition',
      'bloc-state',
    },
  ),
  child: child,
)

🧭 Navigation Tracking

import 'package:flutter/material.dart';
import 'package:ispect/ispect.dart';

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _observer = ISpectNavigatorObserver(
    isLogModals: true,
    isLogPages: true,
    isLogGestures: false,
    isLogOtherTypes: true,
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorObservers: [_observer],
      builder: (context, child) {
        return ISpectBuilder(
          observer: _observer,
          child: child ?? const SizedBox(),
        );
      },
    );
  }
}

Navigation events are logged with the route key.


πŸ”’ Data Redaction

Sensitive data (tokens, passwords, API keys) is automatically redacted by default.

Custom redaction:

// HTTP / WebSocket
final redactor = RedactionService();
redactor.ignoreKeys(['authorization', 'x-api-key']);
redactor.ignoreValues(['<test-token>']);

// Database
ISpectDbCore.config = const ISpectDbConfig(
  redact: true,
  redactKeys: ['password', 'token', 'secret'],
);

// Disable redaction (only for non-sensitive test data)
ISpectDioInterceptor(
  settings: const ISpectDioInterceptorSettings(
    enableRedaction: false,
  ),
);

Examples

Check out the example/ directory for a complete working app with all integrations.

Contributing

Contributions welcome! See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.


Made with ❀️ for Flutter developers

Libraries

ispectify_bloc