📉

Notionで管理しているチケットのバーンダウンチャートを表示する

kinashi@ivry2023/01/25に公開

株式会社IVRy (アイブリー)のエンジニアのkinashiです。

IVRyではエンジニアの人数が増えてきたこともあり、昨年の11月からスクラム開発を導入しました。
Notionの ボードビュー を使って進捗を管理しています。

https://www.notion.so/ja-jp/help/boards

Notionは便利なツールですが、バーンダウンチャートを表示する機能は今のところありません。
デイリースクラムでバーンダウンチャートを確認したかったので、Notion APIGoogle Apps Script(以下GAS) を使って Googleスプレッドシート にバーンダウンチャートを表示してみました。

開発に使ったツール

"devDependencies": {
  "@types/google-apps-script": "^1.0.57",
  "rome": "^11.0.0",
  "typescript": "^4.9.4"
}

今回は TypeScript で書いたコードを tsc でコンパイルし、出力されたコードを GAS のスクリプトエディタにコピペするまごころあふれる方法でコードを反映しました。

romeでlintとformatをしています。

clasp を使うとコマンドでデプロイできるようになるみたいなので機会があれば試してみたいです。

https://github.com/google/clasp

データの取得

グラフを表示するために下記のデータを Notion API から取得します。

  • 現在のスプリントのDevチームのストーリーポイントの合計
  • 現在のスプリントのDevチームの完了したストーリーポイントの合計

今回はスプリント名を指定してデータを取得したかったので Query API を使用しました。

https://developers.notion.com/reference/post-database-query

fetchData

データ取得に必要なアクセストークンやデータベースIDなどはスクリプトのプロパティとして登録しておいて、 PropertiesService を使って読み込んでいます。
データ取得後に開発メンバーでフィルターしたデータを返却しています。

getPoint

取得したデータからポイントを算出します。

const fetchData = (sprintName: string) => {
  const token = PropertiesService.getScriptProperties().getProperty('TOKEN')
  const databaseId = PropertiesService.getScriptProperties().getProperty('DATABASE_ID')

  const payload = JSON.stringify({
    filter: {
      property: 'Sprint',
      select: {
        equals: sprintName,
      },
    },
  })

  const options = {
    headers: {
      Authorization: `Bearer ${token}`,
      'Notion-Version': '2022-06-28',
    },
    contentType: 'application/json',
    method: 'post' as const,
    payload,
  }

  const data = JSON.parse(
    UrlFetchApp.fetch(`https://api.notion.com/v1/databases/${databaseId}/query`, options).getContentText(),
  ) as Data

  const members = PropertiesService.getScriptProperties().getProperty('MEMBERS')?.split(',') ?? []

  return data.results.filter((result) =>
    result.properties.Assign.people?.some((people) => members.includes(people.person.email)),
  )
}

const getPoint = (data: ReturnType<typeof fetchData>) => data.reduce(
  (result, { properties }) => {
    const point = properties['Story Point'].number ?? 0
    const completed = properties.Status.select?.name === 'Completed' ? point : 0

    return {
      all: result.all + point,
      completed: result.completed + completed,
    }
  },
  {
    all: 0,
    completed: 0,
  },
)

初期値を出力する

ストーリーポイントは1日に1回計測することにし、次のような値を出力するようにしました。
日付と休日はスプリントの分だけ入力しておいて、理想線表示用の項目を埋めます。

日付 休日 理想 実績
start 100 100
1/13 80
1/14 80
1/15 80
1/16 60
1/17 40
1/18 20
1/19 0

init

初期化関数をスプレッドシートのメニューに登録して実行できるようにしておきます。

onOpen

onOpen という関数の中に処理をメニュー追加処理を書くことでスプレッドシートを開いたときにメニューが追加されます。

const init = () => {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
  const sprintName = sheet.getRange(SPRINT_NAME_RANGE).getValue() as string
  const data = fetchData(sprintName)
  const { all: allPoint } = getPoint(data)

// スプリント初めのSPを入力
sheet.getRange(`${IDEAL_COLUMN}${START_ROW}:${ACTUAL_COLUMN}${START_ROW}`).setValues([[allPoint, allPoint]])

  const lastRow = sheet.getLastRow()
  const holidaysCont = sheet
    .getRange(`${HOLIDAY_COLUMN}${START_ROW}:${HOLIDAY_COLUMN}${lastRow}`)
    .getValues()
    .flat()
    .filter((value) => !!value).length
  const workDaysCount = lastRow - START_ROW - holidaysCont

  for (let row = START_ROW + 1; row <= lastRow; row++) {
    const isHoliday = sheet.getRange(`${HOLIDAY_COLUMN}${row}`).getValue() !== ''
    const dayBeforeValue = sheet.getRange(`${IDEAL_COLUMN}${row - 1}`).getValue()

    // 休日の次の日は理想線のポイントを据え置き
    const value = isHoliday ? dayBeforeValue : dayBeforeValue - allPoint / workDaysCount

    sheet.getRange(`${IDEAL_COLUMN}${row}`).setValue(value)
  }
}

const onOpen = () => {
  SpreadsheetApp.getActiveSpreadsheet().addMenu('バーンダウン設定', [
    { name: '初期値入力', functionName: 'init' },
    // 手動でも実行できるようにしておく
    { name: '現在の実績を入力', functionName: 'setActualValue' },
  ])
}

日毎の値を出力する

あとは実績の値を出力すれば終わりです!
GASのトリガーを設定し、1日1回実行されるようにします。

const setActualValue = () => {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
  const sprintName = sheet.getRange(SPRINT_NAME_RANGE).getValue() as string
  const data = fetchData(sprintName)
  const { all, completed } = getPoint(data)

  const lastRow =
    START_ROW + sheet.getRange(`${ACTUAL_COLUMN}${START_ROW}:${ACTUAL_COLUMN}20`).getValues().filter(String).length
  sheet.getRange(`${ACTUAL_COLUMN}${lastRow}`).setValue(all - completed)
}

まとめ

Notion API と GAS を使うことでバーンダウンチャートを表示することができました。
少しの遊び心で日々の改善ができたのは良かったです。

今回書いたコードはGitHubに置いてあるので全体が見たい方はこちらでご確認ください。
https://github.com/kinashi/gas_burndown_chart

最後に、IVRyでは一緒に働く仲間を絶賛募集中です!
カジュアル面談もできるので、興味がある方はお気軽にご応募ください!

https://ivry-jp.notion.site/IVRy-e1d47e4a79ba4f9d8a891fc938e02271

IVRyテックブログ

電話DXのIVRy(アイブリー)を開発しているエンジニアのテックブログまとめです。

Discussion

ログインするとコメントできます