flutter_ai_debugger

AI-powered Flutter error tracker & debugger using Gemini: capture, analyze, store (Hive), and export (CSV/JSON).

Quick Start

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_ai_debugger/flutter_ai_debugger.dart';

final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey<NavigatorState>();

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

    const envKey = String.fromEnvironment('GEMINI_API_KEY');
    const fallback = '<PASTE_KEY>'; // for quick local try only
    final apiKey = envKey.isEmpty ? fallback : envKey;

    await AiDebugger.init(
      AiDebugConfig(
        apiKey: apiKey,
        enableInternalLogs: true,
      ),
      onNewReport: (r) {},
    );

    runApp(const MyApp());
  }, (e, s) => AiDebugger.logError(e, s));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: appNavigatorKey,
      builder: (context, child) {
        return Stack(
          children: [
            if (child != null) child,
            AiDebugDraggableButton(
              navigatorKey: appNavigatorKey,
            ),
          ],
        );
      },
      home: Scaffold(
        appBar: AppBar(title: const Text('AI Debugger Example')),
        body: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            ElevatedButton(
              onPressed: () => throw Exception('Forced error'),
              child: const Text('Cause Error'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => Navigator.of(context).push(
                MaterialPageRoute(builder: (_) => const AiDebugDashboard()),
              ),
              child: const Text('Open AI Debug Dashboard (manual)'),
            ),
          ],
        ),
      ),
    );
  }
}

Passing the API key

  • Preferred: flutter run --dart-define=GEMINI_API_KEY=YOUR_KEY
  • For CI: use secrets --dart-define=GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}

Optional: export reports

  • Use ExportService.exportJson(...) or exportCsv(...). In tests, pass a custom Directory.
    • Or let the package pick a user-visible path:
    final jsonFile = await ExportService.exportJsonToPickedDir(AiDebugger.getReports());
    final csvFile  = await ExportService.exportCsvToPickedDir(AiDebugger.getReports());
    

Platform paths and sharing

  • Android: uses app-specific external storage when available; generally visible in file managers.
  • iOS: uses app Documents. To show in Files app, enable file sharing in ios/Runner/Info.plist:
    <key>UIFileSharingEnabled</key><true/>
    <key>LSSupportsOpeningDocumentsInPlace</key><true/>
    
  • Desktop: prefers Downloads, falls back to Documents.

Share or open files right after export:

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

final file = await ExportService.exportCsvToPickedDir(AiDebugger.getReports());
await Share.shareXFiles([XFile(file.path)], text: 'AI error reports');
await OpenFilex.open(file.path);

Notes

  • AiDebugDraggableButton is visible only in debug by default. You can customize visible, onlyInDebug, icon, initialPosition, and size.
  • The dashboard updates live via Hive listenable.

Capturing API exceptions

  • Unhandled async exceptions are captured automatically if your app runs inside a guarded zone (as shown in Quick Start). Ensure network errors throw.
  • If you catch errors yourself (e.g., in repositories or interceptors), call AiDebugger.logError(e, s) so a report is still generated.

Manual try/catch example:

try {
  final res = await http.get(Uri.parse('https://api.example.com'));
  if (res.statusCode >= 400) {
    throw Exception('Request failed: ${res.statusCode}');
  }
} catch (e, s) {
  await AiDebugger.logError(e, s);
}

Dio interceptor example:

dio.interceptors.add(
  InterceptorsWrapper(
    onError: (e, handler) async {
      await AiDebugger.logError(e, e.stackTrace);
      handler.next(e);
    },
  ),
);

Testing API error capture

Inject a fake Gemini service and a temp Hive path, then throw an async error inside a guarded zone:

import 'dart:async';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_ai_debugger/flutter_ai_debugger.dart';
import 'package:flutter_gemini/flutter_gemini.dart';
import 'package:hive/hive.dart';

class FakeGeminiService extends GeminiService {
  @override
  Future<AiErrorReport> analyze({
    required String id,
    required String error,
    required String stack,
    String? modelName,
    List<SafetySetting>? safetySettings,
    GenerationConfig? generationConfig,
  }) async {
    return AiErrorReport(
      id: id,
      originalError: error,
      stackTrace: stack,
      explanation: 'api error captured',
      possibleCauses: const [],
      fixes: const [],
      createdAt: DateTime.now(),
    );
  }
}

void main() {
  test('captures unhandled API exception via zone', () async {
    final temp = await Directory.systemTemp.createTemp();
    Hive.init(temp.path);
    Hive.registerAdapter(AiErrorReportAdapter());
    await Hive.openBox<AiErrorReport>('ai_error_reports');

    await AiDebugger.init(
      const AiDebugConfig(apiKey: 'test'),
      service: FakeGeminiService(),
      hive: Hive,
      hivePath: temp.path,
    );

    final done = Completer<void>();

    runZonedGuarded(() async {
      Future<void>.delayed(const Duration(milliseconds: 10), () {
        throw Exception('API failed');
      });
      await Future.delayed(const Duration(milliseconds: 50));
      done.complete();
    }, (e, s) => AiDebugger.logError(e, s));

    await done.future;

    final last = AiDebugger.getLastReport();
    expect(last, isNotNull);
    expect(last!.explanation, 'api error captured');
  });
}