crash_heal 0.1.1
crash_heal: ^0.1.1 copied to clipboard
Lightweight Flutter SDK for crash capture, deduplication, local persistence, and periodic sync to CrashHeal.
example/lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:crash_heal/crash_heal.dart';
// Global mock server instance
final mockServer = MockServer();
class MockServer {
HttpServer? _server;
final List<Map<String, dynamic>> receivedPayloads = [];
final StreamController<void> _updateController = StreamController<void>.broadcast();
Stream<void> get updates => _updateController.stream;
int get port => _server?.port ?? 0;
bool get isRunning => _server != null;
Future<void> start() async {
try {
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_server!.listen((HttpRequest request) async {
if (request.method == 'POST' && request.uri.path == '/api/v1/errors/ingest') {
final content = await utf8.decoder.bind(request).join();
try {
final data = jsonDecode(content) as Map<String, dynamic>;
receivedPayloads.insert(0, {
'timestamp': DateTime.now().toIso8601String(),
'headers': {
'x-api-key': request.headers.value('x-api-key') ?? 'None',
'content-type': request.headers.value('content-type') ?? 'None',
'user-agent': request.headers.value('user-agent') ?? 'None',
},
'body': data,
});
_updateController.add(null);
request.response
..statusCode = HttpStatus.created
..headers.contentType = ContentType.json
..write(jsonEncode({
'code': 2003,
'status': true,
'message': 'Error event ingested',
'data': {
'stage': 'INGESTED',
'status': 'PENDING',
'occurrenceCount': 1
}
}))
..close();
} catch (e) {
request.response
..statusCode = HttpStatus.badRequest
..write('Invalid JSON: $e')
..close();
}
} else {
request.response
..statusCode = HttpStatus.notFound
..write('Not Found')
..close();
}
});
} catch (e) {
debugPrint('Error starting mock server: $e');
}
}
Future<void> stop() async {
await _server?.close(force: true);
_server = null;
_updateController.add(null);
}
void clearLogs() {
receivedPayloads.clear();
_updateController.add(null);
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Start the mock server first to bind to a free port
await mockServer.start();
// Bootstrap/Initialize the CrashHeal SDK pointing to our local mock server
try {
await CrashHeal.init(
apiKey: 'ch_dev_test_api_key_998877',
appName: 'Crash Heal Flutter',
syncInterval: const Duration(hours: 1), // Rapid sync interval for interactive testing
enableLogging: true,
);
} catch (e) {
debugPrint('Failed to initialize CrashHeal in main(): $e');
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CrashHeal SDK Testbed',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: const Color(0xFF0F172A), // Slate 900
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1), // Indigo 500
brightness: Brightness.dark,
primary: const Color(0xFF6366F1),
secondary: const Color(0xFF10B981), // Emerald 500
),
cardTheme: CardThemeData(
color: const Color(0xFF1E293B), // Slate 800
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 4,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF0F172A),
elevation: 0,
),
),
home: const DashboardScreen(),
);
}
}
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
List<CrashRecord> _pendingCrashes = [];
Timer? _refreshTimer;
StreamSubscription? _syncSubscription;
StreamSubscription? _mockServerSubscription;
String _sdkStatusMsg = 'Running';
bool _sdkInitialized = true;
// SDK Configuration state variables
final _apiKeyController = TextEditingController(text: 'ch_dev_test_api_key_998877');
final _endpointController = TextEditingController(text: 'https://api.dafuu.uk');
final _envController = TextEditingController(text: 'development');
final _appNameController = TextEditingController(text: 'Crash Heal Flutter');
final _releaseController = TextEditingController(text: '1.0.0+1');
int _syncIntervalSeconds = 5;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadPendingCrashes();
// Periodically check local DB for pending crashes
_refreshTimer = Timer.periodic(const Duration(seconds: 2), (_) {
_loadPendingCrashes();
});
// Listen for sync states to update UI
_syncSubscription = CrashHeal.syncStateStream.listen((state) {
if (mounted) {
setState(() {}); // Trigger refresh of sync status UI
}
});
// Listen to mock server events to reload UI when a request lands
_mockServerSubscription = mockServer.updates.listen((_) {
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
_refreshTimer?.cancel();
_syncSubscription?.cancel();
_mockServerSubscription?.cancel();
_tabController.dispose();
_apiKeyController.dispose();
_endpointController.dispose();
_envController.dispose();
_appNameController.dispose();
_releaseController.dispose();
super.dispose();
}
Future<void> _loadPendingCrashes() async {
if (!_sdkInitialized) return;
try {
final crashes = await CrashHeal.getPendingCrashes();
if (mounted) {
setState(() {
_pendingCrashes = crashes;
});
}
} catch (e) {
debugPrint('Error loading pending crashes: $e');
}
}
Future<void> _restartSdk() async {
try {
await CrashHeal.shutdown();
await CrashHeal.init(
apiKey: _apiKeyController.text,
endpoint: _endpointController.text,
environment: _envController.text,
appName: _appNameController.text,
release: _releaseController.text,
syncInterval: Duration(seconds: _syncIntervalSeconds),
enableLogging: true,
);
setState(() {
_sdkInitialized = true;
_sdkStatusMsg = 'Reinitialized';
});
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('SDK Reinitialized Successfully')),
);
_loadPendingCrashes();
} catch (e) {
setState(() {
_sdkInitialized = false;
_sdkStatusMsg = 'Failed to Init: $e';
});
}
}
// Simulations
void _triggerHandledException() async {
try {
throw StateError('Simulated StateError representing a handled validation issue.');
} catch (error, stack) {
await CrashHeal.captureException(
error,
stack,
handled: true,
tags: {
'user_role': 'tester',
'action': 'click_handled_simulation',
'priority': 'low',
},
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Handled Exception Captured locally.')),
);
_loadPendingCrashes();
}
}
void _triggerUnhandledAsyncException() {
// Thrown in microtask so it bypasses sync call stack and triggers PlatformDispatcher
Future.microtask(() {
throw RangeError.index(5, [1, 2, 3], 'list', 'Index out of bounds during simulation.');
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Unhandled Async Exception thrown. Check local storage soon.')),
);
}
void _triggerZonedGuardedException() {
CrashHeal.runGuarded(() {
throw ArgumentError.value(
'invalid_payload',
'payload',
'Zoned exception thrown inside CrashHeal.runGuarded block.',
);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Zoned Exception thrown. Check local storage soon.')),
);
}
void _triggerFlutterBuildException() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Widget Build Crash Test')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'The widget below will fail its build method, producing a FlutterError.',
textAlign: TextAlign.center,
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Go Back'),
),
const SizedBox(height: 20),
const Expanded(
child: BuggyWidget(),
),
],
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
final syncState = _sdkInitialized ? CrashHeal.currentSyncState : null;
String syncStateStr = 'SDK Off';
Color syncColor = Colors.grey;
bool isSyncing = false;
if (syncState != null) {
if (syncState is CrashSyncIdle) {
syncStateStr = 'Idle (Pending: ${_pendingCrashes.length})';
syncColor = Colors.blue;
} else if (syncState is CrashSyncRunning) {
syncStateStr = 'Syncing...';
syncColor = Colors.amber;
isSyncing = true;
} else if (syncState is CrashSyncSuccess) {
syncStateStr = 'Success';
syncColor = Colors.green;
} else if (syncState is CrashSyncHttpFailure) {
syncStateStr = 'HTTP Error';
syncColor = Colors.red;
} else if (syncState is CrashSyncException) {
syncStateStr = 'Exception';
syncColor = Colors.deepOrange;
}
}
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
appBar: AppBar(
title: isMobile
? const Text(
'CH Sandbox',
style: TextStyle(fontWeight: FontWeight.bold),
)
: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(Icons.healing, color: Color(0xFF6366F1), size: 24),
),
const SizedBox(width: 10),
const Text(
'CrashHeal Sandbox',
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 0.5),
),
],
),
actions: [
Container(
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: mockServer.isRunning ? Colors.green.withOpacity(0.15) : Colors.red.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: mockServer.isRunning ? Colors.green : Colors.red,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: mockServer.isRunning ? Colors.green : Colors.red,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
mockServer.isRunning ? (isMobile ? ':${mockServer.port}' : 'Mock: :${mockServer.port}') : 'Offline',
style: TextStyle(
color: mockServer.isRunning ? Colors.green[200] : Colors.red[200],
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. SDK config & control panel
_buildConfigCard(syncStateStr, syncColor, isSyncing),
const SizedBox(height: 16),
// 2. Exception simulation area
_buildSimulationCard(),
const SizedBox(height: 16),
// 3. Tabbed log view (Local database vs server-received requests)
Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
controller: _tabController,
tabs: [
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isMobile) ...[
const Icon(Icons.storage, size: 16),
const SizedBox(width: 6),
],
Text(isMobile ? 'Cache (${_pendingCrashes.length})' : 'Local Cache (${_pendingCrashes.length})'),
],
),
),
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isMobile) ...[
const Icon(Icons.cloud_upload, size: 16),
const SizedBox(width: 6),
],
Text(isMobile ? 'Server (${mockServer.receivedPayloads.length})' : 'Server Ingestion (${mockServer.receivedPayloads.length})'),
],
),
),
],
indicatorColor: Theme.of(context).colorScheme.primary,
labelColor: Colors.white,
unselectedLabelColor: Colors.grey[400],
),
const SizedBox(height: 12),
SizedBox(
height: 450,
child: TabBarView(
controller: _tabController,
children: [
_buildLocalCrashesTab(),
_buildServerReceivedTab(),
],
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildConfigCard(String syncStateStr, Color syncColor, bool isSyncing) {
final isMobile = MediaQuery.of(context).size.width < 600;
final configInputs = Column(
children: [
_buildTextField(_apiKeyController, 'API Key'),
const SizedBox(height: 10),
_buildTextField(_endpointController, 'Ingestion Endpoint'),
const SizedBox(height: 10),
Row(
children: [
Expanded(child: _buildTextField(_envController, 'Environment')),
const SizedBox(width: 10),
Expanded(
child: DropdownButtonFormField<int>(
value: _syncIntervalSeconds,
decoration: const InputDecoration(
labelText: 'Sync Period',
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(value: 5, child: Text('5 Sec')),
DropdownMenuItem(value: 10, child: Text('10 Sec')),
DropdownMenuItem(value: 30, child: Text('30 Sec')),
DropdownMenuItem(value: 60, child: Text('1 Min')),
],
onChanged: (val) {
if (val != null) {
setState(() {
_syncIntervalSeconds = val;
});
}
},
),
),
],
),
],
);
final syncStatusBox = Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'SYNC STATUS',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.grey),
),
const SizedBox(height: 8),
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: syncColor, shape: BoxShape.circle),
),
const SizedBox(width: 8),
Expanded(
child: Text(
syncStateStr,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
),
if (isSyncing)
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _sdkInitialized
? () async {
await CrashHeal.syncNow();
_loadPendingCrashes();
}
: null,
icon: const Icon(Icons.sync, size: 16),
label: const Text('Sync Now', style: TextStyle(fontSize: 12)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6366F1),
foregroundColor: Colors.white,
),
),
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: () {
throw StateError('Simulated crash triggered by clicking Apply Changes button.');
},
icon: const Icon(Icons.restart_alt, size: 16),
label: const Text('Apply Changes', style: TextStyle(fontSize: 12)),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[200],
),
),
],
),
);
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'SDK CONFIGURATION',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
color: Colors.grey,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _sdkInitialized ? Colors.green.withOpacity(0.15) : Colors.red.withOpacity(0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(
_sdkStatusMsg.toUpperCase(),
style: TextStyle(
color: _sdkInitialized ? Colors.green : Colors.red,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
const Divider(height: 20),
if (isMobile) ...[
configInputs,
const SizedBox(height: 16),
syncStatusBox,
] else ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: configInputs,
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: syncStatusBox,
),
],
),
],
],
),
),
);
}
Widget _buildTextField(TextEditingController controller, String label) {
return TextField(
controller: controller,
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.grey[400], fontSize: 12),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Color(0xFF6366F1), width: 1.5),
),
),
style: const TextStyle(fontSize: 13),
);
}
Widget _buildSimulationCard() {
final isMobile = MediaQuery.of(context).size.width < 600;
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'SIMULATE EXCEPTIONS',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
color: Colors.grey,
),
),
const Divider(height: 20),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: isMobile ? 1 : 2,
childAspectRatio: isMobile ? 3.5 : 2.8,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
children: [
_buildSimButton(
label: 'Handled Exception',
icon: Icons.check_circle,
color: const Color(0xFF10B981), // Emerald
onPressed: _triggerHandledException,
description: 'Manually caught & sent with custom tags',
),
_buildSimButton(
label: 'Unhandled Async',
icon: Icons.bolt,
color: const Color(0xFFF43F5E), // Rose
onPressed: _triggerUnhandledAsyncException,
description: 'Caught via PlatformDispatcher',
),
_buildSimButton(
label: 'Zoned Guarded',
icon: Icons.shield,
color: const Color(0xFFF59E0B), // Amber
onPressed: _triggerZonedGuardedException,
description: 'Caught inside runGuarded zone',
),
_buildSimButton(
label: 'Widget Build Error',
icon: Icons.layers,
color: const Color(0xFF8B5CF6), // Violet
onPressed: _triggerFlutterBuildException,
description: 'Throws inside widget build method',
),
],
),
],
),
),
);
}
Widget _buildSimButton({
required String label,
required IconData icon,
required Color color,
required VoidCallback onPressed,
required String description,
}) {
return InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2), width: 1),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(8)),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 10),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
const SizedBox(height: 2),
Text(
description,
style: TextStyle(color: Colors.grey[400], fontSize: 10),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
Widget _buildLocalCrashesTab() {
if (!_sdkInitialized) {
return const Center(child: Text('SDK is not initialized.'));
}
if (_pendingCrashes.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.storage, size: 48, color: Colors.grey[700]),
const SizedBox(height: 12),
const Text(
'No Pending Crashes in Hive Storage',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
),
const SizedBox(height: 4),
Text(
'Trigger some simulations above to record logs.',
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
),
);
}
return Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () async {
await CrashHeal.clearLocalStorage();
_loadPendingCrashes();
},
icon: const Icon(Icons.delete_outline, size: 16, color: Colors.red),
label: const Text('Clear local db', style: TextStyle(color: Colors.red, fontSize: 12)),
),
),
Expanded(
child: ListView.builder(
itemCount: _pendingCrashes.length,
itemBuilder: (context, index) {
final record = _pendingCrashes[index];
final event = record.event;
final isHandled = event.crash.handled;
return Card(
color: const Color(0xFF0F172A),
margin: const EdgeInsets.only(bottom: 10),
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: isHandled ? Colors.green.withOpacity(0.2) : Colors.red.withOpacity(0.2),
child: Icon(
isHandled ? Icons.check : Icons.warning,
color: isHandled ? Colors.green : Colors.red,
size: 20,
),
),
title: Text(
event.crash.type,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Text(
event.crash.message,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[400], fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isHandled ? Colors.green.withOpacity(0.15) : Colors.red.withOpacity(0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
isHandled ? 'HANDLED' : 'UNHANDLED',
style: TextStyle(
color: isHandled ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
),
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('Occurrences', '${record.occurrenceCount} times'),
_buildDetailRow('First Seen', record.firstSeenAt.toLocal().toString()),
_buildDetailRow('Last Seen', record.lastSeenAt.toLocal().toString()),
_buildDetailRow('Fingerprint', record.fingerprint),
_buildDetailRow('OS / Device', '${event.device.manufacturer} ${event.device.model} (OS: ${event.device.osVersion})'),
if (event.context.tags.isNotEmpty)
_buildDetailRow('Tags', event.context.tags.toString()),
const SizedBox(height: 10),
const Text(
'Stack Trace:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Color(0xFF8B5CF6)),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
event.crash.stacktrace.frames.map((frame) {
return 'at ${frame.className ?? ""}.${frame.function ?? "anonymous"} (${frame.file ?? "unknown"}:${frame.line ?? 0}:${frame.column ?? 0})';
}).join('\n'),
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
),
),
),
],
),
),
],
),
);
},
),
),
],
);
}
Widget _buildServerReceivedTab() {
if (mockServer.receivedPayloads.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.cloud_off, size: 48, color: Colors.grey[700]),
const SizedBox(height: 12),
const Text(
'No Ingested Payloads Yet',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
),
const SizedBox(height: 4),
Text(
'When the SDK synchronizes, JSON requests will display here.',
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
),
);
}
return Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () {
mockServer.clearLogs();
},
icon: const Icon(Icons.clear_all, size: 16, color: Colors.orange),
label: const Text('Clear received logs', style: TextStyle(color: Colors.orange, fontSize: 12)),
),
),
Expanded(
child: ListView.builder(
itemCount: mockServer.receivedPayloads.length,
itemBuilder: (context, index) {
final log = mockServer.receivedPayloads[index];
final body = log['body'] as Map<String, dynamic>;
final exceptionType = body['exceptionType'] as String? ?? 'Crash';
final errorMessage = body['errorMessage'] as String? ?? '';
return Card(
color: const Color(0xFF0F172A),
margin: const EdgeInsets.only(bottom: 10),
child: ExpansionTile(
leading: const CircleAvatar(
backgroundColor: Color(0xFF6366F1),
child: Icon(Icons.file_download, color: Colors.white, size: 20),
),
title: Text(
'POST /errors/ingest ($exceptionType)',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Text(
'Time: ${DateTime.parse(log['timestamp']).toLocal().toString().split('.')[0]} - $errorMessage',
style: TextStyle(color: Colors.grey[400], fontSize: 11),
),
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Request Headers:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.blue),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: Text(
const JsonEncoder.withIndent(' ').convert(log['headers']),
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
),
),
const SizedBox(height: 10),
const Text(
'JSON Payload Body:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.green),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text(
const JsonEncoder.withIndent(' ').convert(body),
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
),
),
),
],
),
),
],
),
);
},
),
),
],
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: TextStyle(color: Colors.grey[400], fontWeight: FontWeight.bold, fontSize: 12),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 12),
),
),
],
),
);
}
}
class BuggyWidget extends StatelessWidget {
const BuggyWidget({super.key});
@override
Widget build(BuildContext context) {
// Intentionally trigger a layout/rendering exception during widget build time
throw StateError('This error is generated during BuggyWidget building! Caught via FlutterError.onError.');
}
}