diff --git a/pubspec.lock b/pubspec.lock index 216d4269d..abf81ca1d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -752,10 +752,10 @@ packages: super_logging: dependency: "direct main" description: - name: super_logging - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" + path: "thirdparty/super_logging" + relative: true + source: path + version: "1.3.4" supercharged: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 615abdfde..7f2cb6ca1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,8 @@ dependencies: visibility_detector: ^0.1.5 event_bus: ^1.1.1 sentry: ">=3.0.0 <4.0.0" - super_logging: ^2.0.1 + super_logging: + path: thirdparty/super_logging archive: ^2.0.11 flutter_email_sender: ^3.0.1 like_button: ^0.2.0 diff --git a/thirdparty/super_logging/.gitignore b/thirdparty/super_logging/.gitignore new file mode 100644 index 000000000..3949bb17d --- /dev/null +++ b/thirdparty/super_logging/.gitignore @@ -0,0 +1,239 @@ +.DS_Store +.dart_tool/ + +flutter_export_environment.sh +.flutter-plugins-dependencies + +.packages +.pub/ + +build/ +ios/.generated/ +ios/Flutter/Generated.xcconfig +ios/Runner/GeneratedPluginRegistrant.* + +**/.idea +.flutter-plugins + +android/ + +# Created by https://www.gitignore.io/api/dart,android,androidstudio +# Edit at https://www.gitignore.io/?templates=dart,android,androidstudio + +### Android ### +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches +# Android Studio 3 in .gitignore file. +.idea/caches/build_file_checksums.ser +.idea/modules.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +### Android Patch ### +gen-external-apklibs +output.json + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*~ +*.swp + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### Dart ### +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +# End of https://www.gitignore.io/api/dart,android,androidstudio diff --git a/thirdparty/super_logging/.metadata b/thirdparty/super_logging/.metadata new file mode 100644 index 000000000..281f231a6 --- /dev/null +++ b/thirdparty/super_logging/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8661d8aecd626f7f57ccbcb735553edc05a2e713 + channel: stable + +project_type: package diff --git a/thirdparty/super_logging/LICENSE b/thirdparty/super_logging/LICENSE new file mode 100644 index 000000000..ce2c3c296 --- /dev/null +++ b/thirdparty/super_logging/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 PyCampers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/super_logging/README.md b/thirdparty/super_logging/README.md new file mode 100644 index 000000000..1434a922f --- /dev/null +++ b/thirdparty/super_logging/README.md @@ -0,0 +1,54 @@ +# Super Logging + +[![Sponsor](https://img.shields.io/badge/Sponsor-jaaga_labs-red.svg?style=for-the-badge)](https://www.jaaga.in/labs) + +[![pub package](https://img.shields.io/pub/v/super_logging.svg?style=for-the-badge)](https://pub.dartlang.org/packages/super_logging) + +This package lets you easily log to: +- stdout +- disk +- sentry.io + +```dart +import 'package:super_logging/super_logging.dart'; +import 'package:logging/logging.dart'; + +final logger = Logger("main"); + +main() async { + // just call once, and let it handle the rest! + await SuperLogging.main(); + + logger.info("hello!"); +} +``` + +(Above example will log to stdout and disk.) + +## Logging to sentry.io + +Just specify your sentry DSN. + +```dart +SuperLogging.main(LogConfig( + sentryDsn: 'https://xxxx@sentry.io/yyyy', +)); +``` + +## Log uncaught errors + +Just provide the contents of your `main()` function to super logging. + +```dart +void main() { + SuperLogging.main(LogConfig( + body: _main, + )); +} + +void _main() { + runApp(MyApp()); +} +``` + +[Read the docs](https://pub.dev/documentation/super_logging/latest/super_logging/super_logging-library.html) to know about more customization options. diff --git a/thirdparty/super_logging/lib/super_logging.dart b/thirdparty/super_logging/lib/super_logging.dart new file mode 100644 index 000000000..f11bbf16c --- /dev/null +++ b/thirdparty/super_logging/lib/super_logging.dart @@ -0,0 +1,323 @@ +library super_logging; + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:logging/logging.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sentry/sentry.dart'; + +export 'package:sentry/sentry.dart' show User; + +typedef FutureOr FutureOrVoidCallback(); + +extension SuperString on String { + Iterable chunked(int chunkSize) sync* { + var start = 0; + + while (true) { + var stop = start + chunkSize; + if (stop > length) break; + yield substring(start, stop); + start = stop; + } + + if (start < length) { + yield substring(start); + } + } +} + +extension SuperLogRecord on LogRecord { + String toPrettyString([String extraLines]) { + var header = "[$loggerName] [$level] [$time]"; + + var msg = "$header $message"; + + if (error != null) { + msg += "\n⤷ type: ${error.runtimeType}\n⤷ error: $error"; + } + if (stackTrace != null) { + msg += "\n⤷ trace: $stackTrace"; + } + + for (var line in extraLines?.split('\n') ?? []) { + msg += '\n$header $line'; + } + + return msg; + } + + Event toEvent({String appVersion}) { + return Event( + release: appVersion, + level: SeverityLevel.error, + culprit: message, + loggerName: loggerName, + exception: error, + stackTrace: stackTrace, + ); + } +} + +class LogConfig { + /// The DSN for a Sentry app. + /// This can be obtained from the Sentry apps's "settings > Client Keys (DSN)" page. + /// + /// Only logs containing errors are sent to sentry. + /// Errors can be caught using a try-catch block, like so: + /// + /// ``` + /// final logger = Logger("main"); + /// + /// try { + /// // do something dangerous here + /// } catch(e, trace) { + /// logger.info("Huston, we have a problem", e, trace); + /// } + /// ``` + /// + /// If this is [null], Sentry logger is completely disabled (default). + String sentryDsn; + + /// A built-in retry mechanism for sending errors to sentry. + /// + /// This parameter defines the time to wait for, before retrying. + Duration sentryRetryDelay; + + /// Path of the directory where log files will be stored. + /// + /// If this is [null], file logging is completely disabled (default). + /// + /// If this is an empty string (['']), + /// then a 'logs' directory will be created in [getTemporaryDirectory()]. + /// + /// A non-empty string will be treated as an explicit path to a directory. + /// + /// The chosen directory can be accessed using [SuperLogging.logFile.parent]. + String logDirPath; + + /// The maximum number of log files inside [logDirPath]. + /// + /// One log file is created per day. + /// Older log files are deleted automatically. + int maxLogFiles; + + /// Whether to enable super logging features in debug mode. + /// + /// Sentry and file logging are typically not needed in debug mode, + /// where a complete logcat is available. + bool enableInDebugMode; + + /// If provided, super logging will invoke this function, and + /// any uncaught errors during its execution will be reported. + /// + /// Works by using [FlutterError.onError] and [runZoned]. + FutureOrVoidCallback body; + + /// The date format for storing log files. + /// + /// `DateFormat('y-M-d')` by default. + DateFormat dateFmt; + + LogConfig({ + this.sentryDsn, + this.sentryRetryDelay = const Duration(seconds: 30), + this.logDirPath, + this.maxLogFiles = 10, + this.enableInDebugMode = false, + this.body, + this.dateFmt, + }) { + dateFmt ??= DateFormat("y-M-d"); + } +} + +class SuperLogging { + /// The logger for SuperLogging + static final $ = Logger('ente_logging'); + + /// The current super logging configuration + static LogConfig config; + + static Future main([LogConfig config]) async { + config ??= LogConfig(); + SuperLogging.config = config; + + WidgetsFlutterBinding.ensureInitialized(); + + appVersion ??= await getAppVersion(); + + final enable = config.enableInDebugMode || kReleaseMode; + sentryIsEnabled = enable && config.sentryDsn != null; + fileIsEnabled = enable && config.logDirPath != null; + + if (fileIsEnabled) { + await setupLogDir(); + } + if (sentryIsEnabled) { + sentryUploader(); + } + + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen(onLogRecord); + + if (!enable) { + $.info("detected debug mode; sentry & file logging disabled."); + } + if (fileIsEnabled) { + $.info("using this log file for today: $logFile"); + } + if (sentryIsEnabled) { + $.info("sentry uploader started"); + } + + if (config.body == null) return; + + if (enable) { + FlutterError.onError = (details) { + $.fine( + "uncaught error from FlutterError.onError()", + details.exception, + details.stack, + ); + }; + await runZoned(config.body, onError: (e, trace) { + $.fine("uncaught error from runZoned()", e, trace); + }); + } else { + await config.body(); + } + } + + static var _lastExtraLines = ''; + + static Future onLogRecord(LogRecord rec) async { + // log misc info if it changed + var extraLines = "app version: '$appVersion'\n"; + if (extraLines != _lastExtraLines) { + _lastExtraLines = extraLines; + } else { + extraLines = null; + } + + var str = rec.toPrettyString(extraLines); + + // write to stdout + printLog(str); + + // write to logfile + if (fileIsEnabled) { + final strForLogFile = str + '\n'; + await logFile.writeAsString(strForLogFile, + mode: FileMode.append, flush: true); + } + + // add error to sentry queue + if (sentryIsEnabled && rec.error != null) { + var event = rec.toEvent(appVersion: appVersion); + sentryQueueControl.add(event); + } + } + + // Logs on must be chunked or they get truncated otherwise + // See https://github.com/flutter/flutter/issues/22665 + static var logChunkSize = 800; + + static void printLog(String text) { + text.chunked(logChunkSize).forEach(print); + } + + /// A queue to be consumed by [sentryUploader]. + static final sentryQueueControl = StreamController(); + + /// Whether sentry logging is currently enabled or not. + static bool sentryIsEnabled; + + static Future sentryUploader() async { + var client = SentryClient(dsn: config.sentryDsn); + + await for (final event in sentryQueueControl.stream) { + dynamic error; + + try { + var response = await client.capture(event: event); + error = response.error; + } catch (e) { + error = e; + } + + if (error == null) continue; + $.fine( + "sentry upload failed; will retry after ${config.sentryRetryDelay} ($error)", + ); + doSentryRetry(event); + } + } + + static void doSentryRetry(Event event) async { + await Future.delayed(config.sentryRetryDelay); + sentryQueueControl.add(event); + } + + /// The log file currently in use. + static File logFile; + + /// Whether file logging is currently enabled or not. + static bool fileIsEnabled; + + static Future setupLogDir() async { + var dirPath = config.logDirPath; + + // choose [logDir] + if (dirPath.isEmpty) { + var root = await getExternalStorageDirectory(); + dirPath = '${root.path}/logs'; + } + + // create [logDir] + var dir = Directory(dirPath); + await dir.create(recursive: true); + + var files = []; + var dates = {}; + + // collect all log files with valid names + await for (final file in dir.list()) { + try { + var date = config.dateFmt.parse(basename(file.path)); + dates[file] = date; + } on FormatException {} + } + + // delete old log files, if [maxLogFiles] is exceeded. + if (files.length > config.maxLogFiles) { + // sort files based on ascending order of date (older first) + files.sort((a, b) => dates[a].compareTo(dates[b])); + + var extra = files.length - config.maxLogFiles; + var toDelete = files.sublist(0, extra); + + for (var file in toDelete) { + await file.delete(); + } + } + + logFile = File("$dirPath/${config.dateFmt.format(DateTime.now())}.txt"); + } + + /// Current app version, obtained from package_info plugin. + /// + /// See: [getAppVersion] + static String appVersion; + + static Future getAppVersion() async { + var pkgInfo = await PackageInfo.fromPlatform(); + return "${pkgInfo.version}+${pkgInfo.buildNumber}"; + } +} diff --git a/thirdparty/super_logging/pubspec.yaml b/thirdparty/super_logging/pubspec.yaml new file mode 100644 index 000000000..000d4991d --- /dev/null +++ b/thirdparty/super_logging/pubspec.yaml @@ -0,0 +1,61 @@ +name: super_logging +description: The usual dart logging module, but with superpowers! Let's you easily log to stdout, disk and sentry.io. +version: 1.3.4 +author: Dev Aggarwal +homepage: https://github.com/scientifichackers/super_logging + +environment: + sdk: ">=2.6.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + package_info_plus: ^0.5.0 + device_info: ^0.4.1+4 + logging: ^0.11.4 + sentry: ^3.0.1 + intl: ^0.16.1 + path: ^1.6.4 + path_provider: ^1.6.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + +# To add assets to your package, add an assets section, like this: +# assets: +# - images/a_dot_burr.jpeg +# - images/a_dot_ham.jpeg +# +# For details regarding assets in packages, see +# https://flutter.io/assets-and-images/#from-packages +# +# An image asset can refer to one or more resolution-specific "variants", see +# https://flutter.io/assets-and-images/#resolution-aware. + +# To add custom fonts to your package, add a fonts section here, +# in this "flutter" section. Each entry in this list should have a +# "family" key with the font family name, and a "fonts" key with a +# list giving the asset and other descriptors for the font. For +# example: +# fonts: +# - family: Schyler +# fonts: +# - asset: fonts/Schyler-Regular.ttf +# - asset: fonts/Schyler-Italic.ttf +# style: italic +# - family: Trajan Pro +# fonts: +# - asset: fonts/TrajanPro.ttf +# - asset: fonts/TrajanPro_Bold.ttf +# weight: 700 +# +# For details regarding fonts in packages, see +# https://flutter.io/custom-fonts/#from-packages diff --git a/thirdparty/super_logging/test/test_string_chunked.dart b/thirdparty/super_logging/test/test_string_chunked.dart new file mode 100644 index 000000000..6350c9765 --- /dev/null +++ b/thirdparty/super_logging/test/test_string_chunked.dart @@ -0,0 +1,55 @@ +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:super_logging/super_logging.dart'; + +var random = Random(); + +void main() { + final chunkSize = SuperLogging.logChunkSize; + + test('test with empty text', () { + var text = randomText(0); + + var actual = text.chunked(chunkSize).toList(); + var expected = []; + + expect(expected, actual); + }); + + test('test with length < chunk size', () { + var text = randomText(chunkSize ~/ 2.5); + + var actual = text.chunked(chunkSize).toList(); + var expected = [text]; + + expect(expected, actual); + }); + + test('test with length = chunk size', () { + var text = randomText(chunkSize); + + var actual = text.chunked(chunkSize).toList(); + var expected = [text]; + + expect(expected, actual); + }); + + test('test with length > chunk size', () { + var text = randomText((chunkSize * 2.5).toInt()); + + var actual = text.chunked(chunkSize).toList(); + var expected = [ + text.substring(0, chunkSize), + text.substring(chunkSize, chunkSize * 2), + text.substring(chunkSize * 2) + ]; + + expect(expected, actual); + }); +} + +String randomText(int len) { + var charCodes = List.generate(len, (index) => random.nextInt(0x10FFFF)); + return String.fromCharCodes(charCodes).substring(0, len); +}