Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

BigQueryを使ってCrashlyticsのデータを分析する

技術本部 Mobile Applicationグループ所属の大塚です。

名刺アプリ「Eight」のAndroidアプリの開発と、営業DXサービス「Sansan」とEightの両プロダクトをまたぐプロダクト横断チームの一員として、モバイル領域の中長期的な技術的課題の解決や、PoCの開発を担当しています。

今回は、昨年9月にリリースしたEightのタッチ名刺交換機能の品質調査でBigQueryを利用する機会があったため、弊社の事例を参考に分析方法を共有します。

jp.corp-sansan.com
タッチ名刺交換とは、Eightのアプリを開いた状態で、同じくEightのアプリを開いた他のAndroid端末やiOS端末と自端末をタッチすることで、デジタル名刺が交換できる機能です。

その際に、Bluetooth Low Energy(以降BLE)を用いたタッチの検出や、デジタル名刺の交換に必要な通信のコネクションを張るタッチ検出&通信ライブラリを作成しています。なお、ライブラリの作成で得た知見はこちらの記事に詳しく書かれています。

buildersbox.corp-sansan.com

ハードウェアの機能を扱うライブラリのため、端末の種類や状態によってうまく動かない場合があります。リリース前にチェックできるスマートフォンの機種にも限りがあるため、ライブラリで発生したエラーは非致命的なエラーとして Firebase Crashlytics へ送信するようにしていました。

リリースから一定期間が経過した頃に品質調査を開始しましたが、タッチ名刺交換ライブラリで発生した例外は1つのクラスでラップして送信していたため、1つのクラッシュイベントにまとめられている状態でした。詳細な原因ごとの発生件数をFirebase Crashlytics だけで確認することが難しかったため、BigQueryにエクスポートして分析しました。

目次

Firebase Crashlytics のデータを BigQuery にエクスポートする

Firebaseのデータを BigQuery にエクスポートする設定は Firebase コンソールから確認できます。プロジェクトの設定 > 統合 > BigQuery の順に開いた後、Crashlyticsのセクションを確認してください。

ストリーミングを含めるにチェックを入れると、デフォルトで作成されるバッチテーブルに加えてリアルタイムテーブルがBigQuery上に作成されます。バッチテーブルは1日1回のエクスポートですが、リアルタイムテーブルはデータがリアルタイムでエクスポートされますので用途に応じて選択してください。

設定が完了していれば、連携しているGCPプロジェクトのBigQueryにfirebase_crashlyticsというデータセットが作成されます。

Crashlyticsからエクスポートされたデータのテーブル構造

エクスポートされたデータの構造は「スキーマ」タブ、もしくは公式ドキュメントで確認できます。デバイス情報やメモリ、ストレージ、OSなどCrashlyticsで確認できる情報は基本的にエクスポートされています。スキーマのデータタイプのRECORDRECORD REPEATEDは注意が必要です。

フィールドタイプ RECORD について

RECORDは構造体です。例えばデバイス情報は構造体としてデータが保存されていて、deviceというフィールド名でアクセスできます。

deviceを SELECT の要素として指定した場合はdevice内のすべての要素が出力されますが、. を使って構造体の内部のフィールドにアクセスできます。

SELECT 
  issue_title, device
FROM 
  `{YOUR_GCP_PROJECT_ID}.{YOUR_DATA_SET}.{YOUR_TABLE_NAME}`
WHERE 
  event_timestamp >= TIMESTAMP('2024-04-01') 
  AND event_timestamp <= TIMESTAMP('2024-04-07')

device内の要素が展開される

フィールドタイプ REPEATED RECORD について

REPEATED RECORDは配列型です。ログや例外、スタックトレースはフィールドタイプ REPEATED RECORD として定義されており、構造体の配列としてデータが保存されています。

BigQueryのプレビューを確認するとわかりますが、構造体配列のデータは1行の中にネストされる形で複数のデータが保存されます。例えばスタックトレースは1つのクラッシュイベントの中のネストされた例外の中に配列として保存されています。

配列のデータは次のようなSELECT文でそのまま参照できず、後ほど説明するUNNEST関数でデータを展開する必要があります。

-- exceptions.titleにアクセスできないのでエラー
-- Cannot access field title on a value with type ARRAY<STRUCT<type STRING, exception_message STRING, nested BOOL, ...>> at [2:32]
SELECT 
  issue_title, exceptions.title
FROM 
  `{YOUR_GCP_PROJECT_ID}.{YOUR_DATA_SET}.{YOUR_TABLE_NAME}`
WHERE 
  event_timestamp >= TIMESTAMP('2024-04-01') 
  AND event_timestamp <= TIMESTAMP('2024-04-07')

BigQueryを使ったエラーの分析

前述の通り、ライブラリからのエラーは1つの例外でラップして送信しています。例えば次のように例外を記録すると「SampleException1」と「SampleException2」のイベントはCrashlytics上で1つのイベントにまとめられます。なお、「SampleException1の例外です」というメッセージはCrashlyticsのサブタイトル領域に表示されます。

// 非致命的なエラーの登録
fun sample() {
    try {
        throwableFunction()
    } catch (e: Exception) {
        FirebaseCrashlytics.getInstance().recordException(ParentException(e))
    }
}

// 例外が発生するかもしれない関数
fun throwableFunction() {
    // ...
    throw SampleException1()
}

// ラップする例外
class ParentException(cause: Throwable) : Exception(cause)

// 例外A
class SampleException1(override val message: String = "SampleException1の例外です") : Exception()
// 例外B
class SampleException2(override val message: String = "SampleException2の例外です") : Exception()

まとめることで機能ごとにイベント数や影響を受けたユーザー数が確認できるため、機能単位で問題があるかどうか把握しやすいメリットはありました。しかし、Crashlytics上でどのような原因の分布になっているかの確認は難しい状況のため、BigQueryを利用して原因ごとの件数を出力してみます。

ある機能でネストされている、非致命的なエラーの原因の分布を確認するためのクエリは次の通りです。issue_idはCrashlyticsで確認したい非致命的エラーイベントを開き、URLのissue以下を確認するなどで取得できます。

SELECT 
  issue_subtitle, 
  COUNT(issue_subtitle) as event_count
FROM 
  `{YOUR_GCP_PROJECT_ID}.{YOUR_DATA_SET}.{YOUR_TABLE_NAME}`
WHERE 
  event_timestamp >= TIMESTAMP('2024-04-01') 
  AND event_timestamp <= TIMESTAMP('2024-04-07')
  AND issue_id = "{確認したい非致命的エラーイベントのISSUE_ID}"
GROUP BY issue_subtitle
ORDER BY event_count DESC

サブタイトルごとにイベント数が集計されるため、出力のイメージは次の通りです。

issue_subtitle event_count
subtitle A 88
subtitle B 30
subtitle C 9

UNNEST関数を使った例外の検索

機能ごとにラップして送信している例外はsubtitleの文字列を使って集計しましたが、次のようにネストされた例外を展開して集計できます。前述した通り、ネストされた例外やスタックトレースのデータは配列データとして存在しています。配列データを展開してアクセス可能にしてくれるのがUNNEST関数です。

UNNEST関数を使うとREPEATEDな要素がテーブルに展開されます。例えば例外はexceptionsに保存されていますが、UNNEST関数を使うことで画像のように展開されます。

展開前

展開後

先ほどはクラッシュイベントのsubtitleを使って集計しましたが、次のようにネストされた例外を展開して集計できます。
今回はexceptions.typeexceptions.subtitleを集計します。e.nested = trueという条件がありますが、これはネストされた例外で最後にスローされた例外を省くために指定しています。「最後に投げられた例外以外はすべて真」という定義になりますので、先ほどのParentExceptionSampleException1の例であれば、ParentExceptionはfalse, SampleException1はtrueとなります。

SELECT 
  e.type,
  e.subtitle,
  COUNT(*) as exception_count
FROM 
  `{YOUR_GCP_PROJECT_ID}.{YOUR_DATA_SET}.{YOUR_TABLE_NAME}` d,
  UNNEST(d.exceptions) AS e
WHERE 
  event_timestamp >= TIMESTAMP('2024-04-01') 
  AND event_timestamp <= TIMESTAMP('2024-04-07')
  AND issue_id = "{確認したい非致命的エラーイベントのISSUE_ID}"
  AND e.nested = true
GROUP BY e.type, e.subtitle
ORDER BY exception_count DESC

出力のイメージは次の通りです。

type subtitle exception_count
SampleException1 subtitle A 88
SampleException1 subtitle B 30
SampleException2 subtitle C 9

まとめ

ここまで読んでくださりありがとうございました。
以上がFirebase CrashlyticsのデータをBigQueryで分析した事例の紹介です。

UNNEST関数による配列型の展開により、ログや例外、スタックトレースに対してクエリを実行することが可能になりました。今回問題となったネストされている例外についてもexceptionsフィールドを展開することで、例外の件数を集計できました。

BigQueryに保存されたデータの構造と扱い方を理解することで、Crashlyticsのダッシュボードより細かい分析ができるため、必要に応じて活用してみてください。

最後に

Sansanの技術本部では、一緒にSansan / Eightのモバイルアプリを開発する仲間を募集中です。
選考評価なしで現場のエンジニアのリアルな声が聞けるカジュアル面談もあるので、ご興味ありましたらぜひ面談だけでもお越しいただけたら幸いです。

open.talentio.com


20240312182329

20240315190344

© Sansan, Inc.