タイトルで煽りました。すみません。でも4.6は本当です。
実際はレビュー返信や手厚いサポート対応などの成果がかなり大きいと思っています。でもエンジニアも負けてられません。
この記事ではチームでのCS(不具合)対応フローとともに、Crahlyticsを使ったエンジニアによる対応を書きます。
(後で紹介しますが、AndroidでCrashlyticsを便利に使うライブラリも書きました)
チームが作ってるアプリ
家族向け写真・動画共有アプリ「みてね」のAndroid, iOS, API, Webを開発しています。エンジニアは基本フルスタックで、自分は中でもAndroidをメインにやってます。
※「みてね」がGoogle Playの2015年ベストアプリに選ばれました!!!!
※チームの開発風景についてはこちらもどうぞ
http://ainame.hateblo.jp/entry/mitene-realm-scrum-ios-android-rails
http://ainame.hateblo.jp/entry/2015/12/01/101216
みてねチームでのCS
アプリ開発においてクラッシュや不具合はレビューに直結し、しかもアプリのレビューはアプリのインストール前後の評価にそのまま結びついたりします。みてねチームでは、ユーザ維持だけではなくユーザ獲得のためにも、レビューの星の数は重要だと考えています。
「問い合わせ」のメアドやレビューの自由記述欄に届く不具合報告や機能に関する質問について気になるものがあれば報告を受け、必要に応じてエンジニアが調査したり、機能改善のためのバックログを積んだりしています。
もちろんクラッシュや不具合の修正は重要ですが、新機能開発・改修を進めている現場(特に審査が必要でリリース回数に制約があるiOS)では優先度を決めて対応する必要があります。この時の判断材料となるのが問い合わせの有無、影響ユーザ数、総クラッシュ回数などです。
Androidは特に機種に依存してクラッシュするケースもあり、実機がない状態での調査・修正は情報が得られず骨を折る作業となります。
Crashlytics
これらの対応に必要な情報を集積するためにCrashlyticsを使用しています。
Twitter社が無償提供しているクラッシュレポーティングツールです。
元々は独立したサービスでしたが、2014年にTwitterが出しているFabricの一機能に統合されました。
起きたクラッシュの一覧と、その詳細(いつ、どのバージョンで、どんな端末で、どんなスタックトレースで、など)を一覧・検索することができます。
(NOTE: 以降のスクショに本番でのクラッシュ情報は含まれていません)
クラッシュに対応するフロー
選別する
ぶっちゃけクラッシュの種類は、例えコードに問題がなかったとしても星の数ほど(大げさ)出てくるので、まずはそもそもどのクラッシュに対応するかどうか考えます。
問い合わせメールやレビューに届いたクラッシュについて、件数が多かったりアプリ使用において致命的なものであれば一旦ざっくりとした調査を行っています。クラッシュ一覧画面にはクラッシュが起きた場所が書かれているので、クラッシュ回数が多くて問い合わせの情報が具体的であればすぐに特定できます。わからない時は、ユーザに聞きだしてほしい内容をエンジニアが指定したりします。
また、エンジニアが不定期にクラッシュ一覧を眺めたりして、クラッシュが多かったり(トレース的に)なんか凶悪そうであれば、これは対応しようぜ!と言い出します。Crashlyticsの場合はクラッシュの頻度でざっくりと5段階評価し「Level」として表示してくれるので、ざっくりとチェックしやすいです。(Level 2〜3以上は全部駆逐を目標にしていたこともあります)
さらに、FabricのAnswersを統合していると、Crashが起きていないユーザの割合を見ることができます。定常的には見ていないのですが、この値が悪くなってきたらすぐに対応する、といったこともできそうです。
みてねのiOSは99.8%です・・!(Androidの方はFragment関係のクラッシュなどに引っ張られて98%台です・・w)
優先度を決める
真っ先に見るのがクラッシュ数、影響ユーザ数です。どう見ても多い場合はすぐに対応をはじめる場合もあり、逆にとてもレアだとトリアージされます。
ただし、リリースしたばかりバージョンでだけいっぱい出ているケースでは、今後ユーザがアップデートかけることで影響ユーザがどんどん増える可能性もあります。Crashlyticsではバージョンごとのクラッシュ数もチェックできます。(下記はクラッシュ詳細の最下部に乗ってる表ですが、最上部のグラフ内にもバージョンごとの情報は表示されています。)
原因を特定する
ほとんどの場合はスタックトレースを見れば、あああぬるぽ!!!とかなるのですぐに直せます。
ただしそうではないケースがあります。
だいたいはStack Overflowで探すとOSのバグだったり使い方が悪いみたいなケースに当たります(ソースは俺)が、機種依存、OSバージョン依存、そういうケースも出てくるのです。例えば個人的にAndroidで骨が折れたやつはこんな感じです・・。
- Universal Image Loaderを使うとAndroid 4.4のみでファイルディスクリプタがleak https://github.com/nostra13/Android-Universal-Image-Loader/issues/1020
- GridView系ライブラリのFreeFlowを使うと、特定のディズニーモバイル端末だけスクロールの終端で出るエフェクトの幅が0px(Android Frameworkのコードがいじられていた・・)で、divide by zero https://github.com/Comcast/FreeFlow/pull/82
(ちなみに両ライブラリとももう使ってないです・・。
その原因を突き止めるために本当に役立った情報が、下記の端末情報とOSバージョン情報です。
特定のOS、端末が90%とかだと、このクラッシュ、におうな・・となるので、Androidのissue trackerやコードを見に行ったりします。Androidのコード検索はOpenGrokを、バージョン比較(Blame)はGithubのandroidオーガニゼーションを見ると便利です。
iOSの場合はオープンソースじゃないのでStack OverflowやOpen Radar、Apple Developer Forumsを見るしかないのが厳しいところです(Stack Overflowが一番役に立つ・・)。
直す
PRを作り、動作確認やQAをして、リリースします。
機種依存の場合は社内チャットやメールで誰かオラに端末を分けてくれ〜〜〜してダメだった場合は祈りながらリリースします・・。
こればっかりはどうしようもならないと思ってましたが、AWS Device Farmを使ってクラウド上で動作確認すれば改善できるかもしれません。
不具合の原因を突き止める工夫
基本フローはこんな感じですが、エンジニアとして工夫していることがいくつかあるので紹介します。
問い合わせが来た時のクラッシュを特定
アプリ内の問い合わせメールを送るボタンを押すと、本文にユーザID、OS(Android/iOS)とバージョン、端末状態、一部内部状態が書き込まれた状態でメールアプリが起動するようにしています。(ちなみにAndroid or iOSが一番使っている情報ではあります)
アプリ側でCrahlyticsのSDKにユーザIDをセットしておくと、ユーザIDでクラッシュを検索することができるようになり、特定が非常に簡単になります。
Android:
Crashlytics.getInstance().core.setUserIdentifier(userId);
iOS Swift:
Crashlytics.sharedInstance().setUserIdentifier(account.id)
iOS Objective-C:
[[Crashlytics sharedInstance] setUserIdentifier:account.id];
右上の検索ボックスにユーザのIDを入力するだけの簡単検索です!
Viewing latest crash (More details...)
から開ける個々のクラッシュの詳細にも表示されます。
他にメールアドレスやユーザ名をセットすることもできますが、ユーザのプライバシー情報を送信することになり、プライバシーポリシー等の更新する必要があるので、ユーザIDをオススメします。
アプリのログをクラッシュレポートに残す
スタックトレースだけでは原因が把握できない場合を考えて、ログを残しています。
専用のメソッドを叩く必要があるのが面倒ですが、後ほど紹介するロギングライブラリを使って送信するようにすると便利になります。
Android:
Crashlytics.log("Log message.");
iOS Swift:
CLSLogv("Log awesomeness %d %d %@", getVaList([1, 2, "three"]))
iOS Objective-C:
CLS_LOG(@"Higgs-Boson detected! Bailing out... %@", attributesDict);
個々のクラッシュの詳細画面にあるKeys/Logsに、クラッシュ前に出ていたログを表示することができるようになります。
実際にこれのお世話になったケースは少ないですが、状況が把握できない場合はログだけでも見れると安心です。
例えば通信エラーを繰り返しているから通信状況が悪いのかな・・?とかそう言った類推もできるようになります。
クラッシュはさせないけどException/NSErrorのログは残す(Android 追記:iOSはベータ)
基本的にクラッシュはユーザ体験を大きく損ねる一方で、明らかなプログラミングミスや、発生することがかなりマズいシチュエーションなどに関しては、それを明らかにして修正するためにクラッシュさせたいという場合もあります(assertなど)。
Crashlytics のAndroid版 には、Non-fatal Exceptionsとして、クラッシュはさせずにExceptionの記録だけを行う機能が備わっています。
try {
doSomeMemoryBoundTask();
} catch (OutOfMemoryError e) {
Crashlytics.logException(e);
}
なおiOS版は作るかもしれないとのことです。
追記: Fabricの中の人から、iOS版の対応はベータリリースされているとのご連絡をいただきました!
NSErrorを記録する機能が利用できるようになるようです。
ベータ版の登録はこちら: https://get.fabric.io/labs
https://crashlytics.com/blog/from-crashlytics-labs-unity-and-tvos-support-logged-errors-for-ios
ロギングを便利にするライブラリ
ロギングライブラリを導入すると、上記のCrashlyticsのSDKを使ったロギングが簡単にできます(ライブラリに合わせたカスタムのロガーの作成が必要です)。
例えば、Log.w()
やNSLog()
の出力とCrashlyticsへのログ記録を同時に行ったり、開発中はクラッシュさせるけど本番ではExceptionのログをCrashlyticsに送る、などといったことを単一のメソッドを叩くだけでできるようになります。
※みてねではCrashlyticsへの送信は今のところAndroidだけやってます
- Android: Timber
Timber.e(e, "Exception occurred!");
- iOS: CocoaLumberjack
DDLogError("Error!");
Crashlyticsへの統合をライブラリ化しました(Android)
Androidで上で紹介したロギングをTimberで透過的に行うためのカスタムロガーのライブラリを公開しています。
https://github.com/ypresto/TimberTreeUtils
下記のロガーを(開発・本番などに合わせて)Timber.plant(new HogeTree())
することでロギングの挙動を設定することができます。
- CrashlyticsLogExceptionTree: エラーやログを
Crashlytics.logException()
でNon-fatalとして記録 - CrashlyticsLogTree:
Crashlytics.log()
でログを記録 - ThrowErrorTree:
Timber.e()
されたらクラッシュさせる(開発時にバグを見つける用)
Timber標準のDebugTreeと組み合わせるとCrashlyticsと同時に通常のAndroid Studioのログにも同時に流すことができます。
みてねでの設定例です
private void setupLoggingPlugins() {
if (BuildConfig.DEBUG) {
Timber.plant(new Timber.DebugTree());
Timber.plant(new ThrowErrorTree());
} else {
Fabric.with(this, new Crashlytics());
Timber.plant(new CrashlyticsLogTree(Log.INFO));
Timber.plant(new CrashlyticsLogExceptionTree());
}
}
ただひたすら、高品質なソフトウェアを追求する
エンジニアができる最も効果的なことは、なるべくクラッシュや不具合のでないよう、設計やコーディングに注意を払い、必要な箇所にテストを書き、疑わしい箇所の動作確認を行っていき、問題を見つけては直していく、といった地道なことだと思います。
そのためにはCSでのコミュニケーションや情報収集が欠かせないです。
この記事がそのようなアプリの改善に少しでもお役に立てるなら幸いです。
(・・・ぐぬぬ、エモい話をしようと思ってネタが切れましたw)