DOORS DX

ベストなDXへの入り口が
見つかるメディア

Pull Requestメトリクスの計測・収集と可視化

公開日
2024.06.03
更新日
2024.06.03
<エンジニアブログ>Pull Requestメトリクスの計測・収集と可視化

XaaSユニット クロスエンジニアリング所属の田中です。
エンジニアリング組織では、開発力の向上を目指し、日々様々な改善を行なっています。エンジニアリング組織のパフォーマンスを可視化することで現状把握、ボトルネックの把握、改善策の実行を行うことができます。

今回は、 Pull Request の指標を収集・計測をし組織のパフォーマンスを可視化しましたので、その事例を紹介します。

Pull Requestとは

Pull Requestとは、ソフトウェア開発において、プロジェクトの途中で変更・追加されたコードが、メインのコードベースに組み込まれる前のレビュープロセスのことです。

Pull Requestにて開発プロセスの効率を評価するために、次の3点のデータに注目することが重要だと考えられます。

  • PR数:この数値は、特定の期間における活動的な開発作業の量を示します。
    多いほど、多くの機能や修正が行われていることを意味します。
  • コメント数:PRに対するコメントの数を通じて、そのPRがどれだけ多くのレビューが必要だったのかがわかります。
    コメントが多い場合は、問題が複雑だったか、あるいはレビュー前の仕様説明が不足しているなどの可能性が考えられます。
  • 平均PRクローズ時間(日):PRが開始されてからクローズされるまでの平均日数を示し、プロジェクトの対応の速さを反映します。
    短い時間でクローズできることは、効率的な開発プロセスを示します。

これらのデータを可視化することで、開発プロセスのボトルネックがどこにあるか、どのように改善できるかを具体的に把握することができます。


概要

  • Pull Request の各指標を収集・計測
    • マージ済みPR数
    • コメント数
    • PR作成〜クローズまでの時間 (Primary 指標)
  • 計測対象期間
    • 2023/1/1 ~ 2023/12/31

収集・計測方法

Bitbucket →Goolge Cloud Platform

Bitbucket APIから取得し、GCSにrow data として保存します。GCS から Dataflowで整形を行い、各指標をBigQueryに保存します。最後にLooker Studioで可視化を行いました。

コード

Bitbucket APIから取得 → GCS

import axios from "axios";
import "dotenv/config";
import fs from "fs";
import { Storage } from "@google-cloud/storage";
const bitbucketApi = axios.create({
  baseURL: "https://api.bitbucket.org/2.0",
  timeout: 10000,
  headers: { Authorization: `Bearer ${process.env.BITBUCKET_ACCESS_TOKEN}` },
});
const repoSlug = process.env.REPO || "";
const workspaceId = process.env.WORKSPACE || "";
const bucketName = process.env.BUCKET_NAME || "";
const getPRs = async (repoSlug: string, workspaceId: string) => {
  let hasMore = true;
  let page = 1;
  let allPRs: Object[] = [];
  while (hasMore) {
    const response = await bitbucketApi.get(
      `/repositories/${workspaceId}/${repoSlug}/pullrequests`,
      {
        params: {
          state: "MERGED",
          page: page,
        },
      }
    );
    allPRs = allPRs.concat(response.data.values);
    hasMore = Boolean(response.data.next);
    page++;
    console.log("page", page);
  }
  return allPRs;
};
const uploadFileToGCS = async (bucketName: string, filePath: string) => {
  const storage = new Storage();
  const bucket = storage.bucket(bucketName);
  const fileName = filePath.split("/").pop();
  try {
    await bucket.upload(filePath, {
      destination: fileName,
    });
    console.log(`Success: Uploaded ${filePath} to ${bucketName}/${fileName}`);
  } catch (error) {
    console.error("Error in GCS upload:", error);
  }
};
const main = async () => {
  try {
    const prs = await getPRs(repoSlug, workspaceId);
    const ndJsonContent = prs.map((obj) => JSON.stringify(obj)).join("\n");
    const filePath = `data/${repoSlug}-full.ndjson`;
    fs.writeFileSync(filePath, ndJsonContent);
    console.log("Success: write ndJson");
    await uploadFileToGCS(bucketName, filePath);
  } catch (error) {
    console.error("Error", error);
  }
};
main();

Bitbucket APIでPRを一括で取得できないので、ページ毎に取得しています。

GCS → Dataflow

import json
import apache_beam as beam
from apache_beam.options.pipeline_options import PipelineOptions
pipeline_options = PipelineOptions(
    runner="DataflowRunner",
    project="your_project",
    temp_location="gs://your/backet-path/temp",
    region="asia-northeast1",
)
def flatten_json_array(json_str):
    json_arr = json.loads(json_str)
    return json_arr
def process_pr_data(pr_json_str):
    from datetime import datetime
    pr = json.loads(pr_json_str)
    created_on = datetime.fromisoformat(pr["created_on"].rstrip("Z"))
    updated_on = datetime.fromisoformat(pr["updated_on"].rstrip("Z"))
    duration_seconds = (updated_on - created_on).total_seconds()
    duration_days = duration_seconds / 86400
    return {
        "pr_id": pr["id"],
        "title": pr["title"],
        "author": pr["author"]["display_name"],
        "comment_count": pr["comment_count"],
        "created_on": created_on,
        "updated_on": updated_on,
        "duration_days": duration_days,
        "insertion_timestamp": datetime.utcnow(),
    }
schema = "pr_id:INTEGER, title:STRING, author:STRING, comment_count:INTEGER, created_on:TIMESTAMP, updated_on:TIMESTAMP, duration_days:FLOAT, insertion_timestamp:TIMESTAMP"
def run():
    with beam.Pipeline(options=pipeline_options) as pipeline:
        prs = (
            pipeline
            | "ReadFromGCS"
            >> beam.io.ReadFromText("gs://backet-path/path.ndjson")
            | "ProcessPRData" >> beam.Map(process_pr_data)
        )
        prs | "WriteToBigQuery" >> beam.io.WriteToBigQuery(
            "project:dataset-path.table-path",
            schema=schema,
            create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
            write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND,
        )
if __name__ == "__main__":
    run()
ジョブグラフ

結果

あるプロダクトのPull Requestに関する過去1年のデータを可視化した結果がこちらになります。

あるプロダクトのPull Requestの過去一年間の各データ

一般的にPR数は多く、平均PRクローズ時間は短い方がパフォーマンスが高い状態とされます。
また開発タスクがあまりない、アクティブではないプロジェクトの場合はPR数は減り、平均PRクローズ時間は増える傾向にありました。

平均PRクローズ時間(日)とPR数のグラフ

このグラフを例にすると①の期間はPR数が少なく(週20PR以下)、平均PRクローズ時間が長く(12日以上)良い状態とは言えません。
それに対し②の期間はPR数が多く(週50PR以上)、平均PRクローズ時間も短く(1.5日以下)パフォーマンスが高い状態と言えることがわかります。

①~②の期間では、”Pull Requestをできるだけ小さくする”、”レビューを優先度の高いタスクとする”、”レビュアーのボトルネックを解消する”などの改善取り組みを行いました。

その結果、パフォーマンスが向上していることがわかりやすくグラフから読み取ることができます。

まとめ

これらの取り組みの結果、組織のパフォーマンスをPull Requestの指標から評価することができました。個々の感覚ではなく、可視化することで現在の状態、改善の効果を客観的に把握することができました。

また”PRクローズまでの時間”に加え、”PR作成からレビューが開始されるまでの時間”や、”チケット開始からクローズまでの時間”など計測できるものはまだ多くあるので、より多角的にエンジニア組織のパフォーマンスを評価できるようにしていきたいと思います。


このページをシェアする

株式会社ブレインパッドについて

2004年の創業以来、「データ活用の促進を通じて持続可能な未来をつくる」をミッションに掲げ、データの可能性をまっすぐに信じてきたブレインパッドは、データ活用を核としたDX実践経験により、あらゆる社会課題や業界、企業の課題解決に貢献してきました。 そのため、「DXの核心はデータ活用」にあり、日々蓄積されるデータをうまく活用し、データドリブン経営に舵を切ることであると私達は考えています。

メールマガジン

Mail Magazine