Change initialization and logging
This commit is contained in:
67
lib/core/analytics/analytics_controller.dart
Normal file
67
lib/core/analytics/analytics_controller.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hiddify/core/analytics/analytics_filter.dart';
|
||||
import 'package:hiddify/core/analytics/analytics_logger.dart';
|
||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||
import 'package:hiddify/core/logger/logger_controller.dart';
|
||||
import 'package:hiddify/core/model/environment.dart';
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'analytics_controller.g.dart';
|
||||
|
||||
const String enableAnalyticsPrefKey = "enable_analytics";
|
||||
|
||||
bool _testCrashReport = false;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AnalyticsController extends _$AnalyticsController with AppLogger {
|
||||
@override
|
||||
bool build() {
|
||||
return _preferences.getBool(enableAnalyticsPrefKey) ?? true;
|
||||
}
|
||||
|
||||
SharedPreferences get _preferences =>
|
||||
ref.read(sharedPreferencesProvider).requireValue;
|
||||
|
||||
Future<void> enableAnalytics() async {
|
||||
loggy.debug("enabling analytics");
|
||||
if (!state) {
|
||||
await _preferences.setBool(enableAnalyticsPrefKey, true);
|
||||
}
|
||||
|
||||
final env = ref.read(environmentProvider);
|
||||
final appInfo = await ref.read(appInfoProvider.future);
|
||||
final dsn = !kDebugMode || _testCrashReport ? Environment.sentryDSN : "";
|
||||
final sentryLogger = SentryLoggyIntegration();
|
||||
LoggerController.instance.addPrinter("analytics", sentryLogger);
|
||||
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = dsn;
|
||||
options.environment = env.name;
|
||||
options.dist = appInfo.release.name;
|
||||
options.debug = kDebugMode;
|
||||
options.enableNativeCrashHandling = true;
|
||||
options.enableNdkScopeSync = true;
|
||||
options.attachThreads = true;
|
||||
options.tracesSampleRate = 0.20;
|
||||
options.enableUserInteractionTracing = true;
|
||||
options.addIntegration(sentryLogger);
|
||||
options.beforeSend = sentryBeforeSend;
|
||||
},
|
||||
);
|
||||
|
||||
state = true;
|
||||
}
|
||||
|
||||
Future<void> disableAnalytics() async {
|
||||
loggy.debug("disabling analytics");
|
||||
await _preferences.setBool(enableAnalyticsPrefKey, false);
|
||||
await Sentry.close();
|
||||
LoggerController.instance.removePrinter("analytics");
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
29
lib/core/analytics/analytics_filter.dart
Normal file
29
lib/core/analytics/analytics_filter.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
FutureOr<SentryEvent?> sentryBeforeSend(SentryEvent event, {Hint? hint}) {
|
||||
if (canSendEvent(event.throwable)) return event;
|
||||
return null;
|
||||
}
|
||||
|
||||
bool canSendEvent(dynamic throwable) {
|
||||
return switch (throwable) {
|
||||
UnexpectedFailure(:final error) => canSendEvent(error),
|
||||
DioException _ => false,
|
||||
SocketException _ => false,
|
||||
HttpException _ => false,
|
||||
HandshakeException _ => false,
|
||||
ExpectedFailure _ => false,
|
||||
ExpectedMeasuredFailure _ => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
bool canLogEvent(dynamic throwable) => switch (throwable) {
|
||||
ExpectedMeasuredFailure _ => true,
|
||||
_ => canSendEvent(throwable),
|
||||
};
|
||||
98
lib/core/analytics/analytics_logger.dart
Normal file
98
lib/core/analytics/analytics_logger.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:hiddify/utils/sentry_utils.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
// modified version of https://github.com/getsentry/sentry-dart/tree/main/logging
|
||||
class SentryLoggyIntegration extends LoggyPrinter
|
||||
implements Integration<SentryOptions> {
|
||||
SentryLoggyIntegration({
|
||||
LogLevel minBreadcrumbLevel = LogLevel.info,
|
||||
LogLevel minEventLevel = LogLevel.error,
|
||||
}) : _minBreadcrumbLevel = minBreadcrumbLevel,
|
||||
_minEventLevel = minEventLevel;
|
||||
|
||||
final LogLevel _minBreadcrumbLevel;
|
||||
final LogLevel _minEventLevel;
|
||||
|
||||
late Hub _hub;
|
||||
|
||||
@override
|
||||
void call(Hub hub, SentryOptions options) {
|
||||
_hub = hub;
|
||||
options.sdk.addIntegration('LoggyIntegration');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
|
||||
bool _shouldLog(LogLevel logLevel, LogLevel minLevel) {
|
||||
if (logLevel == LogLevel.off) {
|
||||
return false;
|
||||
}
|
||||
return logLevel.priority >= minLevel.priority;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLog(LogRecord record) async {
|
||||
if (!canLogEvent(record.error)) return;
|
||||
|
||||
if (_shouldLog(record.level, _minEventLevel)) {
|
||||
await _hub.captureEvent(
|
||||
record.toEvent(),
|
||||
stackTrace: record.stackTrace,
|
||||
hint: Hint.withMap({TypeCheckHint.record: record}),
|
||||
);
|
||||
}
|
||||
|
||||
if (_shouldLog(record.level, _minBreadcrumbLevel)) {
|
||||
await _hub.addBreadcrumb(
|
||||
record.toBreadcrumb(),
|
||||
hint: Hint.withMap({TypeCheckHint.record: record}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LogRecordX on LogRecord {
|
||||
Breadcrumb toBreadcrumb() {
|
||||
return Breadcrumb(
|
||||
category: 'log',
|
||||
type: 'debug',
|
||||
timestamp: time.toUtc(),
|
||||
level: level.toSentryLevel(),
|
||||
message: message,
|
||||
data: <String, Object>{
|
||||
if (object != null) 'LogRecord.object': object!,
|
||||
if (error != null) 'LogRecord.error': error!,
|
||||
if (stackTrace != null) 'LogRecord.stackTrace': stackTrace!,
|
||||
'LogRecord.loggerName': loggerName,
|
||||
'LogRecord.sequenceNumber': sequenceNumber,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
SentryEvent toEvent() {
|
||||
return SentryEvent(
|
||||
timestamp: time.toUtc(),
|
||||
logger: loggerName,
|
||||
level: level.toSentryLevel(),
|
||||
message: SentryMessage(message),
|
||||
throwable: error,
|
||||
// ignore: deprecated_member_use
|
||||
extra: <String, Object>{
|
||||
if (object != null) 'LogRecord.object': object!,
|
||||
'LogRecord.sequenceNumber': sequenceNumber,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension LogLevelX on LogLevel {
|
||||
SentryLevel? toSentryLevel() => switch (this) {
|
||||
LogLevel.all || LogLevel.debug => SentryLevel.debug,
|
||||
LogLevel.info => SentryLevel.info,
|
||||
LogLevel.warning => SentryLevel.warning,
|
||||
LogLevel.error => SentryLevel.error,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/core/directories/directories_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
LazyDatabase openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbDir = await FilesEditorService.getDatabaseDirectory();
|
||||
final dbDir = await AppDirectories.getDatabaseDirectory();
|
||||
final file = File(p.join(dbDir.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
|
||||
56
lib/core/directories/directories_provider.dart
Normal file
56
lib/core/directories/directories_provider.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hiddify/core/model/directories.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'directories_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class AppDirectories extends _$AppDirectories with InfraLogger {
|
||||
final _methodChannel = const MethodChannel("app.hiddify.com/platform");
|
||||
|
||||
@override
|
||||
Future<Directories> build() async {
|
||||
final Directories dirs;
|
||||
if (Platform.isIOS) {
|
||||
final paths = await _methodChannel.invokeMethod<Map>("get_paths");
|
||||
loggy.debug("paths: $paths");
|
||||
dirs = (
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
);
|
||||
} else {
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
final workingDir =
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
dirs = (
|
||||
baseDir: baseDir,
|
||||
workingDir: workingDir!,
|
||||
tempDir: tempDir,
|
||||
);
|
||||
}
|
||||
|
||||
if (!dirs.baseDir.existsSync()) {
|
||||
await dirs.baseDir.create(recursive: true);
|
||||
}
|
||||
if (!dirs.workingDir.existsSync()) {
|
||||
await dirs.workingDir.create(recursive: true);
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
static Future<Directory> getDatabaseDirectory() async {
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
return getLibraryDirectory();
|
||||
} else if (Platform.isWindows || Platform.isLinux) {
|
||||
return getApplicationSupportDirectory();
|
||||
}
|
||||
return getApplicationDocumentsDirectory();
|
||||
}
|
||||
}
|
||||
33
lib/core/logger/custom_logger.dart
Normal file
33
lib/core/logger/custom_logger.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:loggy/loggy.dart';
|
||||
|
||||
class FileLogPrinter extends LoggyPrinter {
|
||||
FileLogPrinter(
|
||||
String filePath, {
|
||||
this.minLevel = LogLevel.debug,
|
||||
}) : _logFile = File(filePath);
|
||||
|
||||
final File _logFile;
|
||||
final LogLevel minLevel;
|
||||
|
||||
late final _sink = _logFile.openWrite(
|
||||
mode: FileMode.writeOnly,
|
||||
);
|
||||
|
||||
@override
|
||||
void onLog(LogRecord record) {
|
||||
final time = record.time.toIso8601String().split('T')[1];
|
||||
_sink.writeln("$time - $record");
|
||||
if (record.error != null) {
|
||||
_sink.writeln(record.error);
|
||||
}
|
||||
if (record.stackTrace != null) {
|
||||
_sink.writeln(record.stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_sink.close();
|
||||
}
|
||||
}
|
||||
30
lib/core/logger/logger.dart
Normal file
30
lib/core/logger/logger.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
|
||||
class Logger {
|
||||
static final app = Loggy("app");
|
||||
static final bootstrap = Loggy("bootstrap");
|
||||
|
||||
static void logFlutterError(FlutterErrorDetails details) {
|
||||
if (details.silent) {
|
||||
return;
|
||||
}
|
||||
|
||||
final description = details.exceptionAsString();
|
||||
|
||||
app.error(
|
||||
'Flutter Error: $description',
|
||||
details.exception,
|
||||
details.stack,
|
||||
);
|
||||
}
|
||||
|
||||
static bool logPlatformDispatcherError(Object error, StackTrace stackTrace) {
|
||||
app.error(
|
||||
'PlatformDispatcherError: $error',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
62
lib/core/logger/logger_controller.dart
Normal file
62
lib/core/logger/logger_controller.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_loggy/flutter_loggy.dart';
|
||||
import 'package:hiddify/core/logger/custom_logger.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:loggy/loggy.dart';
|
||||
|
||||
class LoggerController extends LoggyPrinter with InfraLogger {
|
||||
LoggerController(
|
||||
this.consolePrinter,
|
||||
this.otherPrinters,
|
||||
);
|
||||
|
||||
final LoggyPrinter consolePrinter;
|
||||
final Map<String, LoggyPrinter> otherPrinters;
|
||||
|
||||
static LoggerController get instance => _instance;
|
||||
|
||||
static late LoggerController _instance;
|
||||
|
||||
static void init(String appLogPath) {
|
||||
_instance = LoggerController(
|
||||
const PrettyDeveloperPrinter(),
|
||||
{"app": FileLogPrinter(appLogPath)},
|
||||
);
|
||||
Loggy.initLoggy(logPrinter: _instance);
|
||||
}
|
||||
|
||||
static Future<void> postInit(bool debugMode) async {
|
||||
final logLevel = debugMode ? LogLevel.all : LogLevel.info;
|
||||
final logToFile = debugMode || (!Platform.isAndroid && !Platform.isIOS);
|
||||
|
||||
if (!logToFile) _instance.removePrinter("app");
|
||||
|
||||
Loggy.initLoggy(
|
||||
logPrinter: _instance,
|
||||
logOptions: LogOptions(logLevel),
|
||||
);
|
||||
}
|
||||
|
||||
void addPrinter(String name, LoggyPrinter printer) {
|
||||
loggy.debug("adding [$name] printer");
|
||||
otherPrinters.putIfAbsent(name, () => printer);
|
||||
}
|
||||
|
||||
void removePrinter(String name) {
|
||||
loggy.debug("removing [$name] printer");
|
||||
final printer = otherPrinters[name];
|
||||
if (printer case FileLogPrinter()) {
|
||||
printer.dispose();
|
||||
}
|
||||
otherPrinters.remove(name);
|
||||
}
|
||||
|
||||
@override
|
||||
void onLog(LogRecord record) {
|
||||
consolePrinter.onLog(record);
|
||||
for (final printer in otherPrinters.values) {
|
||||
printer.onLog(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,23 +70,6 @@ class SilentStartNotifier extends _$SilentStartNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class EnableAnalytics extends _$EnableAnalytics {
|
||||
late final _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"enable_analytics",
|
||||
true,
|
||||
);
|
||||
|
||||
@override
|
||||
bool build() => _pref.getValue();
|
||||
|
||||
Future<void> update(bool value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class DisableMemoryLimit extends _$DisableMemoryLimit {
|
||||
late final _pref = Pref(
|
||||
|
||||
Reference in New Issue
Block a user