クラシルリワードiOSに導入したデコードエラーを検知する仕組みについて
こんにちは!こんばんは!
家系ラーメンが大好きなdelyでクラシルリワードiOSの開発に携わっている、takkyです!
クラシルリワードのモバイルアプリでは、Karte
やFirebase Remote Config
(今回は便宜上、データ配信サービス
と呼びます)を活用して機能の動的制御やA/Bテストを行っています。(KarteとFirebase Remote Configの詳細に関しては、それぞれリンクを確認ください。)
これらのデータ配信サービスで配信しているデータをクライアントでデコードする際に、デコードエラーが発生した場合にSlackにメッセージを送信して、検知できる仕組みをiOSアプリに導入したので、そちらについて解説します🙋♂️
当時の状況と課題
クラシルリワードではデータ配信サービスを活用して、動的にコンテンツを出しわけしています。
追加の開発を必要としないため、非常に有用なサービスですが、配信のデータ形式としてJsonを用いており、意図しない形式で設定された場合、アプリ側でデコードエラーが発生します。
このデコードエラーを素早く検知できない状況だったため以下の問題が発生していました。
- デコードエラーが発生していることを認識できず、誰も気付けない可能性がある(施策が山ほど走っているので、誰がどこの施策を管理しているかは、開発者は把握しきっていない)
そこで、手作業でJSONを設定して運用している、データ配信サービスを利用する際は、デコードエラーを検知できるように実装を整えました。
課題解決のためのアプローチ
デコードエラーの検知と通知の仕組み
まずは、デコードエラー検知・通知システムの全体像から、フロー図で確認してみましょう。
アプリ内で発生したデコードエラーは以下の流れで最終的にSlackの指定したチャンネルにメッセージが届くようにシステムを構築しています。
全体のシステム図
後述でも少し触れますが、Firebase Crashlyticsから直接Slackにメッセージを送信できれば楽だったのですが、非重大なエラーログのパラメータを閾値に指定してメッセージ送信することができなさそうなので、クエリを書いて定期実行することとしました!
(Cloud Functionを利用すれば出来そうですが、後述のBigQueryからクエリ抽で値を引っ張って出した方がカスタマイズ性も高そうなので、今回はCloudFunctionは利用していないです!)
クラシルリワードアプリ
クラシルリワードアプリ内で行っていることは、データ配信サービスで配信しているJSONを特定の型へのデコードに失敗した場合に、以下のようにCrashlyticsに非重大なエラーを送信する
ことです。
なお、KarteとRemoteConfigで設定した値をデコードする実装自体はほぼ同じなので、Karteの実装のみ記載しています。
クライアント側のCrashlyticsへの非重大なエラー送信処理
/// デコードエラーを引き起こした際は、FirebaseCrashlyticsに非重大なエラーとしてログを送信しするための配信値取得関数
public func getDecodedVariableWithErrorReporting<T: Codable>(variablesKey: KarteVariablesKey) -> T? {
guard let json = getStringVariable(variablesKey)?.data(using: .utf8) else {
return nil
}
return decodeJSON(from: json, for: variablesKey)
}
private func decodeJSON<T: Codable>(from data: Data, for variablesKey: KarteVariablesKey) -> T? {
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
// デコードエラーの場合のみ非重大なエラーをレポートする
if let decodingError = error as? DecodingError {
reportDecodeError(decodingError as NSError, keyName: variablesKey.key)
}
return nil
}
}
/// デコードエラーを送信するための処理
/// Firebase Crashlyticsでは、非重大なエラーはNSErrorを利用してエラーを送信する必要があるため、
/// NSErrorを作成して、エラーレポーティングを行う
private func reportDecodeError(_ error: NSError, keyName: String) {
let additionalInfo: [String: Any] = [
NSLocalizedDescriptionKey: error.localizedDescription,
"Karte Key": "\(keyName)"
]
let mergedUserInfo = error.userInfo.merging(additionalInfo) { _, new in new }
let adjustedNsError = NSError(domain: error.domain, code: error.code, userInfo: mergedUserInfo)
Crashlytics.crashlytics().record(error: adjustedNsError)
}
上記のように、単純にtry JSONDecoder().decode(T.self, from: data)
の処理でエラー理由がDecodingError
であれば、Crashlyticsに非重大なエラーを送信します。
仮にエラーログが送信された時にCrashlyticsダッシュボード画面でKarte Key
にどの配信値でデコードエラーが発生しているかを識別できるようにログを送信していています。(RemoteConfigに設定した値の取得の際にデコードエラーになれば、Remote Config Key
をログに仕込んでいます。)
Firebase Crashlytics & BigQuery
Firebase Crashlyticsでは、基本的に発生したデコードエラーの原因の特定とログデータの蓄積に利用するのみで、そのログデータをBigQueryに連携してデコードエラー数値を抽出できるようにします!
Crashlyticsダッシュボードのイベントキー
エラー発生した際の情報
上記の画像にもある通り、どの箇所でどのような理由でデコードエラーが発生しているか可視化されており、素早く対策できるようになっています。
可視化はできるものの、前述の通り非重大なエラーが閾値を超えてもCrashlyticsから直接Slackにメッセージを送信する機能が存在しないため、Firebase CrashlyticsデータをBigQueryにエクスポートして、BigQueryにエクスポートしたクラッシュリティクスのログにRedashからアクセスして、Slackに通知するようにします。(後続で詳しく解説します。)
Firebase CrashlyticsのログデータをBigQueryでも利用できるようにするには、以下のようにBigQueryにデータをエクスポートするような設定が必要になります。
Firebase CrashlyticsデータをBigQueryにエクスポートする方法
加えて、今回はデコードエラーが発生したというデータはリアルタイムでBigQuery側に反映しておきたいので、ストリーミングエクスポートの設定も有効にしてデータにほぼリアルタイムでアクセスできる状態にする必要があります。
ストリーミングエクスポートを有効にする方法
幸いにもリワードアプリでは別用途で、これらの設定がすでに有効化されていたため、工程としては「Firebase CrashlyticsデータのBigQueryへのエクスポート」と「ストリーミングエクスポート」が有効化されているかの確認のみで済みました!
Redash
Redashでは、Firebase CrashlyticsデータのBigQueryへのエクスポート
されたデータを用いて、Slackへメッセージを送信するためのクエリとアラートを作成する作業が必要です。
デコードエラー数抽出クエリ作成
デコードエラーが起きているキーが閾値以上になったら、クエリは以下の要件に沿って作成しました。
- 特定のRemote ConfigまたはKarteのキーの原因としたデコードエラーの発生傾向を取得
- 同じキーで配信している設定値でデコードエラーが何件発生しているか
- Firebase Crashlytics上の該当issueへの直リンクの生成(直で問題が発生している箇所を確認できるようにするため)
- ある閾値を超えたもののみをクエリ結果として出力する(定期実行時に毎回Slackに通知が飛ぶのを防止&古い配信値をデコードしてエラーになったりするエッジケースの排除するため)
ストリーミングエクスポートを有効にしてほぼリアルタイムのデータを取得するので、${Firebase Crashlyticsに設定しているプロジェクト名}+_REALTIME
のようにテーブルを指定して、テーブルの指定を行います。このテーブルから取得できるパラメータは、こちらのドキュメントに取得できるパラメータが記載されているため参考にしてみてください🙌
作成されたクエリは、1時間に1回定期的に実行されるように設定しておき、各配信値キーでデコードエラー数が閾値を超えた時に、次に作成するアラートを発動するようにします。
毎時間チェックするようにクエリ実行
アラート作成
アラートに関しては、Redash Alerts
を新規に作成し『送信したいSlackのチャンネル名』と『WebhookURLを設定して送信先の設定』し、作成したデコードエラー数抽出クエリのデータに値が存在したら、アラートを送信するような仕組みにします。
(順番前後しますが、この時点でSlack側のチャンネルにメッセージを送信するためのWebhookURLが必要になります。)
Redash Alertsの設定
上記のように設定することで、各キーのデコードエラー数の閾値である100件を超えるとアラートが警告仕様になってくれるようになります!
また、上記の右上のNotificationsにSlackのチャンネル名を追加することで、上記の設定通りデコードエラー検知時にメッセージを送信できます!
Slack
最終的にデコードエラーの対応が必要になった場合、Redashのアラート機能経由でWebhookURLを用いて、Slackチャンネルにメッセージを送信します。WebhookURLの取得等は省きますが、WebhookURL取得して、Redash Alertsで設定したAlertにチャンネルメッセージ送信チャンネルを指定して完了です!
調査対象が記載されたクエリが送信される
上記のようなkeyがわかるようなBotも導入
現在は、上記をチャンネル内で、クエリ結果をチャンネルに表示するなどを用いて、クエリをRedashにみに行くことなく、Slack上で調査対象のデータ配信サービスのkeyを表示するようにしてより円滑に調査を行えるようにしています!
運用してみて
こちらの仕組みを運用し出して、1ヶ月ほどになりますが以下のようなことを検知できています。
- デグレーションの検知をいち早く認識できた
- 元々実装されて利用されていたコードに問題があったことが検知できた
などが挙げられます!
当初想定していた、人為的なJSON設定ミスは発生していませんがデコードが発生していることが素早く検知できて、より安定したアプリ運営には寄与していると思います!
まとめ
データ配信サービスの設定ミスによるデコードエラーを即座に検知・対応できるようにすることで、機会損失を起こしたとしてもすぐに検知できるようにして、初動の対応を素早く行えるようになりました。
「Crashlytics上でエラーの情報をより素早く検索できるように検索性を向上させる」や「誤検知を完全に防ぎノイズをゼロにする」などまだまだ改善できるところはありますが、現状安定して運用できているので、ユーザー体験の向上や機会損失を極限まで減らせるようにしていきたいと思います!🙌
また、今回は、Redashを利用してSlackにメッセージを通知する方法でシステムを構築しましたが、Google Apps Scriptでも同じようなシステムを構築できそうなので、そちらも参考にしてみてください!
参考記事
Discussion