Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Save logging to cache directory #196

Merged
merged 3 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ Future<Map<String, dynamic>> loadConfig() async {
}

void main() async {
final crashProvider = CrashLogNotifier();
WidgetsFlutterBinding.ensureInitialized();
final crashProvider = CrashLogNotifier();

if (kIsWeb) {
html.document.onContextMenu.listen((event) => event.preventDefault());
Expand Down
90 changes: 90 additions & 0 deletions lib/models/error_log_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'package:flutter/material.dart';

import 'package:collection/collection.dart';
import 'package:logging/logging.dart';

enum ErrorType {
severe,
warning,
shout,
}

class ErrorLogModel {
final ErrorType type;
final String message;
final DateTime time;
final StackTrace? stackTrace;

const ErrorLogModel({
required this.type,
required this.message,
required this.time,
required this.stackTrace,
});

factory ErrorLogModel.fromLogRecord(LogRecord record) {
late ErrorType type;
if (record.level == Level.WARNING) {
type = ErrorType.warning;
} else if (record.level == Level.SHOUT) {
type = ErrorType.shout;
} else {
type = ErrorType.severe;
}

return ErrorLogModel(
type: type,
message: record.message,
time: record.time,
stackTrace: record.stackTrace,
);
}

String get label {
var join = _label;
if (join.length > 250) {
return "${join.substring(0, 250)}... \n \nTruncated copy log to see more";
} else {
return join;
}
}

String get _label {
return [
type.name.toUpperCase(),
" | ",
message,
].join();
}

String get content => [
time.toIso8601String(),
"\n",
"\n",
stackTrace,
].whereNotNull().join();

String get clipBoard => [_label, content].toString();

Color get color => switch (type) {
ErrorType.severe => Colors.redAccent,
ErrorType.warning => Colors.orange,
ErrorType.shout => Colors.yellowAccent,
};

Map<String, dynamic> toJson() => {
'time': time.toIso8601String(),
'level': type.name,
'message': message,
'stackTrace': stackTrace?.toString(),
};

static ErrorLogModel fromJson(Map<String, dynamic> json) {
return ErrorLogModel(
type: ErrorType.values.firstWhereOrNull((level) => level.name == json['level']) ?? ErrorType.warning,
message: json['message'],
stackTrace: json['stackTrace'] != null ? StackTrace.fromString(json['stackTrace']) : null,
time: DateTime.parse(json['time']),
);
}
}
103 changes: 46 additions & 57 deletions lib/providers/crash_log_provider.dart
Original file line number Diff line number Diff line change
@@ -1,75 +1,32 @@
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';

enum ErrorType {
severe,
warning,
shout,
}

class ErrorViewModel {
final LogRecord rec;

const ErrorViewModel({required this.rec});

ErrorType get type {
if (rec.level == Level.WARNING) {
return ErrorType.warning;
}
if (rec.level == Level.SHOUT) {
return ErrorType.shout;
}
return ErrorType.severe;
}

String get label {
var join = [
type.name.toUpperCase(),
" | ",
rec.message,
].join();
if (join.length > 250) {
return "${join.substring(0, 80)}... \n \nTruncated copy log to see more";
} else {
return join;
}
}

String get content => [
rec.time.toIso8601String(),
"\n",
"\n",
rec.stackTrace,
].whereNotNull().join();

String get clipBoard => [label, content].toString();

Color get color => switch (type) {
ErrorType.severe => Colors.redAccent,
ErrorType.warning => Colors.orange,
ErrorType.shout => Colors.yellowAccent,
};
}
import 'package:fladder/models/error_log_model.dart';

final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorViewModel>>((ref) => CrashLogNotifier());
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorLogModel>>((ref) => CrashLogNotifier());

class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
class CrashLogNotifier extends StateNotifier<List<ErrorLogModel>> {
CrashLogNotifier() : super([]) {
init();
}

late final Logger logger;
final maxLength = 100;
final maxLength = 50;
String? logFilePath;

void init() {
void init() async {
logger = Logger.root;
logger.level = Level.ALL;
logger.onRecord.listen(logPrint);

FlutterError.onError = (FlutterErrorDetails details) => logFile(details);

PlatformDispatcher.instance.onError = (error, stack) {
logFile(FlutterErrorDetails(
exception: error,
Expand All @@ -78,27 +35,59 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
));
return false;
};

if (!kIsWeb) {
await _initializeLogFile();
await _loadLogsFromFile();
}
}

Future<void> _initializeLogFile() async {
final directory = await getApplicationCacheDirectory();
logFilePath = '${directory.path}/crash_logs.json';
}

Future<void> _loadLogsFromFile() async {
if (logFilePath == null) return;
final file = File(logFilePath!);
if (await file.exists()) {
final content = await file.readAsString();
final List<dynamic> jsonData = jsonDecode(content);
state = jsonData.map((json) => ErrorLogModel.fromJson(json)).toList();
}
}

Future<void> _saveLogsToFile() async {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): File operations should include error handling for I/O exceptions

Add try-catch blocks to handle potential file system errors and implement appropriate error recovery

if (logFilePath == null) return;
final file = File(logFilePath!);
final jsonData = state.map((log) => log.toJson()).toList();
await file.writeAsString(jsonEncode(jsonData));
}

void clearLogs() {
state = [];
if (!kIsWeb) {
_saveLogsToFile();
}
}

void logPrint(LogRecord rec) {
if (kDebugMode) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
}
if (rec.level > Level.INFO) {
state = [ErrorViewModel(rec: rec), ...state];
state = [ErrorLogModel.fromLogRecord(rec), ...state];
if (state.length >= maxLength) {
state = state.sublist(0, maxLength);
}
if (!kIsWeb) {
_saveLogsToFile();
}
}
}

void logFile(FlutterErrorDetails details) {
logger.severe('Flutter error: ${details.exception}', details.exception, details.stack);

if (details.stack != null && kDebugMode) {
print('${details.stack}');
}
Expand Down
1 change: 1 addition & 0 deletions lib/screens/crash_screen/crash_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/services.dart';

import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:fladder/models/error_log_model.dart';
import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/list_padding.dart';
Expand Down
7 changes: 4 additions & 3 deletions lib/screens/shared/detail_scaffold.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
),
Padding(
padding: EdgeInsets.only(
bottom: 0,
left: MediaQuery.of(context).padding.left,
top: MediaQuery.of(context).padding.top + 50),
bottom: 0,
left: MediaQuery.of(context).padding.left,
top: MediaQuery.of(context).padding.top,
),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height,
Expand Down
Loading