バイクアプリのデータポイント仕様書のフォーマットチェック機能を実装

# はじめに

本田技研工業でRoadSyncの分析を担当している石山です。
今回はTechブログ第五弾の投稿です。

前回までは主に開発チーム側の人が記事を投稿していましたが、分析チームもということで、今回は分析時に使うイベントログ周りで実施しているOps関連の話題を記事にしました。

ちなみに、私が所属しているグループは内製開発に必要なスキルを持っている人達が集められており、その中でさらに内製開発チームとデータ分析チームに分けられています。
今年の4月に組織改変を受けて立ち上がったばかりのグループなので、これからもっと連携を図っていきたいと思っています。

RoadSyncの概要説明はTechブログ第一弾に記載していますので、ぜひご覧ください。

[https://qiita.com/nakamuloud/items/a4fe2d3173b361bcb3fe](https://qiita.com/nakamuloud/items/a4fe2d3173b361bcb3fe)

# TL;DR

- RoadSyncではデータポイントの仕様書をYAMLで記述し、Git管理しています。
- GitHub Actionsで仕様書が期待通りのフォーマットになっているかチェックする機能を実装しました。
- 実装状況の変更し忘れやイベントの重複など見落としやすい部分がチェックできるようになり、仕様書の健全性を向上させることができました。

# データポイントとは？

Hondaでは、ユーザー操作などで発火するイベントとその時に収集する値（プロパティ）を総称してデータポイントと呼んでいます。

# データポイント仕様書のフォーマットをチェックするモチベーション

RoadSyncでは、ログ収集していたものの仕様書が存在しておらず、開発と分析の間でイベント定義に関する認識に齟齬がある状態がありました。
そこで、分析チームが主体となって各イベントを棚卸して整理し、仕様書にまとめることで齟齬が生まれにくい状態に改善していくことにしました。
仕様書をYAMLファイルにしてGit管理することで、差分チェックや問題が起こった場合のリカバリーをしやすくなることは良いのですが、一方で、200を超えるイベントを複数のチームを横断してデグレーションを起こさずに運用するのは結構難しいです。
そのため、GitHub Actionsを使ってPull Request時に期待するフォーマットになっているかチェックさせ、それが通らないとマージできない仕組みにしました。

# データポイントの仕様書

データポイントの仕様書をナビゲーションを開始した時に発火するStarted Navigationを例に解説します。
下が1つのイベントの仕様になっており、これが定義されているイベント数分書かれたYAMLファイルを仕様書とし、GitHubのリポジトリ上で管理しています。

```yaml
- status_android: リリース済
  status_ios: 設計済
  domain: 2. ナビゲーション
  event_name: Started Navigation
  event_category: 状態遷移
  event_def: （具体的な発火タイミングなどを記載する）
  initial_target: KPI/UX検証
  purpose: ナビゲーションの利用開始タイミングを計測するため。
  properties:
    - property_name: search_category
      property_def: 選択したナビゲーションカテゴリー
      type: STRING
      values:
        - value: voice_search
          value_def: 音声検索
    - property_name: action_source
      property_def: 入力種別（APP_UI：UI操作、HARDWARE：スイッチ操作等）
      type: STRING
  comment: （注意すべき点などをコメントとして記載する）
  release_version_android: 123456789
  release_version_ios: なし
```

status_androidとstatus_iosはOS毎にそのイベントが設計、実装、リリース、廃止のどの状況にあるのかを示し、リリース状態ならば対応するrelease_version_*にリリースしたバージョンが記録されます。
event_nameはそのログの名称、event_categoryは発火タイミングによってユーザー操作や状態遷移などのカテゴリ分けされています。
propertiesはそのイベントで収集したいデータを定義する項目です。
例えば、どのような経路でナビを開始したのかは興味の対象の1つなので、音声検索 (voice_search) など経路を示すsearch_categoryを設定しています。（もちろんvoice_search以外の値も設定していますが、例としてvoice_searchのみ紹介しています）
また、RoadSyncはもちろんスマホ画面上で操作できますが、バイクのハンドル部分に付いているスイッチでも操作できます。
運転時にハンドルスイッチで安全かつ快適にナビが使えることはRoadSyncが提供したい価値の1つなので、どの操作でナビを開始したのか (action_source) も取得しています。

このような仕様書を分析チームが主体となって開発チーム側とやりとりすることで運用しています。

# 確認する項目

仕様書のフォーマットチェック機能で確認したい項目は次の6つです。
最後のYAMLの仕様に沿っているか以外はpythonで内製しました。

1. イベント名に重複はないか
2. 設定したフィールドに抜け漏れが発生していないか、また、重複が許されないフィールドで重複が発生していないか
3. イベントのカテゴリは予め決められたものに分類されているか
4. ステータスに合わせて適切なリリースバージョンが設定されているか
5. データポイントの仕様書はYAMLの仕様に沿っているか

ちなみに、YAMLの仕様チェックは[yamllint]([https://github.com/adrienverge/yamllint](https://github.com/adrienverge/yamllint))を利用したのですが、設定ファイルでチェックする項目を細かく指定できて便利でした。

# 実装

フォーマットチェックのエントリーポイントはこのような感じです。

確認する項目の1〜4はそれぞれEventNameUniqueChecker, ElementsChecker, EventCategoryChecker, StatusReleaseVersionPairsCheckerで実装されています。
コードからも分かるように、これらは各チェック機能はCheckerという抽象クラスの具象クラスとして定義すればよく、機能を追加するときに他へ影響を与えないように設計しました。

```python
import sys
from argparse import ArgumentParser, Namespace
from enum import IntEnum
from typing import List

from specification_checker.checker import Checker
from specification_checker.elements import ElementsChecker
from specification_checker.event_category import EventCategoryChecker
from specification_checker.event_names import EventNameUniqueChecker
from specification_checker.status_release import StatusReleaseVersionPairsChecker

class ExitCode(IntEnum):
    SUCCESS = 0
    UNEXPECTED_FORMAT = 1

def get_commandline_args() -> Namespace:
    """コマンドライン引数を受け取って返す

    Returns:
        Namespace: 受け取ったコマンドライン引数
    """
    parser = ArgumentParser()
    parser.add_argument("specification_path", help="データポイント仕様書のパス", type=str)
    return parser.parse_args()

def main():
    """データポイント仕様書チェックのエントリーポイント"""
    args = get_commandline_args()

    specification_path = args.specification_path
    checkers_list: List[Checker] = [
        EventNameUniqueChecker(specification_path),
        ElementsChecker(specification_path),
        StatusReleaseVersionPairsChecker(specification_path),
        EventCategoryChecker(specification_path),
    ]

    is_success = True
    for checker in checkers_list:
        result = checker.check()
        if not result.is_ok:
            is_success = False
        print(result)
        print()

    if is_success:
        sys.exit(ExitCode.SUCCESS)
    else:
        sys.exit(ExitCode.UNEXPECTED_FORMAT)

if __name__ == "__main__":
    main()
```

これは開発時に試したアウトプット例ですが、このようにNGが出た確認項目はどのイベントで問題があったのかを表示してくれます。
一部の確認項目では、これだけだと分かりづらいという指摘もありましたが、それは今後の課題として残しています。

```bash
$ python -m specification_checker.main tests/data/event_list_phase_release_failure.yaml 
event name unique: OK

missing or unexpected elements: OK

phase and release version: NG
Tapped Cancel Destination Voice Search Button
Changed Place Favorites Setting
```

このエントリーポイントの実行を.github/workflow/ci.yamlに入れてあげます。
yamllintは独立したコマンドとして使えるので、上のエントリーポイントとは別で実行しています。

```yaml
name: code check

on: [pull_request]

jobs:
  test:

    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["<希望のpythonバージョン>"]

    steps:
      - uses: actions/checkout@v3
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install Poetry
        run: |
          curl -sSL https://install.python-poetry.org | python3 -
          echo "$HOME/.poetry/bin" >> $GITHUB_PATH
      - name: Poetry Version
        run: |
          poetry --version
      - name: Install dependencies with poetry
        run: |
          poetry install --no-interaction
      - name: YAML Linter
        run: |
          poetry run yamllint .
      - name: Check datapoints specification format
        run: |
          poetry run python -m specification_checker.main <データポイント仕様書のPATH>
```

なお、GitHub Actionsだけではフォーマットチェックの結果の確認・修正を行うのはとても時間がかかるので、同じ内容のチェックをpre-commitでpush前に実行するフローも入れています。
pre-commitとGitHub Actionsの内容が重複して無駄なのでは？というご指摘もありそうですが、pre-commitは各作業者が忘れずに設定することが前提になるので、GitHub Actionsを最後の砦として利用しています。

# できたもの

実際にGitHub Actionsが動いた場合の例です。
実装直後は結構な数のミスが見つかりました…
なので、チェック機能によって仕様書の健康状態が上がったと考えています。
![GitHub Actionsの実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218401/1b60fb18-fd4a-1c68-ab82-02b710232893.png)

# まとめ

- イベントログの仕様書をYAMLで記述し、Git管理するようにしました。
- データポイント仕様書のフォーマットチェック機能を実装し、GitHub ActionsでCIを設定しました。
- 人手でチェックする場合に比べて、仕様書の健全性を上げることができました。