ServiceNowのAgile BurnDownChartを見やすくした

  • このエントリーをはてなブックマークに追加
  • Pocket

ServiceNow ITBMのアジャイル開発では、登録したストーリの進捗状況からBurnDownChartを表示することができます。
しかし、私達のチームがそれを使うにはいくつかの問題があったため、ServiceNowのREST APIから指定スプリントの情報を取得し、チャートを描画しSlack投稿するPythonプログラムを作成しました。

いくつかの問題
・土日祝休日も仕事をするような理想線になっている
・チャートを見るのにもたつく
・デイリーで関係者に共有したい(ロールの有無によらず)

ServiceNowのBurnDownChartはこの様な感じです。
Feb 8,9,11は休日なのでポイントは消化されないべきですが、理想線(青色)が一直線に引かれています。
これでは進捗が遅れているのか一目で分かりにくいです。

今回作成したプログラムで出力したBurnDownChartです。
土日祝日はポイントが消化されない様になっています、

これが定期的(自動ではない)にSlackに投稿される仕組みです。

ソースコード

import pprint
import requests
import jpholiday
import datetime
import matplotlib.dates as mdates
import pandas as pd
import matplotlib.pyplot as plt
import urllib.request
import urllib.parse
import json
import datetime
from pytz import timezone
point_dict = {}

# --------- #
# 変数初期化 #
# --------- #
total_points = 0
done = 0
undone = 0
BASIC = 'Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
SLACK_TOKEN = "xoxb-123456789012-123456789012-XXXXXXXXXXXXXXXXXXXXXXXX"
SLACK_CHANNEL = "XXXXXXXXX"
SPRINT = "XX"

# ----------- #
# Sprintを取得 #
# ----------- #
params = {
    'sysparm_query': 'short_description=Sprint ' + SPRINT
}
param = urllib.parse.urlencode(params)
url = "https://{your instance name}.service-now.com/api/now/table/rm_sprint?" + param
req = urllib.request.Request(url)
req.add_header("authorization", BASIC)
with urllib.request.urlopen(req) as res:
    r = res.read().decode("utf-8")
obj = json.loads(r)
# Sprintの開始日と終了日を取得
start_date = obj['result'][0]['start_date']
start_date = (datetime.datetime.strptime(start_date, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(hours=9)).date()
print(start_date)
end_date = obj['result'][0]['end_date']
end_date = (datetime.datetime.strptime(end_date, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(hours=9)).date()
# ポイント配列の初期化
while start_date <= end_date:
    point_dict[str(start_date)] = 0
    start_date = start_date + datetime.timedelta(days=1)

# ----------- #
# Storyを取得 #
# ----------- #
params = {
    'sysparm_query': 'sprint.short_descriptionLIKESprint ' + SPRINT
}
p = urllib.parse.urlencode(params)
url = "https://{your instance name}.service-now.com/api/now/table/rm_story?" + p
req = urllib.request.Request(url)
req.add_header("authorization", BASIC)
with urllib.request.urlopen(req) as res:
    r = res.read().decode("utf-8")
obj = json.loads(r)
# ストーリーでループ
for name in obj['result']:
    if len(name['story_points']) > 0:
        total_points += int(name['story_points'])
        if name['closed_at'] != '':
            close_date = datetime.datetime.strptime(
                name['closed_at'], '%Y-%m-%d %H:%M:%S')
            close_date = close_date.date()
            if name['state'] == '3':
                if str(close_date) in point_dict:
                    point_dict[str(close_date)] += int(name['story_points'])
                else:
                    point_dict[str(close_date)] = int(name['story_points'])
        if name['state'] == '3':
            done += int(name['story_points'])
        else:
            undone += int(name['story_points'])
counta = 0
for i in point_dict.items():
    counta += int(i[1])
    point_dict[i[0]] = total_points - counta
plt.xkcd()
fig, ax = plt.subplots()
# 実績線を作成する
x = []
y = []
plt.ylim(0, total_points + 5)
counta = 0
for key in point_dict.keys():
    if datetime.datetime.today() >= datetime.datetime.strptime(key, '%Y-%m-%d'):
        x.append(datetime.datetime.strptime(key, '%Y-%m-%d'))
        y.append(point_dict[key])
# 祝日判定
DATE = "yyyymmdd"
def isBizDay(DATE):
    Date = datetime.date(int(DATE[0:4]), int(DATE[4:6]), int(DATE[6:8]))
    if Date.weekday() >= 5 or jpholiday.is_holiday(Date):
        return 0
    else:
        return 1

# 平日の日数を取得
total_BizDay = 0
for key in point_dict.keys():
    if isBizDay(key.replace('-', '')) == 1:
        total_BizDay += 1
# 理想線を作成する
x2 = []
y2 = []
point_dict_len = len(point_dict)
average = total_points / (total_BizDay - 1)
for key in point_dict.keys():
    dtm = datetime.datetime.strptime(key, '%Y-%m-%d')
    x2.append(dtm)
    y2.append(total_points)
    # 翌日が平日なら理想線を消化する
    if isBizDay((dtm + datetime.timedelta(days=1)).strftime("%Y%m%d")) == 1:
        total_points -= average
days = mdates.DayLocator()
daysFmt = mdates.DateFormatter('%m/%d')
ax.xaxis.set_major_locator(days)
ax.xaxis.set_major_formatter(daysFmt)
plt.title("Sprint" + SPRINT + " Burndown")
plt.plot(x2, y2, label="Ideal", color='green')
plt.plot(x2, y2, marker='.', markersize=20, color='green')
plt.plot(x, y, label="Actual", color='red')
plt.plot(x, y, marker='.', markersize=20, color='red')
plt.grid()
plt.xlabel("Days")
plt.ylabel("Remaining Effort(pts)")
plt.subplots_adjust(bottom=0.2)
plt.legend()
# グラフの表示
# plt.show()
# グラフの保存
plt.savefig('figure.png')
# --------- #
# Slack通知 #
# --------- #
files = {'file': open("figure.png", 'rb')}
param = {
    'token': SLACK_TOKEN,
    'channels': SLACK_CHANNEL,
    'filename': "burndown_" + datetime.datetime.now().strftime('%Y%m%d'),
    'initial_comment': datetime.datetime.now().strftime('%Y/%m/%d') + "のバーンダウンチャートをお届けします:crab:",
    'title': "burndown_" + datetime.datetime.now().strftime('%Y%m%d')
}
res = requests.post(url="https://slack.com/api/files.upload",params = param, files = files)
print(res)
pprint.pprint(res.json())

今回はServiceNowのAPIからストーリー情報を取得しバーンダウンチャートを描画しました。
データの取得、集計、表示が簡単にできるので、さらに活用が期待できそうです。

  • このエントリーをはてなブックマークに追加
  • Pocket

SNSでもご購読できます。

コメントを残す

*