[tips] FirebaseCrashlyticsによるユーザ体験向上の仕組みづくり(Logic Error)

# はじめに
株式会社withでiOSエンジニアをやっている　@PictoMki　です!
withのiOSチームでFirebaseCrashlyticsを使用して導入している仕組みがあるのですが、
数ヶ月運用してみて、導入効果を少し実感できたので紹介させていただきます!

※[こちらの記事(Carefully Unwrapping)](https://qiita.com/zrn-ns/items/e9fa2836aea1b274f852)は姉妹記事になります。

# 背景
## Carefully Unwrappingの登場
元の背景としてはwithアプリにて特定の条件によってクラッシュが起き始めたことがきっかけになります。
直接の原因としてはForce Unwrappingによるものだったので、上記の記事であるCarefully Unwrappingが登場しました。

:::note info
Carefully Unwrappingの概要
- nilでない場合は通常通りアンラップする
- nilの場合はCrashlyticsにerrorを送信する かつ デフォルト値を渡してアンラップする
- 詳しくははじめにの記事をお読みください
:::

こちらの導入により、安全にアンラップできるかつ意図しないデータが渡された場合は
Crashlyticsに送信されるようになったため、早期発見できるようになりました。

## LogicErrorの登場
Carefully Unwrappingの導入によりアンラップ時の意図しない挙動は築けるようになりましたが、
以下の問題点がありました。

- アンラップの時しか使えない
    - guardを使っている部分にも使いたい
    - if文でアプリの仕様と異なる動きをしている部分にも使いたい

- オプショナルの型が決まっていない場合にデフォルト値を設定しづらい
    - classの継承を意識しなければいけなくなってしまう

- デフォルト値でアプリが正常に動作し続けられないかもしれない

上記を解決するために登場したのが今回のLogicErrorになります。

# 実装
## LogicError
```LogicError.swift
import Foundation
import FirebaseCrashlytics

/// ロジックエラーが発生したときに呼び出すと、Crashlyticsにエラー情報を通知した上で、デバッグ環境であればクラッシュさせる。
/// （assertionFailure(_ description: )のラッパー的メソッド）
public func logicError(_ description: String? = nil, needsCrashOnDevelopEnvironment: Bool = true, originFilePath: String = #file, originFileLine: Int = #line, originMethodName: String = #function) {
    let error = LogicError(description: description,
                           originFilePath: originFilePath,
                           originFileLine: originFileLine,
                           originMethodName: originMethodName)
    let infoDic: [String: Any]? = {
        if let _description = error.description {
            // CrashlyticsがDictionary型を求めるので適当なキーでDictionary化
            return ["value": _description]
        } else {
            return nil
        }
    }()
    let nsError: NSError = {
        let tempError = error as NSError
        return NSError(domain: tempError.domain,
                       code: tempError.code,
                       userInfo: infoDic)
    }()
    Crashlytics.crashlytics().record(error: nsError)

    // 開発環境でクラッシュさせたくない時は呼び出し側で制御する
    if needsCrashOnDevelopEnvironment {
        assertionFailure(description ?? "")
    }
}

/// ロジックエラーの発生を雑にFirebaseに通知するためのエラー型。
/// loginError()に渡すと、Crashlyticsにエラーの発生箇所、説明文が記録される。
///
/// 例)
/// ```
/// guard idx < list.count else {
///     loginError("範囲外へのアクセス")
///     return
/// }
/// list[idx]
/// ```
private struct LogicError: Error {

    init(description: String? = nil, originFilePath: String = #file, originFileLine: Int = #line, originMethodName: String = #function) {
        self.description = description
        self.originFilePath = originFilePath
        self.originFileLine = originFileLine
        self.originMethodName = originMethodName
    }

    var _domain: String {
        let originFileName: String = originFilePath.components(separatedBy: "/").last ?? originFilePath
        return "\(String(describing: Self.self))(\(originFileName):\(originFileLine) >>> \(originMethodName))"
    }

    let description: String?
    let originFilePath: String
    let originFileLine: Int
    let originMethodName: String
}

```

## 使い方


```.swift
let compNumber: Int = 11

// if文などのロジックで使う
if compNumber > 10 {
    logicError("compNumberが10より大きい compNumber:\(compNumber)")
}

var nilNumber: Int?

// アンラップの時に使う
if let number = nilNumber {
    print(number)
} else {
    logicError("nilNumberがnil")
}

// guard文で使う
guard let number = nilNumber else {
    logicError("nilNumberがnil")
    return
}
print(number)
```

上記のようにアンラップ時だけではなく、より柔軟にCrashlyticsへのエラー通知が行えるようになりました。

## エラー確認
### 一覧での確認
今回テストプロジェクトにてAppDelegate.swiftの34行目と35行目で試しにlogicErrorを起こしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514870/b87a7883-6a79-adc1-8d63-4cc20b622d3a.png)

上記の画像にようにファイル名、行数、実行関数が一覧にてわかります。

### 詳細にて内容確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514870/713a41b9-5f75-fea1-c914-afff3d857577.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514870/138908d0-41f2-274e-c506-b11cb83e5ce4.png)

上記のように値が取れるようになります。

# iOSチームでの運用

私たちのチームではissue棚卸し会という名目で、
定期的にチームメンバーでCrashlyticsを見る機会を作っています。

Crashlyticsの中で発生件数が多いものに関しては、Github issueを用いてissue化し
Crashlyticsにメモを残し、時間がある時に調査と修正を行っています。(logicError以外のエラーも含む)

↓メモ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514870/a2e6a6ce-6dbb-7e0c-9200-d96e93b238eb.png" width="200px">

具体的な事例は今回提示できないのですが、再現手順が難しくユーザー環境で起こるエラーの
問題の切り分けにlogicErrorが役立っています！


# 課題
特に大きな課題感はないのですが、将来的には以下のようなことは起こり得るかなと思います。

## 課題1. logicErrorが増えると見るのが大変

気軽に追加できるのでlogicErrorの件数が増えると、
Crashlyticsの一覧がlogicErrorに占領されてしまうケースが発生してしまう可能性があります。

解決策として、以下を考えています。

1. logicError判別用のカスタムキーを追加する
2. 一覧にvalueの値(nilNumberがnil)を表示する(現在は行数、クラス、関数を表示)

現状は特にlogicErrorに支配されているわけではないので、
増えてきた場合には上記の対策を取ろうと思います。

## 課題2. 同じlogicErrorが重複する可能性

現在のlogicErrorの実装的にファイル名、行数、関数が同じ箇所でのlogicErrorを同じエラーとして認識して記録されるため、
コード改修を行った場合は同じエラーが別のエラーとして記録されるようになってしまいます。

こちらについてはlogicErrorによって多く検出されたものに関して長い間放置するわけではないので、
コード改修が起こる前にlogicErrorが起きる箇所は直していくため、今のところは特に課題感は感じていないです。

将来的に追い切れないくらい重複してしまう場合は検討が必要かなと思います。

# 最後に
今回はチームで行っているCrashlytics運用のtipsを紹介しました。
今後もサービスの改善に向けてチーム一同精進していきます。