[iOS]Firebase Crashlyticsを使って致命的でないとして回避したイベントを記録する
アプリケーションのエラー設計で、アプリ側のクラッシュは極力避ける方針でコードを実装する場合があります。それでも開発中はクラッシュさせたい場合にassertなどを使うことがあると思います。本番環境でユーザーがアプリを触る時にエラーを踏んで操作は継続するもののエラーを踏んで回避したいことを知りたい場合にCrashlyticsのnon-fatalが便利です。
Swiftのエラー分類
SwiftにはErrorHandlingRationale.rst
というエラーハンドリングに関するドキュメントがあります。そこでは、Simple domain errors
、Recoverbable errors
、Universal errors
、Logic failures
という四つのエラー分類があります。
このタイプのエラーはこれらのどれに分類される、といったような分類の仕方ではなく、どのようにエラーを扱いたいかによってエラーを分類します。使いわけの例自体は示されていますが、実際の運用によってどれに分類するかはエラー設計次第だと思います。
本筋ではないためこの記事では割愛しますがこのエラー分類について、日本語で説明された記事は以下があります。
これらの内Simple domain errorsとLogic failuresが起きた時に、開発中はクラッシュさせて迅速に解決させたいが、本番環境では落とさずに済ませたい。しかしそれらのエラーが生じてしまったことは検知したい場合がありました。
precondition と assert
fatalErrorで落とすと、これまでに書いた要件を満たせないです。Swiftはprecodtionとassertというデバッグ用のツールを提供しています。
満たされると予想される条件に対して自分のコードをチェックする必要があるときに使用できます。満たされない場合は例外がスローされます。
C言語由来(ソースコードにC-style assert with optional message
と記載がある`)の assert() はリリース環境では評価対象にならないので開発のためだけに積極的に使用することができます。
Swiftコンパイラの最適化レベルでいうと-Onone
の時のみ評価が行われます。
preconditionは-Onone
に加えて-O
の時も評価されます。
-Onone
はデバッグビルド時のコンパイラの最適化レベルでリリースビルドの時の最適化レベルは-O
です。
assert(条件式)
、preconditionFailure(条件式)
のように使えますが、評価されると条件式の真偽が偽になったことを表わすのにそれぞれassertionFailureとpreconditionFailureが使えます
最適化レベルやそれぞれの概要についてはソースコードに加えて以下のドキュメントが参考になります。
- Difference between "precondition" and "assert" in swift? - Stack Overflow
- Swift asserts - the missing manual
Crashlyticsの導入
iOSアプリにFirebaseを追加するまで
ガイドに従いFirebaseの初期設定を行います。Info.plistの設定やBunlde Identifierの指定にミスがなければ問題なく動きます。ステップバイステップで動作確認まで行えるのでここで躓くことは少いと思います。
PodfileにFirebase/Analytics
を追加します。
# add the Firebase pod for Google Analytics pod 'Firebase/Analytics' # add pods for any other desired Firebase products # https://firebase.google.com/docs/ios/setup#available-pods
保存後pod install
を叩いてライブラリを導入します。問題なく成功したらAppDelegate.swiftでFirebaseをimportしてapplication(_:didFinishLaunchingWithOptions:)
内でFirebaseAppのstaticメソッド configure()
を呼び出して動作確認を行います。
導入が成功していることを確認できました。
Crashlyticsの有効化
Firebase Consoleの赤枠からCrashlyticsを有効化した後、PodfileにFirebase/Crashlytics
を追加してpod install
します。
pod 'Firebase/Crashlytics'
問題なく終わったらアプリを再コンパイルしておきます。
Crashlyticsの設定をBuild Phasesに追加します。新しくRun scriptを追加して"${PODS_ROOT}/FirebaseCrashlytics/run"
をペーストします。
Input filesに以下の二つを指定します。例なのでこのInput filesは環境変数などをチェックして任意のpathを指定してください。今回はドキュメントに記載のままで問題なかったです。
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME} $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
non-fatal exceptionのレポートを送信
クラッシュレポートのカスタマイズについて扱ったページにnon-fatal exceptionを含んだドキュメントがあります。Crashlyticsの日本語のドキュメントはFabricの頃の記述が残っていて言語設定でEnglishに変更して読みました。
リンク先ではFirebase Crashlyticsのクラッシュレポートをカスタマイズする方法が紹介されています。デフォルトの設定でも自動でクラッシュレポートが集められていくのですが、自動のクラッシュレポート収集をユーザー判断でオフにすることも可能ですし、オプトインのレポートを有効化することもできます。
Firebase Crashlyticsが提供しているロギングの仕組みは以下です。
- Custom keys
- Custom logs
- User identifiers
- Caught exceptions
non-fatalはCaught exceptions
として説明されています。
non-fatal exceptionはrecord(error:)
を使用して NSErrorのインスタンスを渡すことで、致命的ではない例外を記録することができます。 recordError は、[NSThread callStackReturnAddresses] を呼び出すことでスレッドのコールスタックをキャプチャします。
let userInfo = [ NSLocalizedDescriptionKey: NSLocalizedString("The request failed.", comment: ""), NSLocalizedFailureReasonErrorKey: NSLocalizedString("The response returned a 404.", comment: ""), NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString("Does this page exist?", comment: ""), "ProductID": "123456", "View": "MainView" ] let error = NSError(domain: "NSCocoaErrorDomain", code: 1001, userInfo: userInfo) Crashlytics.crashlytics().record(error: error)
これとActive Compilation Coditionsなどでデバッグやリリースによって処理を振り分けられます。
#if DEBUG assertionFailure("The response returned a 404.") #else let userInfo = [ NSLocalizedDescriptionKey: NSLocalizedString("The request failed.", comment: ""), NSLocalizedFailureReasonErrorKey: NSLocalizedString("The response returned a 404.", comment: ""), NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString("Does this page exist?", comment: ""), "ProductID": "123456", "View": "MainView" ] let error = NSError(domain: "NSCocoaErrorDomain", code: 1001, userInfo: userInfo) Crashlytics.crashlytics().record(error: error) #endif
Report non-fatal exceptionsの注意事項
non-fatal exceptionのレポートはrecord(error:)に渡したNSErrorのインスタンスの内domainとcodeを基準にグループ化されます。
そしてこのdomainとcodeにはタイムスタンプのようなユニークな値を使用しないよう警告されています。ユニークな値の格納場所はkey-valueで値が設定できるuserInfoに渡す値で使用しましょう。
加えてパフォーマンスの問題も注意が促されています。コールスタックのキャプチャを行うプロセスでCPUとI/Oのコストがかかります。
まとめ
実装の効率を落とさずに、仕様を満たした上で検知しやすいエラー設計をしたくてドキュメントや公開されているプロジェクトのコード、コミュニティ内のやりとりを見ていますが初めて知ることも多いので勉強になります。