記事

CrashlyticsとBig Query連携|Slackで即時にクラッシュ追跡を実現

CrashlyticsのクラッシュデータをBig Query経由で自動転送し、Slackチャンネルでリアルタイムに共有。開発者のクラッシュ対応時間を大幅短縮し、効率的な問題解決をサポートします。

CrashlyticsとBig Query連携|Slackで即時にクラッシュ追跡を実現

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


Crashlytics + Big Query でよりリアルタイムで便利なクラッシュ追跡ツールを作る

Crashlytics と Big Query を連携してクラッシュログを自動的に Slack チャンネルに転送する

成果

Pinkoi iOSチームの実写写真

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)以外にも、BugsnagBugfenderなどの選択肢があります。各ツールは実際に比較していませんので、興味のある方はご自身で調べてみてください。もし他のツールを使う場合は、本記事で紹介する内容は適用されません。

Crashlytics

Crashlytics を選ぶメリットは以下の通りです:

  • 安定性はGoogleによって支えられています

  • 無料で、インストールも簡単かつ迅速

  • クラッシュ以外にも、エラーイベントをログできます(例:デコードエラー)

  • Firebase 一つで全てをカバー:他のサービスには Google Analytics、Realtime Database、Remote Config、Authentication、Cloud Messaging、Cloud Storage などがあります…

余談:正式なサービスを完全に Firebase で構築することはおすすめしません。後でトラフィックが増えると料金が高くなるため、いわゆる「育ててから一気に課金する」仕組みだからです。

Crashlytics の欠点も多い:

  • Crashlytics はクラッシュデータの API クエリを提供していません

  • Crashlytics は直近90日間のクラッシュ記録のみを保存します

  • Crashlytics の Integrations はサポートと柔軟性が非常に低いです

一番困るのは、Integrations のサポートと柔軟性が非常に低く、API がないため自分でスクリプトを書いてクラッシュデータを連携できないことです。そのため、時々手動で Crashlytics にログインしてクラッシュ記録を確認し、クラッシュ問題を追跡するしかありません。

Crashlytics が対応している統合:

  1. [Email 通知] — トレンドの安定性問題(増加しているクラッシュ問題)

  2. [Slack, Email 通知] — 新しい致命的な問題(クラッシュ問題)

  3. [Slack, Email 通知] — 新しい非致命的な問題(クラッシュしない問題)

  4. [Slack, Email 通知] — Velocity アラート(急激に増加しているクラッシュ問題)

  5. [Slack, Email 通知] — 回帰アラート(解決済みだが再発した問題)

  6. 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でデータを取得できるAPIを提供する予定はないと思いますし、実際ありません。なぜなら公式の唯一の推奨はデータをBig Queryにインポートして使うことであり、Big Queryは無料のストレージやクエリの上限を超えると料金が発生するからです。

保存:毎月最初の10GBは無料です。

クエリ:毎月最初の1TBは無料です。(クエリ容量とは、SELECT実行時に処理されたデータ量を指します)

詳細は Big Query の料金説明をご参照ください

Crashlytics から Big Query の設定詳細は 公式ドキュメント を参照してください。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日間の毎日のクラッシュ数を集計:

1
2
3
4
5
6
7
8
9
10
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のクラッシュを検索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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種:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 データが Big Query にインポートされているか確認してください(例えば、デフォルトの SQL サンプルは当日のクラッシュ記録を検索しますが、実際にはまだデータが同期されていないため、結果が取得できないことがあります)。データが確実にある場合は、次にフィルター条件が正しいか確認してください。

Top 10 Crashlytics Issue Big Query SQL

こちらは公式の第2番目のサンプルを参考に修正しました。私たちが望む結果は、Crashlyticsの最初のページで見るのと同じクラッシュ問題とその並び順のデータです。

直近7日間のクラッシュ問題トップ10:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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を組み込みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 に変更可能です。

返却されるオブジェクト構造は以下の通りです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[
  [
    "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への転送機能を追加:

上述のコードの下に新しい関数を追加してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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"; //あなたのインカミング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 クラッシュフリーユーザー率を取得する方法」を参照してください。

関連記事

ご質問やご意見がございましたら、こちらからご連絡ください

Post Mediumから変換、ZMediumToMarkdownによる。


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。