feature: Save logging to cache directory (#196)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-01-02 11:54:54 +01:00 committed by GitHub
parent ae4707d3a6
commit 1a42be4be0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 143 additions and 62 deletions

View file

@ -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());

View file

@ -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']),
);
}
}

View file

@ -1,75 +1,32 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.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,
}
import 'package:fladder/models/error_log_model.dart';
class ErrorViewModel {
final LogRecord rec;
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorLogModel>>((ref) => CrashLogNotifier());
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,
};
}
final crashLogProvider = StateNotifierProvider<CrashLogNotifier, List<ErrorViewModel>>((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,
@ -78,10 +35,40 @@ 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 {
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) {
@ -89,16 +76,18 @@ class CrashLogNotifier extends StateNotifier<List<ErrorViewModel>> {
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}');
}

View file

@ -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';

View file

@ -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,