LoginSignup
4
4

【Flutter】アプリのクラッシュログまたはエラーを送信する

Last updated at Posted at 2023-05-18

前言

クラッシュレポートなどの回収はFirebase Crashlyticsの利用はおすすめですが、この記事ではFirebase Crashlyticsを利用しなくて、自分のサーバーへ送信したいときの実装案について述べます。

例外キャッチ

  • FlutterError.onError
    主にUIの構築エラーをキャッチできる
  • runZonedGuarded
    null pointerエラー、リストのインデックス超過エラーとかtry catchしていないエラーはrunZonedGuardedでキャッチできる
void main() async {
 await runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    //Flutterでキャッチされた例外/エラー
    FlutterError.onError = (errorDetails) {
      logger.e(errorDetails);
    };
    runApp();
  }, (error, stackTrace) async {
    //Flutterでキャッチされなかった例外/エラー
    logger.e(error);
  });
}

ログ送信

エラーキャッチできたため、アプリの強制終了がなく、WebApi経由でエラーログを送信することが可能です。

  final String apiUrl = 'YOUR_API_URL';

  try {
    await http.post(
      Uri.parse(apiUrl),
      headers: {'Content-Type': 'application/json'},
      body: '{"error": "$errorMessage"}',
    );
  } catch (e) {
    print('送信失敗: $e');
  }

logger

上記のSDKを継承することで、ログを出力処理すると共にログの送信処理を行います。

class RemoteLogOutput extends LogOutput {
  @override
  void output(OutputEvent event) async {
    try {
      final logMessage = event.lines.join('\n');
      print(logMessage);
      // 省略
      // Web Apiでログ送信処理
    } catch (e) {
      print('Failed to send log to remote server: $e');
    }
  }
}

キャッチできなかった例外

キャッチできなかった例外またはクラッシュ発生した時、ログ送信処理終了する前、アプリが強制終了される場合、クラッシュレポートまたはエラーログをローカルに保存し、次回アプリを開くとき、ログ送信処理を行います。

Flutterでクラッシュキャッチできないため、iOSとAndroidそれぞれの実装となります。

  • iOS
    ios/Runner/AppDelegate.swift
    上記ファイルに下記のソースを追加する
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    NSSetUncaughtExceptionHandler { exception in
      // ログを保存する
      AppDelegate.writeCrashLogToFile(exception: exception)
    }

    GeneratedPluginRegistrant.register(with: self)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  // クラッシュ発生時ログファイルをローカルに保存する
  static func writeCrashLogToFile(exception: NSException) {
    // UIApplication.shared.delegate のAppDelegateインスタンスを取得
    guard let self = UIApplication.shared.delegate as? AppDelegate else { return }
    let fileManager = FileManager.default
    let urls = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
    if let crashLogFile = urls.first?.appendingPathComponent("crash_log.txt") {
        let crashLog = """
            Name: \(exception.name.rawValue)
            Reason: \(String(describing: exception.reason))
            UserInfo: \(exception.userInfo)
            StackTrace: \(exception.callStackSymbols.joined(separator: "\n"))

            """
        do {
            if fileManager.fileExists(atPath: crashLogFile.path) {
                let fileHandle = try FileHandle(forWritingTo: crashLogFile)
                fileHandle.seekToEndOfFile()
                if let crashLogData = crashLog.data(using: .utf8) {
                    fileHandle.write(crashLogData)
                }
                fileHandle.closeFile()
            } else {
                try crashLog.write(to: crashLogFile, atomically: true, encoding: .utf8)
            }
        } catch {
            print("Error writing crash log to file: \(error)")
        }
    }
  }
}

  • Android
    • android/app/src/main/AndroidManifest.xml
      上記ファイルにストレージの保存と読み取り権限を追加します。
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    
    • android/app/src/main/kotlin/YOU_APP_ID/CrashLogUtils.kt
      上記のファイルを作成する
    package YOU_APP_ID // 自分のAPPID com.xxx.xxx
    
    import android.content.Context
    import android.util.Log
    import java.io.File
    import java.io.FileOutputStream
    import java.io.PrintWriter
    import java.io.StringWriter
    
    object CrashLogUtils {
        private var isCrashLogSaved = false
    
        fun saveCrashLog(context: Context, crashLog: String) {
            if (isCrashLogSaved) {
                return
            }
    
            var outputStream: FileOutputStream? = null
    
            try {
                val crashLogDir = getCrashLogDirectory(context)
                if (crashLogDir != null) {
                    val crashLogFile = File(crashLogDir, "crash_log.txt")
                    outputStream = FileOutputStream(crashLogFile, true)
                    outputStream.write(crashLog.toByteArray())
                        Log.d("CrashLogUtils", "Crash log saved: " + crashLogFile.absolutePath)
                    isCrashLogSaved = true
                } else {
                    Log.e("CrashLogUtils", "Failed to create crash log directory")
                }
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e("CrashLogUtils", "Error saving crash log: " + e.message)
            } finally {
                outputStream?.close()
            }
        }
    
        private fun getCrashLogDirectory(context: Context): File? {
            val directory = File(context.getExternalFilesDir(null), "crash_logs")
            if (!directory.exists()) {
                if (!directory.mkdirs()) {
                    return null
                }
            }
            return directory
        }
    }
    
    • android/app/src/main/kotlin/YOU_APP_ID/MainActivity.kt
    package YOU_APP_ID // 自分のAPPID com.xxx.xxx
    
    import android.content.Context
    import android.os.Bundle
    import androidx.annotation.NonNull
    import io.flutter.embedding.android.FlutterActivity
    import io.flutter.embedding.engine.FlutterEngine
    import io.flutter.plugins.GeneratedPluginRegistrant
    import java.io.PrintWriter
    import java.io.StringWriter
    
    class MainActivity : FlutterActivity() {
        override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
            GeneratedPluginRegistrant.registerWith(flutterEngine)
            Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(this))
        }
    
        private class CustomUncaughtExceptionHandler(private val context: Context) :
            Thread.UncaughtExceptionHandler {
            override fun uncaughtException(thread: Thread, throwable: Throwable) {
                val crashLog = getStackTraceAsString(throwable)
                CrashLogUtils.saveCrashLog(context, crashLog)
            }
    
            private fun getStackTraceAsString(throwable: Throwable): String {
                val sw = StringWriter()
                val pw = PrintWriter(sw)
                throwable.printStackTrace(pw)
                return sw.toString()
            }
        }
    }
    
    

ストレージから読み取り

クラッシュログの保存と違って、読み取りはFlutterで実現できます。path_providerを使います。

  /// ドキュメント保存ディレクトリ
  static Future<String> getFilePath() async {
    final directory = await getApplicationDocumentsDirectory();
    return directory.path;
  }
  /// 保存したファイルからテキストを取得する
  static Future<String> readFromFile(String fileName) async {
    final path = await getCrashLogFilePath();
    final file = File('$path/$fileName.txt');
    if (await file.exists()) {
      return await file.readAsString();
    }
    return '';
  }

  /// ファイル削除
  static Future<void> deleteFile(String fileName) async {
    final path = await getCrashLogFilePath();
    final file = File('$path/$fileName.txt');
    if (await file.exists()) {
      await file.delete();
    }
  }

  /// ファイル保存ディレクトリ
  static Future<String> getCrashLogFilePath() async {
    if (Platform.isIOS) {
      final path = await getFilePath();
      return path;
    }
    final directory = await getExternalStorageDirectory();
    final crashLogFilePath = '${directory?.path}/crash_logs';
    return crashLogFilePath;
  }
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4