$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

PullRequestの統計情報をBigQueryに送り、変更のリードタイムを柔軟に可視化する

以前PullRequestからチーム開発の生産性・健全性を測るCLIツールを書いてみた - $shibayu36->blog;のようなことをやってみたのだが、BigQueryにデータを送ることでもう少し柔軟に変更のリードタイム*1などを可視化してみたのでメモ。

BigQueryで可視化するためにやったことは3つ。

  • BigQueryにマージされたPullRequest情報を送るスクリプトを作成
  • 上記スクリプトGithub Actionsで動かし、定期的にデータを送る
  • BigQueryでViewを作り、Data Studioで可視化

BigQueryにマージされたPullRequest情報を送るスクリプトを作成

こういうイメージ。

import { execFileSync } from "child_process";
import { writeSync } from "fs";
import { BigQuery } from "@google-cloud/bigquery";
import { startOfToday, addDays, parseISO } from "date-fns";
import { fileSync } from "tmp";

// PullRequestの統計情報をBigQueryに入れるスクリプト
//
// 指定する環境変数
// - GITHUB_TOKEN: repoアクセスできるgithub token
// - GOOGLE_APPLICATION_CREDENTIALS: Google Cloud APIsを呼び出すための鍵ファイルへのパス
//     - BigQueryデータ編集者とBigQueryジョブユーザーのロールが必要
//     - See Also: https://cloud.google.com/docs/authentication/production?hl=ja
// - PROJECT_ID: Google Cloudのproject id
// - START_DATE(optional): 集計対象の始点。ISO8601形式。デフォルトは前日0:00
// - END_DATE(optional): 集計対象の終点。ISO8601形式。デフォルトは当日0:00
//
// 実行例
// GITHUB_TOKEN=... GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json START_DATE=2020-10-05 END_DATE=2020-10-12 yarn --silent ts-node dev-performance-stat/pull-request.ts

const QUERY = "org:microsoft";

async function main(): Promise<void> {
  const startDateISO = process.env.START_DATE;
  const endDateISO = process.env.END_DATE;
  const projectId = process.env.PROJECT_ID;

  const endDate = endDateISO ? parseISO(endDateISO) : startOfToday();
  const startDate = startDateISO ? parseISO(startDateISO) : addDays(endDate, -1);

  console.log("input parameters: ", {
    projectId,
    startDate,
    endDate,
  });

  const prLogs = execFileSync(
    "merged-pr-stat",
    [
      "log",
      "--start",
      startDate.toISOString(),
      "--end",
      endDate.toISOString(),
      "--query",
      QUERY,
      "--format",
      "csv",
    ],
    { encoding: "utf8" }
  );
  if (!prLogs) {
    console.log("No merged pull request");
    return;
  }

  // loadに渡せるように一時ファイルに書き込む
  const tmpFile = fileSync();
  writeSync(tmpFile.fd, prLogs);

  const bigquery = new BigQuery({
    projectId,
  });

  // 重複を防ぐため、先に該当範囲をDELETEしておく
  await bigquery.query({
    query: `DELETE FROM \`sample_dataset.github_pr_logs\`
              WHERE @startDate <= mergedAt
                AND mergedAt <= @endDate`,
    params: {
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
    },
  });

  const [job] = await bigquery
    .dataset("sample_dataset")
    .table("github_pr_logs")
    .load(tmpFile.name, {
      skipLeadingRows: 1,
      encoding: "UTF-8",
      autodetect: true,
    });

  console.log(`Job ${job.id} completed.`);
  const errors = job.status?.errors;
  if (errors && errors.length > 0) {
    throw errors;
  }
}

main().catch(error => {
  console.error(error);
  process.exit(1);
});

指定した時間範囲のPullRequest情報をBigQueryに送ってくれる。指定した時間範囲のBigQuery側のデータを削除してくれるようにして、何回実行しても重複が起こらないように工夫した。

このスクリプトによって、https://github.com/shibayu36/merged-pr-stat で出力されるものが自動でBigQueryのスキーマとして認識される。

f:id:shiba_yu36:20201025150938p:plain

上記スクリプトGithub Actionsで動かし、定期的にデータを送る

最近の定期実行はGithub Actionsで行っておくと便利。こんなイメージ。GitHubのsecretsには必要な情報を保存しておく。

name: dev-performance-stat-pull-request

on:
  schedule:
    - cron: 30 1 * * * # JST 10:30
  # 再実行やデバッグ用に手動でも実行可能にしておく
  workflow_dispatch:
    inputs:
      start_date:
        description: 集計対象の始点。ISO8601形式。
        required: false
      end_date:
        description: 集計対象の終点。ISO8601形式。
        required: false

env:
  TZ: "Asia/Tokyo"
jobs:
  pull-request-stat:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: "14"
      - run: yarn install
      # GCP_SA_KEYにGCPのキーファイルをbase64化したものを
      # 入れておけば、GCPの認証の設定をしてくれる。
      - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true
      - run: yarn --silent ts-node pull-request-to-bigquery.ts
        env:
          START_DATE: ${{ github.event.inputs.start_date }}
          END_DATE: ${{ github.event.inputs.end_date }}
          GITHUB_TOKEN: ${{ secrets.DEV_PERFORMANCE_GITHUB_TOKEN }}
      # 失敗に気付けるように、Slackに通知する
      - name: Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2.1.0
        if: failure()
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_COLOR: danger
          SLACK_MESSAGE: pull-request-to-bigquery Failed
          SLACK_USERNAME: GitHub Action
          SLACK_ICON_EMOJI: ":github:"

BigQueryでViewを作り、Data Studioで可視化

あとはBigQueryでSQLを使っていい感じに可視化するViewを作り、Data Studioで可視化する。例えば以下はleadTime(1commit〜マージまで)の中央値、timeToMerge(PR作成〜マージまで)の中央値、PullRequest数の28日移動平均を作るSQLである。

  WITH
    -- 対象範囲を出す
    start_and_end_days AS (
    SELECT
      MIN(DATE_TRUNC(DATE(mergedAt), DAY)) AS start_day,
      MAX(DATE_TRUNC(DATE(mergedAt), DAY)) AS end_day
    FROM
      sample-account.sample_dataset.github_pr_logs),

    -- 日付の集合を作る
    date_seq AS (
    SELECT
      date
    FROM
      UNNEST(GENERATE_DATE_ARRAY(
        (SELECT start_day FROM start_and_end_days),
        (SELECT end_day FROM start_and_end_days)
      )) AS date
    ORDER BY
      date),

    github_pr_logs AS (
    SELECT
      DATE_TRUNC(DATE(mergedAt), DAY) AS merged_at_trunc,
      github_pr_logs.*
    FROM
      sample-account.sample_dataset.github_pr_logs),

    -- 1日ごとに各指標を集計していく
    github_pr_logs_with_median AS (
    SELECT
      date_seq.date,
      PERCENTILE_CONT(github_pr_logs.leadTimeSeconds, 0.5) OVER (PARTITION BY date_seq.date) AS lead_time_median,
      PERCENTILE_CONT(github_pr_logs.timeToMergeSeconds, 0.5) OVER (PARTITION BY date_seq.date) AS time_to_merge_median,
      COUNT(*) OVER (PARTITION BY date_seq.date) AS pr_count,
      github_pr_logs.*
    FROM
      date_seq
    INNER JOIN
      github_pr_logs
    ON
      date_seq.date >= github_pr_logs.merged_at_trunc
      AND DATE(github_pr_logs.merged_at_trunc) >= DATE_SUB(date_seq.date, INTERVAL 28 DAY)
    WHERE
      -- チーム所属エンジニアで絞り込む
      github_pr_logs.author IN ("shibayu36", "engineer1", "engineer2") )

  SELECT
    date,
    MAX(pr_count) AS pr_count,
    -- hoursに直す
    MAX(lead_time_median) / 3600 AS lead_time_hours_median,
    MAX(time_to_merge_median) / 3600 AS time_to_merge_hours_median,
  FROM
    github_pr_logs_with_median
  GROUP BY
    date
  ORDER BY
    date DESC

このSQLを実行すると、このようなデータを取得できる。 f:id:shiba_yu36:20201025151746p:plain

これのデータを使ってDataStudioで折れ線グラフを定義してあげると、こんな感じで変更のリードタイムが可視化される。左上の日付コントローラを触ることで範囲を変えることも出来る。 f:id:shiba_yu36:20201025152114p:plain

まとめ

以前PullRequestからチーム開発の生産性・健全性を測るCLIツールを書いてみた - $shibayu36->blog;で集計していた時は、データを入れる段階で統計情報に加工してしまっていたので、可視化する段階では柔軟な調整が出来なかった。今回PullRequestの生ログを入れるようにしたことで、可視化の段階でauthorのフィルタをしたり、移動平均の範囲を決めたり、レポジトリを絞り込んだり出来るようになった。結構便利に開発チームのデリバリのパフォーマンスの1指標を見れるようになったので満足。

*1:この記事の変更のリードタイムはLeanとDevOpsの科学の指標と厳密に一致してないことに注意。この記事では1commit〜mergeまでの時間を変更のリードタイムと置いている