ispect 4.8.0-dev03 copy "ispect: ^4.8.0-dev03" to clipboard
ispect: ^4.8.0-dev03 copied to clipboard

Logging and inspector tool for Flutter development and testing

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-dev03
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. Redaction is enabled by default in all network interceptors.

Note: Network interceptors (ISpectDioInterceptor, ISpectHttpInterceptor, ISpectWSInterceptor) have enableRedaction: true by default. The built-in SettingsBuilder.production() and SettingsBuilder.staging() factory constructors also enable redaction automatically.

Built-in rules cover most common sensitive data: auth headers and tokens, passwords, API keys, cookies, PII (SSN, passport, driver's license), financial data (credit cards, bank accounts, IBAN), phone numbers, and more.

If a key is being redacted that you don't want masked (e.g., ?mobile=true used as a platform flag, not a phone number), or you need to add your own sensitive keys — you can customize the RedactionService.

Customizing RedactionService #

Pass a configured RedactionService to the interceptor:

final redactor = RedactionService(
  // Add your own sensitive keys on top of the defaults
  sensitiveKeys: {
    ...kDefaultSensitiveKeys,
    'x-custom-secret',
    'internal_token',
  },

  // Add custom regex patterns for sensitive key detection
  sensitiveKeyPatterns: [
    RegExp(r'my_app_secret_\w+', caseSensitive: false),
  ],

  // Keys to fully mask (value replaced entirely with placeholder)
  fullyMaskedKeys: {'filename'},

  // Change the placeholder text (default: '[REDACTED]')
  placeholder: '***',

  // Number of characters visible at each edge of masked strings (default: 2)
  // e.g., "Bearer abc...xyz ([REDACTED])"
  stringEdgeVisible: 3,

  // Control binary and base64 redaction
  redactBinary: true,   // default: true
  redactBase64: true,    // default: true
);

ISpectDioInterceptor(
  redactor: redactor,
);

Ignoring keys and values #

If a default key is being redacted but shouldn't be in your context, pass ignoredKeys or ignoredValues in the constructor:

final redactor = RedactionService(
  // "mobile" won't be redacted (e.g., ?mobile=true for platform detection)
  ignoredKeys: {'mobile', 'platform_token'},
  // Specific values that should never be masked
  ignoredValues: {'<test-token>', 'public-api-key'},
);

You can also pass ignored keys/values per-call:

redactor.redact(
  data,
  ignoredKeys: {'mobile'},
  ignoredValues: {'public'},
);

Disabling redaction #

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

// Via builder
final settings = ISpectDioInterceptorSettingsBuilder()
    .withoutRedaction()
    .build();

Database-level redaction #

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

Modular Packages #

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

dependencies:
  ispect: ^4.8.0-dev03 # Core UI, inspector, log viewer
  ispectify: ^4.8.0-dev03 # Logging backbone (Dart-only, no Flutter)
  ispectify_dio: ^4.8.0-dev03 # Dio HTTP interceptor
  ispectify_http: ^4.8.0-dev03 # http package interceptor
  ispectify_ws: ^4.8.0-dev03 # WebSocket traffic capture
  ispectify_db: ^4.8.0-dev03 # Database operation tracking
  ispectify_bloc: ^4.8.0-dev03 # 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.