$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までの時間を変更のリードタイムと置いている

「知的戦闘力を高める独学の技法」読んだ

独学は、1.戦略、2.インプット、3.抽象化・構造化、4.ストックという流れがあるんだよということを教えてくれて中々面白かった。

読書ノート

* 独学は、1.戦略、2.インプット、3.抽象化・構造化、4.ストックという流れ 287
    * 1. どのようなテーマを学習するか
    * 2. テーマに基づいて情報をインプット
    * 3. 知識を抽象化・他のものと結合し、自分なりのユニークな示唆・洞察・気づきを得る
    * 4. 知識と示唆・洞察・気づきをセットで検索可能なところに整理
* テーマを決めておき、ジャンルは横断して学ぶ 613
* 本を読んでいて強い反感や嫌悪感を覚えるものもメモする 999
* 見識ある人物に聞くのが最も効率の良いインプット 1276
* 転記は5箇所、多くても9箇所 1744
* 最初にハイライト、次にハイライトを読み直してやはり面白いところに別色ハイライト、後々参照しそうなら転記 1781
* 転記するなら、面白かった事実、ビジネスや実生活に対する示唆、具体的な行動の仮設の3点セットで 1837
* 経営学、心理学、脳科学、宗教、自然科学のおすすめ書籍 2340

TypeScriptの型を手に馴染ませるためにやっていること

最近TypeScriptが好きで勉強していっている。しかしなかなか型定義周りが手に馴染まず、少し複雑な型定義を読んだり、自分でユーティリティ型を定義したりすることが難しかった。

そこで型を手に馴染ませるために色々学習をしてみたので、やっていることをメモしておく。

まずざっとTypeScriptの型概要を学ぶ

まずTypeScriptでの型を簡単に学ぶには以下の2つの資料がわかりやすかった。

ひたすら型演習をする

資料を読むだけでは全く手に馴染まないと思ったので、その後ひたすら型演習をしている。

まずは TypeScriptの型演習 - Qiita 。これは先程の型初級、型入門の記事を書いた人が演習問題を作っているため同じ流れで学習でき、さらに解説編も充実しているので、手を動かしながら学ぶのに最適であった。これを自分で二周解いてみた。

1周目に比べて、2周目では解ける問題が増え、解く速度も上がった。いい感じに復習効果で定着してきているので、周回してみたのは良かったと思う。もう1周くらいやってみても良さそう。

さらにtype-challengesの演習問題も解いてみている。こちらはまだ1周目のmediumまで。こっちも何回か周回してみると良さそう。

面白かった話題

union distribution

union distributionは頻出なんだけど、めちゃくちゃ難しかった。https://qiita.com/uhyo/items/da21e2b3c10c8a03952f#conditional-type%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bunion-distribution にわかりやすい解説がある。

自分の中では、union型でforループやfilter、mapが出来て便利だね、という理解に落ち着いた(全然理解が正確では無いと思うけど...)。

-?を使ってフィールドをundefinedで無くす

こういう書き方が出来る。

type Type1<Obj> = {
  [Key in keyof Obj]-?: Obj[Key]
};
type Type2 = Type1<{
  hoge?: number;
  fuga: string;
  piyo?: undefined;
}>;

// 以下のような定義になる
// type Type2 = {
//     hoge: number;
//     fuga: string;
//     piyo: never;
// }

型レベルで検証する型を作れる

https://github.com/type-challenges/type-challenges/blob/f9b3bb0b5f7e9095be840fe3cba154f6f0566693/utils/index.d.ts が面白かった。こういう型を作ることで、

type ExpectHogeAndFugaEqual = Expect<Equal<Hoge, Fuga>>;

のように書いておくと、HogeとFugaの型が一致するなら型チェックが通り、HogeとFugaの型が一致しないなら型エラーで落ちてくれる。

まとめ

今回はTypeScriptの型を手に馴染ませるためにやっていることについてメモしてみた。徐々に型定義ファイルを読めるようになってきていて、自分でもちょっとした型を作れるようにもなってきているので、このまま演習を続けていきたい。