Crashlytics + Big Query でよりリアルタイムで便利なクラッシュ追跡ツールを作る
Crashlytics と BigQuery を連携してクラッシュログを自動で Slack チャンネルに転送する

成果

Pinkoi iOSチーム 実写写真
まず成果画面を示します。毎週定期的に Crashlytics のクラッシュ記録をクエリし、クラッシュ回数上位10件の問題を抽出します。そのメッセージを Slack チャンネルに送信し、全ての iOS チームメンバーが現在の安定性を素早く把握できるようにします。
問題
App開発者にとって、Crash-Free Rateは最も重要な指標と言えます。これは、アプリのユーザーがクラッシュに遭遇しなかった割合を示しています。どんなアプリでもCrash-Free Rateを約99.9%に保ちたいと思うでしょう。しかし、現実には不可能です。プログラムには必ずバグがあり、さらにクラッシュの原因がAppleの基盤やサードパーティSDKにある場合もあります。また、DAU(デイリーアクティブユーザー)の規模によってもCrash-Free Rateに影響があり、DAUが多いほど偶発的なクラッシュ問題に遭遇しやすくなります。
既に 100% クラッシュしないアプリは存在しないため、クラッシュの追跡と対応は非常に重要です。最も一般的な Google Firebase Crashlytics(旧 Fabric)以外にも、Bugsnag、Bugfender などの選択肢があります。これらのツールは実際に比較していないので、興味がある方はご自身で調べてみてください。もし他のツールを使う場合は、本記事で紹介する内容は該当しません。
Crashlytics
Crashlytics を選ぶメリットは以下の通りです:
-
安定性が高く、Google のサポート付き
-
無料で、インストールも簡単かつ迅速
-
クラッシュ以外にも、エラーイベントのログ(例:デコードエラー)が可能
-
Firebase 一つで全てをカバー:他のサービスには Google Analytics、Realtime Database、Remote Config、Authentication、Cloud Messaging、Cloud Storage などがあります…
余談ですが、本格的なサービスを完全に Firebase で構築することはおすすめしません。後でトラフィックが増加した際の料金が非常に高くなるためです…いわゆる囲い込みビジネスモデルです。
■■■■■■■■■■■■■■
𝖟𝖔𝖓𝖇𝖑𝖊 🍺 ゾンビル KDCEHQ 4H111 @ Twitter は以下のように述べています:
昔、あるフードデリバリープラットフォームが Firebase を使ってバックエンド全体を構築していましたが、そのプラットフォームは業者への支払いができなくなったと聞きました。
2019年10月6日 08:54:06にツイートされました。
■■■■■■■■■■■■■■
Crashlytics の欠点も多い:
-
Crashlytics はクラッシュデータのAPIクエリを提供していません
-
Crashlytics は直近90日間のクラッシュ記録のみを保存します
-
Crashlytics の統合機能はサポートと柔軟性が非常に低いです
一番困るのは、Integrations のサポートと柔軟性が非常に低く、さらに API がないため、自分でスクリプトを書いてクラッシュデータを連携できないことです。手動で時々 Crashlytics を確認してクラッシュ記録を追跡するしかありません。
Crashlytics がサポートするインテグレーション:
-
[Email 通知] — トレンドの安定性問題(増加中のクラッシュ問題)
-
[Slack, Email 通知] — 新しい致命的な問題(クラッシュ問題)
-
[Slack, Email 通知] — 新しい非致命的な問題(非クラッシュ問題)
-
[Slack, Email 通知] — 速度アラート(急激に増加しているクラッシュ問題)
-
[Slack, Email 通知] — 回帰アラート(解決済みだが再発した問題)
-
Crashlytics から Jira への課題連携
以上のIntegrationsの内容やルールはカスタマイズできません。
最初は直接「2.New Fatal Issue to Slack or Email」を使用し、Emailの場合はGoogle Apps Scriptで後続処理スクリプトをトリガーしていました。しかし、この通知はチャンネルを大量に通知してしまいます。大小問わず、ユーザーの端末やiOS自体の断続的な問題で発生したクラッシュもすべて通知されるためです。DAUが増えるにつれて毎日通知が大量に届き、その中で本当に価値があり、多くの人が遭遇していて、かつ私たちのプログラムのバグに関係する通知は約10%に過ぎません。
Crashlyticsの自動追跡が難しい問題はまったく解決されず、結局この問題が本当に重要かどうかを確認するのに多くの時間を費やすことになります。
Crashlytics + Big Query

ぐるぐる探してもこの方法しか見つからず、公式もこの方法だけを提供しています。これが無料の甘い罠だと思います。CrashlyticsもAnalytics Eventも、APIでデータを取得できる機能はなく、今後も提供する予定はないと思われます。なぜなら公式の唯一の推奨はデータをBigQueryにエクスポートして利用することであり、BigQueryは無料の保存・クエリ容量を超えると料金が発生するからです。
保存:毎月最初の10GBは無料です。
クエリ:毎月最初の1TBは無料です。(クエリ容量とは、SELECT時に処理されたデータ量を指します)
詳細については Big Query の料金説明をご参照ください
Crashlytics から BigQuery への設定詳細は 公式ドキュメント を参照してください。GCP サービスの有効化やクレジットカードの紐付けなどが必要です。
Big QueryでCrashlyticsログのクエリを開始する
Crashlytics Log to Big Query のインポート周期を設定し、初回のインポートでデータが揃ったら、データのクエリを開始できます。

まず Firebase プロジェクト -> Crashlytics -> リスト右上の「•••」-> 「BigQuery dataset に移動」をクリックしてください。

GCP の Big Query にアクセス後、左側の「Explorer」から「firebase_crashlytics」を選択し、テーブル名を選んで「Detail」をクリックすると、右側に最新の更新日時、使用容量、保存期間などのテーブル情報が表示されます。
すでにインポートされたデータがクエリ可能か確認する。

上部のタブで「SCHEMA」に切り替えて、テーブルのカラム情報を確認するか、公式ドキュメント を参照してください。

右上の「Query」をクリックすると、補助付きSQLビルダーの画面が開きます(SQLに不慣れな場合はこちらの使用を推奨します):

または「COMPOSE NEW QUERY」をクリックして、空白のクエリエディタを開きます:

どの方法でも同じテキストエディタを使用します;SQLを入力した後、右上で自動的にSQL構文のチェックと予想されるクエリ使用量(This query will process XXX when run.)を確認できます:

クエリを実行するには、左上の「RUN」をクリックしてください。結果は下の Query results セクションに表示されます。
⚠️ 「RUN」を押してクエリを実行するとクエリ使用量が累積され、課金されます。無闇にクエリを実行しないようご注意ください。
SQLに不慣れな場合は、基本的な使い方を学んでから、Crashlyticsの公式サンプルを参考にカスタマイズしてください:
1.直近30日間の毎日のクラッシュ数を集計:
SELECT
COUNT(DISTINCT event_id) AS number_of_crashes,
FORMAT_TIMESTAMP("%F", event_timestamp) AS date_of_crashes
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
GROUP BY
date_of_crashes
ORDER BY
date_of_crashes DESC
LIMIT 30;
2. 過去7日間に最も多く発生したTOP10クラッシュのクエリ:
SELECT
DISTINCT issue_id,
COUNT(DISTINCT event_id) AS number_of_crashes,
COUNT(DISTINCT installation_uuid) AS number_of_impacted_user,
blame_frame.file,
blame_frame.line
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(),INTERVAL 168 HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
GROUP BY
issue_id,
blame_frame.file,
blame_frame.line
ORDER BY
number_of_crashes DESC
LIMIT 10;
しかし、公式サンプルのこのクエリでは、Crashlyticsで見る並び順と異なる結果が出ます。おそらく blame_frame.file(nullable)や blame_frame.line(nullable)でグループ化しているためです。
3.直近7日で最もクラッシュが多い10種類のデバイスをクエリする:
SELECT
device.model,
COUNT(DISTINCT event_id) AS number_of_crashes
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
event_timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 168 HOUR)
AND event_timestamp < CURRENT_TIMESTAMP()
GROUP BY
device.model
ORDER BY
number_of_crashes DESC
LIMIT 10;
さらに詳しい例は公式ドキュメントをご参照ください。
実行した SQL にデータがない場合は、まず指定した条件の Crashlytics データが BigQuery にインポートされているか確認してください(例えば、デフォルトの SQL サンプルは当日のクラッシュ記録を検索しますが、実際にはまだデータが同期されていないため、結果が取得できないことがあります)。データが確実にある場合は、次にフィルタ条件が正しいか確認してください。
Top 10 Crashlytics Issue Big Query SQL
こちらは公式の2.のサンプルを参考に修正しました。私たちが望む結果は、Crashlyticsの最初のページで見るのと同じクラッシュ問題とその並び順のデータです。
過去7日間のクラッシュ問題トップ10:
SELECT
DISTINCT issue_id,
issue_title,
issue_subtitle,
COUNT(DISTINCT event_id) AS number_of_crashes,
COUNT(DISTINCT installation_uuid) AS number_of_impacted_user
FROM
`你的ProjectID.firebase_crashlytics.你的TableName`
WHERE
is_fatal = true
AND event_timestamp >= TIMESTAMP_SUB(
CURRENT_TIMESTAMP(),
INTERVAL 7 DAY
)
GROUP BY
issue_id,
issue_title,
issue_subtitle
ORDER BY
number_of_crashes DESC
LIMIT
10;

Crashlytics のトップ10クラッシュ問題の結果と一致 ✅。
Google Apps Script を使って定期的にクエリを実行&Slackへ転送する
Google Apps Script ホームページ にアクセス -> Big Query と同じアカウントでログイン -> 左上の「新しいプロジェクト」をクリックし、新しいプロジェクトを開いたら左上でプロジェクト名を変更できます。
まずは Big Query と連携してクエリデータを取得しましょう:
参考 公式ドキュメント の例に従い、上記のクエリSQLを適用します。
function queryiOSTop10Crashes() {
var request = {
query: 'SELECT DISTINCT issue_id, issue_title, issue_subtitle, COUNT(DISTINCT event_id) AS number_of_crashes, COUNT(DISTINCT installation_uuid) AS number_of_impacted_user FROM `firebase_crashlytics.你的TableName` WHERE is_fatal = true AND event_timestamp >= TIMESTAMP_SUB( CURRENT_TIMESTAMP(), INTERVAL 7 DAY ) GROUP BY issue_id, issue_title, issue_subtitle ORDER BY number_of_crashes DESC LIMIT 10;',
useLegacySql: false
};
var queryResults = BigQuery.Jobs.query(request, '你的ProjectID');
var jobId = queryResults.jobReference.jobId;
// クエリジョブの状態を確認する。
var sleepTimeMs = 500;
while (!queryResults.jobComplete) {
Utilities.sleep(sleepTimeMs);
sleepTimeMs *= 2;
queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
}
// 結果のすべての行を取得する。
var rows = queryResults.rows;
while (queryResults.pageToken) {
queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
pageToken: queryResults.pageToken
});
Logger.log(queryResults.rows);
rows = rows.concat(queryResults.rows);
}
var data = new Array(rows.length);
for (var i = 0; i < rows.length; i++) {
var cols = rows[i].f;
data[i] = new Array(cols.length);
for (var j = 0; j < cols.length; j++) {
data[i][j] = cols[j].v;
}
}
return data
}
query: 件数は任意に作成したクエリSQLに変更可能です。
返却されるオブジェクトの構造は以下の通りです:
[
[
"67583e77da3b9b9d3bd8feffeb13c8d0",
"<compiler-generated> line 2147483647",
"specialized @nonobjc NSAttributedString.init(data:options:documentAttributes:)",
"417",
"355"
],
[
"a590d76bc71fd2f88132845af5455c12",
"libnetwork.dylib",
"nw_endpoint_flow_copy_path",
"259",
"207"
],
[
"d7c3b750c3e5587c91119c72f9f6514d",
"libnetwork.dylib",
"nw_endpoint_flow_copy_path",
"138",
"118"
],
[
"5bab14b8f8b88c296354cd2e",
"CoreFoundation",
"-[NSCache init]",
"131",
"117"
],
[
"c6ce52f4771294f9abaefe5c596b3433",
"XXX.m line 975",
"-[XXXX scrollToMessageBottom]",
"85",
"57"
],
[
"712765cb58d97d253ec9cc3f4b579fe1",
"<compiler-generated> line 2147483647",
"XXXXX.heightForRow(at:tableViewWidth:)",
"67",
"66"
],
[
"3ccd93daaefe80f024cc8a7d0dc20f76",
"<compiler-generated> line 2147483647",
"XXXX.tableView(_:cellForRowAt:)",
"59",
"59"
],
[
"f31a6d464301980a41367b8d14f880a3",
"XXXX.m line 46",
"-[XXXX XXX:XXXX:]",
"50",
"41"
],
[
"c149e1dfccecff848d551b501caf41cc",
"XXXX.m line 554",
"-[XXXX tableView:didSelectRowAtIndexPath:]",
"48",
"47"
],
[
"609e79f399b1e6727222a8dc75474788",
"Pinkoi",
"specialized JSONDecoder.decode<A>(_:from:)",
"47",
"38"
]
]
二次元配列であることがわかります。
Slackへの転送機能を追加:
上述のコードの下に新しい関数を追加してください。
function sendTop10CrashToSlack() {
var iOSTop10Crashes = queryiOSTop10Crashes();
var top10Tasks = new Array();
for (var i = 0; i < iOSTop10Crashes.length ; i++) {
var issue_id = iOSTop10Crashes[i][0];
var issue_title = iOSTop10Crashes[i][1];
var issue_subtitle = iOSTop10Crashes[i][2];
var number_of_crashes = iOSTop10Crashes[i][3];
var number_of_impacted_user = iOSTop10Crashes[i][4];
var strip_title = issue_title.replace(/[\<\\|\>]/g, '');
var strip_subtitle = issue_subtitle.replace(/[\<\\|\>]/g, '');
top10Tasks.push("<https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的專案ID/issues/"+issue_id+"\\|"+(i+1)+". Crash: "+number_of_crashes+" 回 ("+number_of_impacted_user+"人) - "+strip_title+" "+strip_subtitle+">");
}
var messages = top10Tasks.join("\n");
var payload = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":bug::bug::bug: iOS 過去7日間のクラッシュランキング :bug::bug::bug:",
"emoji": true
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": messages
}
},
{
"type": "divider"
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Crashlyticsで過去7日間の記録を見る",
"emoji": true
},
"url": "https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的專案ID/issues?time=last-seven-days&state=open&type=crash&tag=all"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Crashlyticsで過去30日間の記録を見る",
"emoji": true
},
"url": "https://console.firebase.google.com/u/1/project/你的ProjectID/crashlytics/app/你的專案ID/issues?time=last-thirty-days&state=open&type=crash&tag=all"
}
]
},
{
"type": "context",
"elements": [
{
"type": "plain_text",
"text": "クラッシュ回数および発生バージョンは過去7日間のデータのみを集計しており、すべてのデータではありません。",
"emoji": true
}
]
}
]
};
var slackWebHookURL = "https://hooks.slack.com/services/XXXXX"; //あなたのin-coming webhook URLに置き換えてください
UrlFetchApp.fetch(slackWebHookURL,{
method : 'post',
contentType : 'application/json',
payload : JSON.stringify(payload)
})
}
もし Incoming WebHook URL の取得方法がわからない場合は、こちらの記事 の「Incoming WebHooks App URL の取得」セクションを参照してください。
テスト&スケジュール設定

この時点で、あなたの Google Apps Script プロジェクトには上記の2つの関数があるはずです。
次に、上部の「sendTop10CrashToSlack」関数を選択し、Debug または Run をクリックして一度テスト実行してください。初回実行時は認証が必要なため、少なくとも一度実行してから次のステップに進んでください。

テストを一度問題なく実行できたら、スケジュール設定による自動実行を開始できます:

左側でアラームアイコンを選択し、次に右下の「+ Add Trigger」を選択します。

最初の「Choose which function to run」(実行する関数の入口)は sendTop10CrashToSlack に変更してください。時間の周期はお好みに合わせて設定可能です。
⚠️⚠️⚠️ クエリは実行するたびにデータ処理量が累積し料金が発生するため、設定を安易に変更しないでください。さもないとスケジュール実行で高額な請求が発生する可能性があります。
完了

サンプル成果画像
今後は、Slack 上で現在のアプリのクラッシュ問題を素早く追跡でき、さらにそのまま議論を行うことも可能です。
アプリのクラッシュフリー率?
もし追跡したいのが App のクラッシュフリーユーザー率であれば、次の記事「Crashlytics + Google Analytics 自動で App Crash-Free Users Rate を照会する」を参照してください。
関連記事
Post Mediumから変換 by ZMediumToMarkdown.



コメント