[BigQuery]アプリバージョンごとのユーザー比率を取得してSlackへ通知する

Android や iOS でアプリでは何度もバージョンアップしてリリースすることが多いです。

ですのでユーザーが最新のバージョンのアプリを使っているかなど、アプリバージョンごとのユーザー比率が気になることがあるかと思います。

最近、アプリバージョンごとのユーザー比率を BigQuery から取得して Slack に通知する仕組みを実装したので、その方法についてご紹介いたします。

実際に実行してみると、以下のような感じで Slack へ通知が行われるようになります。

```
アプリバージョンごとのユーザー比率 (2021年03月15日)

## Android

- 1.2.0   : 57.1% (4)
- 1.1.2   : 14.3% (1)
- 1.1.0   : 14.3% (1)
- 1.0.0   : 14.3% (1)

## iOS

- 1.2.0   : 54.2% (13)
- 1.1.1   : 37.5% (9)
- 1.1.0   : 4.2%  (1)
- 1.0.4   : 4.2%  (1)
```


この仕組みを実装するにあたって、以下の対応が必要になります。

1. BigQuery からアプリバージョンごとのユーザー比率を取得するためのスクリプトを作成
2. GitHub Actions のスケジュール機能を使って一日一回、上記で作成したスクリプトを実行
3. 実行完了後、GitHub Actions から Slack へ通知

ちなみにアプリには [Firebase Analytics](https://firebase.google.com/docs/analytics) を導入していることが前提になります。

# 1. スクリプト作成

## 認証のための準備

BigQuery からデータを取得するにあたって認証が必要になります。

詳細は [こちら](https://cloud.google.com/docs/authentication/getting-started?hl=ja) を参照してください。

まずはローカル環境で確認するために、私は以下の対応を行いました。

- 認証情報が記載されている JSON ファイルをダウンロードして用意
- 環境変数 `GOOGLE_APPLICATION_CREDENTIALS` でダウンロードした JSON ファイルのパスを設定

余談ですが環境変数の設定は [direnv](https://direnv.net/) を使うと、特定のディレクトリ配下にのみ環境変数を設定できるので便利です。

このファイルは後ほど GitHub Actions の設定でも必要になります。

## スクリプト作成

スクリプトは Ruby で書きました。

まずは以下の内容で Gemfile を作成し、`bundle install` を実行して `Gemfile.lock` ファイルを生成します。

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

gem "google-cloud-bigquery"
```

続いて、スクリプトを以下のように作成します。

スクリプトは [こちら](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-bigquery/samples/snippets/query.rb) を参考に実装しています。

```ruby:get_ratio_of_users.rb
require "google/cloud/bigquery"

def create_user_ratio_string(rows, os)
  users = rows.filter { |row| row[:os] == os && !row[:ver].nil? }
  # 「os」で指定されたユーザー数の合計を算出
  user_count = users.reduce(0) { |sum, row| sum + row[:cnt] }
  # 上位10件まで表示
  users.take(10).reduce("## #{os}\n\n") do |str, row|
    percent = sprintf("%.1f", row[:cnt] * 100.0 / user_count)
    percent_str = "#{percent}%".ljust(6)
    "#{str}- #{row[:ver].ljust(8)} : #{percent_str}(#{row[:cnt]})\n"
  end
end

def query
  bigquery = Google::Cloud::Bigquery.new
  yesterday = Date.today - 1
  sql = "SELECT device.operating_system AS os, app_info.version AS ver, COUNT(DISTINCT user_pseudo_id) AS cnt " \
        "FROM `<project_name>.analytics_<property_id>.events_#{yesterday.strftime("%Y%m%d")}` " \
        "GROUP BY os, ver " \
        "ORDER BY os, cnt DESC"

  # Location must match that of the dataset(s) referenced in the query.
  rows = bigquery.query sql do |config|
    config.location = "US"
  end

  str = "```\n#{yesterday.strftime("アプリバージョンごとのユーザー比率 (%Y年%m月%d日)")}\n\n"
  str += create_user_ratio_string(rows, "Android")
  str += "\n"
  str += create_user_ratio_string(rows, "iOS")
  str += "```"

  puts str
end

query
```

`query` メソッドの先頭の `sql =`  部分が BigQuery からデータを取得するための SQL になります。


```sql
SELECT device.operating_system AS os, app_info.version AS ver, COUNT(DISTINCT user_pseudo_id) AS cnt
FROM `<project_name>.analytics_<property_id>.events_#{yesterday.strftime("%Y%m%d")}`
GROUP BY os, ver
ORDER BY os, cnt DESC
```

### テーブルについて

Firebase のプロジェクトごとに `analytics_<property_id>` という名前のデータセットが BigQuery のプロジェクトに追加されます。

ですので `FROM` の部分の `<project_name>.analytics_<property_id>` にはそれぞれのプロジェクトで追加されているデータセットの名前を指定するようにしてください。

また、データセット内には `events_YYYYMMDD` という形式の名前でテーブルが毎日作成されます。

ですので `events_#{yesterday.strftime("%Y%m%d")}` の部分は前日のテーブルから情報を取得していることになります。

参考: https://support.google.com/firebase/answer/7029846?hl=ja

### テーブルから取得できる情報について

テーブルから取得できる情報は [こちら](https://support.google.com/firebase/answer/7029846?hl=ja) に記載があります。

上記の SQL では以下の情報を参照しています。

- `device.operating_system` : OS
- `app_info.version` : アプリバージョン
- `user_pseudo_id` : ユーザーの仮ID

これらの情報から、SQL では OS、アプリバージョン、アプリバージョンごとのユーザー数を取得しています。

### データの整形

上記で取得した情報 `create_user_ratio_string` メソッドに渡すことによって、OS ごとにユーザー数を合計してユーザー比率を計算して文字列に整形しています。

以上でスクリプトの作成は完了です。

# 2. GitHub Actions の設定

次に GitHub Actions の設定を行います。

GitHub 管理下のプロジェクトで先ほど作成した `Gemfile` と `get_ratio_of_users.rb` と自動生成された `Gemfile.lock` を `.ci/bigquery` ディレクトリ内に置きます。

私はファイルをまとめるためにここに置きましたが、好きなところに置いてください。ただしその場合、次の yml ファイルに記載してあるファイルパスを変更する必要があります。

そして以下の yml ファイルを `.github/workflows` 内に作成します。

```yml:get_ratio_of_users.yml
# アプリバージョンごとのユーザー比率を取得し、Slackへ通知するためのワークフロー
name: Get ratio of users by app version

on:
  workflow_dispatch:
  schedule:
    - cron:  '30 0 * * *'

jobs:
  get-ratio-of-users:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.7'

      - name: Install bundler
        run: |
          GEM_BUNDLER_VERSION=$(grep -A1 -E -i -w '(BUNDLED WITH){1,1}' .ci/bigquery/Gemfile.lock | grep -E -i -w "[0-9\.]{1,}" | xargs)
          gem install bundler -v=$GEM_BUNDLER_VERSION --force

      # 推奨されている方法ではないので注意
      # https://docs.github.com/ja/actions/reference/encrypted-secrets#about-encrypted-secrets
      - name: Create service account file
        run: echo ${{ secrets.BIGQUERY_SERVICE_ACCOUNT_JSON_BASE64 }} | base64 -d > service-account.json

      - name: Run ruby script
        env:
          GOOGLE_APPLICATION_CREDENTIALS: './service-account.json'
        run: |
          bundle install --gemfile=.ci/bigquery/Gemfile
          echo 'SLACK_MESSAGE<<EOF' >> $GITHUB_ENV
          ruby .ci/bigquery/get_ratio_of_users.rb >> $GITHUB_ENV
          echo 'EOF' >> $GITHUB_ENV

      - name: Notify to slack
        if: ${{ success() }}
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXX
          SLACK_COLOR: '#58A359'
          SLACK_CHANNEL: XXXXXX
          SLACK_ICON: XXXXXXXXXXXXXXXXXXXXXX
          SLACK_TITLE: アプリバージョンごとのユーザー比率
          SLACK_USERNAME: XXXXXXX
```

## 実行タイミングについて

上記の `schedule:` の設定により、毎日午前9時30分に実行されます。

参考: https://docs.github.com/ja/actions/reference/events-that-trigger-workflows#scheduled-events

設定する時間は UTC なので、日本時間で午前9時30分に設定する場合はその9時間前の午前0時30分にする必要があります。

また `workflow_dispatch:` の設定により、手動でも実行できるようにしています。

## GitHub Actions の Secrets の設定について

BigQuery の実行のために認証が必要なので、そのための設定を行う必要があります。

私が行った方法は **GitHub Actions の公式ドキュメントでは非推奨なので、設定する場合は自己責任**でお願いします。

- 前述の認証に必要な JSON ファイルの内容を Base64 エンコードする
  - Mac であれば `$ cat XXX.json | base64 | pbcopy` でエンコードした内容をクリップボードにコピーできます
- GitHub のプロジェクトページを開き、`Settings` > `Secrets` で `New repository secrets` ボタンを押し、`Name` に`BIGQUERY_SERVICE_ACCOUNT_JSON_BASE64`、`Value` にはクリップボードにコピーされた値を貼り付けて `Add secret` ボタンを押す

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/190187/628ff2ed-67e3-3bb6-e057-d6770f527a9e.png" width=640>


これで設定は完了です。

上記の yml ファイルではスクリプト実行前にこの Secret の内容を Base64 デコードし、その内容を JSON ファイルとして書き出しています。

## スクリプトの実行

上記の yml ファイルの `Run ruby script` の部分で作成した Ruby スクリプトを実行しています。

`env` の部分で `GOOGLE_APPLICATION_CREDENTIALS` という名前で JSON ファイルのパスを環境変数として設定しています。

スクリプトの実行の結果で出力された文字列は Slack に通知するために `SLACK_MESSAGE` という名前の環境変数に保存するようにしておきます。（後述）


## Slack への通知

最後に Slack へ通知しますが、GitHub Actions のマーケットプレイスにある [Slack Notify](https://github.com/marketplace/actions/slack-notify) を使用しました。

yml ファイルを見てわかるかと思いますが、色々と環境変数の設定が必要になります。

先ほど環境変数で設定した `SLACK_MESSAGE` は Slack 通知が行われる際のメッセージの設定になります。

以上で設定は全て完了になります。

# 最後に

今までは Firebase Analytics の管理画面をからアプリバージョンごとのユーザー比率を見ていたのですが、この仕組みを導入したことによってその手間がなくなりました。

どなたかの参考になれば幸いです。

# 参考

- サービスアカウントの認証について
  - https://cloud.google.com/bigquery/docs/authentication/service-account-file?hl=ja
  - https://cloud.google.com/docs/authentication/production
- BigQuery にエクスポートされる Firebase アナリティクスのデータについて
  - https://support.google.com/firebase/answer/7029846?hl=ja
- Ruby での BigQuery のサンプル
  - https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-bigquery/samples/snippets/query.rb
- Slack Notify (GitHub Actions で Slack 通知を行うためのライブラリ)
  - https://github.com/marketplace/actions/slack-notify