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
- Interface Preview
- Architecture
- Features
- Getting Started
- Logger Configuration
- Internationalization
- Production Safety
- Customization
- Contributing
- License
Live Web Demo: https://yelmuratoff.github.io/ispect/
Drag and drop exported log files to explore them in the browser.
Modular designβadd only what you need:
| Package | Role | Version |
|---|---|---|
| ispect | Core panel + inspectors | |
| ispectify | Logging backbone | |
| ispectify_dio | Dio HTTP capture | |
| ispectify_http | http package capture | |
| ispectify_ws | WebSocket traffic | |
| ispectify_db | Database operations | |
| ispectify_bloc | BLoC events/states |
Inspect HTTP requests and responses with headers, bodies, timing, and errors.
Track queries with execution time, result counts, and errors.
- Log levels (info, warning, error, debug)
- Category-based filtering
- Configurable history retention
- Export and sharing
- Observer pattern for third-party integrations
Widget tree inspection, layout measurements, color picker.
Frame rates, memory usage, performance metrics.
Device details, app version, platform info.
In-app feedback with screenshot capture and log attachment.
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.
dependencies:
ispect: ^4.7.0ISpect 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 apkNote: When
ISPECT_ENABLEDis not set (default),ISpect.run(),ISpectBuilder, andISpectLocalizations.delegates()automatically become no-ops, allowing Dart's tree-shaking to remove all ISpect code from your production build.
void main() {
ISpect.run(() => runApp(const MyApp()));
}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()));
}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),
);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.
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.
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.
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.
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 apkflutter 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
| 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
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=stagingfinal 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(),
)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(),
)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(),
);
}
}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 storageonOpenFileβ Open exported files with system viewersonShareβ Share logs via system share sheet
Useful packages: file_picker, open_filex, share_plus
ISpect provides companion packages for common Flutter libraries.
dependencies:
ispect: ^4.7.0 # Core package (required)
ispectify_dio: ^4.7.0 # Dio HTTP client
ispectify_http: ^4.7.0 # Standard http package
ispectify_db: ^4.7.0 # Database operations
ispectify_ws: ^4.7.0 # WebSocket traffic
ispectify_bloc: ^4.7.0 # BLoC/Cubit integrationimport '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,
),
),
);
},
);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,
),
),
);
},
);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));
},
);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},
);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);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,
)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.
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,
),
);Check out the example/ directory for a complete working app with all integrations.
Contributions welcome! See CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.
- ispectify β Core logging system
- ispectify_dio β Dio integration
- ispectify_http β HTTP package integration
- ispectify_ws β WebSocket monitoring
- ispectify_db β Database logging
- ispectify_bloc β BLoC integration







