pub version License GitHub stars

Pub likes Pub points Pub downloads

ISpect is a production-safe debugging toolkit for Flutter. It provides visual inspection, structured logging, network monitoring, and data redaction — all automatically removed from release builds via compile-time tree-shaking.

Live Web Demo — drag and drop exported log files to explore them in the browser.


Why ISpect?

Most Flutter debugging tools stay in your binary. ISpect doesn't — when ISPECT_ENABLED is not defined, the entire toolkit compiles to no-ops and is eliminated by Dart's tree-shaker. Zero bytes in production.

Capability What it does
Zero-footprint builds Compile-time const guard removes all code from release APK/IPA
Visual inspector Tap any widget to see its render box, padding, constraints, and color
Structured logs Typed log entries with levels, filtering, export/import, and session history
Network capture Request/response inspection for Dio, http, and WebSocket clients
Automatic redaction Tokens, passwords, PII, and credit cards masked before they reach logs
Observer hooks Forward log events to Sentry, Crashlytics, or any backend in real-time
12 languages en, ru, kk, zh, es, fr, de, pt, ar, ko, ja, hi

Quick Start

dependencies:
  ispect: ^4.8.0-dev02
import 'package:flutter/material.dart';
import 'package:ispect/ispect.dart';

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: ISpectNavigatorObserver.observers(),
      builder: (_, child) => ISpectBuilder.wrap(child: child!),
      home: const HomePage(),
    );
  }
}
# Development — toolkit active
flutter run --dart-define=ISPECT_ENABLED=true

# Release — toolkit removed via tree-shaking
flutter build apk

When ISPECT_ENABLED is not set (default), ISpect.run(), ISpectBuilder, and ISpectLocalizations.delegates() become no-ops. Dart's tree-shaker strips everything out.


Production Safety

Debug tools can expose API keys, tokens, and user data. ISpect solves this at the compiler level.

flutter build apk --release --obfuscate --split-debug-info=debug-info/
Build APK Size "ispect" strings
Obfuscated release 42.4 MB 6
Non-obfuscated release 44.5 MB 34
Development 51.0 MB 276

For environment-based control:

import 'package:flutter/foundation.dart';

class ISpectConfig {
  static const bool isEnabled = bool.fromEnvironment(
    'ISPECT_ENABLED',
    defaultValue: kDebugMode,
  );

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

  static bool get shouldInitialize => isEnabled && environment != 'production';
}

void main() {
  if (ISpectConfig.shouldInitialize) {
    ISpect.run(() => runApp(const MyApp()));
  } else {
    runApp(const MyApp());
  }
}
flutter build apk \
  --dart-define=ISPECT_ENABLED=true \
  --dart-define=ENVIRONMENT=staging

Logger Configuration

void main() {
  final logger = ISpectFlutter.init(
    options: ISpectLoggerOptions(
      enabled: true,
      useHistory: true,
      useConsoleLogs: kDebugMode,
      maxHistoryItems: 5000,
      logTruncateLength: 4000,
    ),
  );

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

Disable console output (logs still flow to observers and UI):

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

Streaming-only (no in-memory history, useful for observer-driven pipelines):

final logger = ISpectFlutter.init(
  options: const ISpectLoggerOptions(useHistory: false),
);

Filter by priority:

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

final logger = ISpectFlutter.init(filter: WarningsAndAbove());

Localization

ISpect ships with 12 built-in locales. Pass delegates through ISpectLocalizations.delegates() — it merges ISpect's translations with your app's own delegates in a single call:

MaterialApp(
  localizationsDelegates: ISpectLocalizations.delegates(
    delegates: [
      // your app's delegates go here
    ],
  ),
)

To force a specific locale for the debug panel regardless of the app locale:

ISpectBuilder(
  options: ISpectOptions(
    observer: observer,
    locale: const Locale('ru'),
  ),
  child: child ?? const SizedBox.shrink(),
)

Observers

Observers tap into the log stream without coupling your app to ISpect's internals. Use them to bridge events to any external service.

class SentryISpectObserver implements ISpectObserver {
  @override
  void onLog(ISpectLogData data) {
    // Add as Sentry breadcrumb
  }

  @override
  void onError(ISpectLogData err) {
    // Sentry.captureException(err.exception, stackTrace: err.stackTrace);
  }

  @override
  void onException(ISpectLogData err) {
    // Sentry.captureException(err.exception, stackTrace: err.stackTrace);
  }
}

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

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

Data Redaction

Sensitive data is automatically masked before it reaches logs or observers. Built-in patterns cover auth tokens, passwords, API keys, PII (SSN, passport), credit cards, and bank accounts.

Note: Network interceptors (ISpectDioInterceptor, ISpectHttpInterceptor, ISpectWSInterceptor) have enableRedaction: true by default. Authorization headers, tokens, passwords, and other sensitive data are automatically redacted. If you need to see unredacted data during debugging, you can opt out:

ISpectDioInterceptor(
  settings: const ISpectDioInterceptorSettings(enableRedaction: false),
);

The built-in SettingsBuilder.production() and SettingsBuilder.staging() factory constructors also enable redaction automatically.

// Extend redaction with your own keys
final redactor = RedactionService();
redactor.ignoreKeys(['authorization', 'x-api-key']);
redactor.ignoreValues(['<test-token>']);

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

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

Modular Packages

Install only what your project needs. Each package works independently.

dependencies:
  ispect: ^4.8.0-dev02 # Core UI, inspector, log viewer
  ispectify: ^4.8.0-dev02 # Logging backbone (Dart-only, no Flutter)
  ispectify_dio: ^4.8.0-dev02 # Dio HTTP interceptor
  ispectify_http: ^4.8.0-dev02 # http package interceptor
  ispectify_ws: ^4.8.0-dev02 # WebSocket traffic capture
  ispectify_db: ^4.8.0-dev02 # Database operation tracking
  ispectify_bloc: ^4.8.0-dev02 # BLoC event/state observer

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

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

final 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,
        ),
      ),
    );
  },
);

WebSocket

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);

Database

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

ISpectDbCore.config = const ISpectDbConfig(
  sampleRate: 1.0,
  redact: true,
  attachStackOnError: true,
  slowQueryThreshold: Duration(milliseconds: 400),
);

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},
);

BLoC

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);
  },
);

Theming and Customization

ISpectBuilder(
  options: ISpectOptions(observer: observer),
  theme: ISpectTheme(
    pageTitle: 'Debug Panel',
    background: ISpectDynamicColor(light: Colors.white, dark: Colors.black),
    divider: ISpectDynamicColor(
      light: Colors.grey.shade300,
      dark: Colors.grey.shade800,
    ),
    logColors: {
      'error': Colors.red,
      'warning': Colors.orange,
      'info': Colors.blue,
      'debug': Colors.grey,
    },
    logIcons: {
      'error': Icons.error,
      'warning': Icons.warning,
      'info': Icons.info,
      'debug': Icons.bug_report,
    },
    logDescriptions: {
      'error': 'Critical application errors',
      'info': 'Informational messages',
    },
    disabledLogTypes: {
      'riverpod-add',
      'riverpod-update',
      'riverpod-dispose',
      'riverpod-fail',
    },
  ),
  child: child ?? const SizedBox.shrink(),
)

Panel Actions

ISpectBuilder(
  options: ISpectOptions(
    observer: observer,
    locale: const Locale('en'),
    actionItems: [
      ISpectActionItem(
        onTap: (context) { /* Clear cache, reset state */ },
        title: 'Clear All Data',
        icon: Icons.delete_sweep,
      ),
    ],
    panelItems: [
      DraggablePanelItem(
        enableBadge: false,
        icon: Icons.settings,
        onTap: (context) { /* Open settings */ },
      ),
    ],
    panelButtons: [
      DraggablePanelButtonItem(
        icon: Icons.info,
        label: 'App Info',
        onTap: (context) { /* Show app version */ },
      ),
    ],
  ),
  child: child ?? const SizedBox.shrink(),
)

Settings Persistence

Load saved settings on startup and persist changes via onSettingsChanged:

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

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

  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)));
}

// In your widget:
ISpectBuilder(
  options: ISpectOptions(
    observer: observer,
    initialSettings: initialSettings ?? const ISpectSettingsState(
      enabled: true,
      useConsoleLogs: true,
      useHistory: true,
    ),
    onSettingsChanged: (settings) async {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('ispect_settings', jsonEncode(settings.toJson()));
    },
  ),
  child: child ?? const SizedBox.shrink(),
)

Callbacks

ISpectBuilder(
  options: ISpectOptions(
    observer: observer,
    onLoadLogContent: (context) async {
      // Load log files from storage via file_picker
      return 'Loaded log content';
    },
    onOpenFile: (path) async {
      // Open with system viewer via open_filex
    },
    onShare: (ISpectShareRequest request) async {
      // Share via share_plus
    },
  ),
  child: child ?? const SizedBox.shrink(),
)
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: (_, child) => ISpectBuilder(
        observer: _observer,
        child: child ?? const SizedBox(),
      ),
    );
  }
}

Examples

See the example/ directory for a complete working app.

Contributing

Contributions welcome! See CONTRIBUTING.md.

License

MIT — see LICENSE.


Libraries

ispectify_dio