Backlog × UiPath でメンバーごとの工数管理をした話

この記事は、[PERSOL PROCESS & TECHNOLOGY Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ppt) の 11 日目の記事です。

# はじめに

[Backlog](https://backlog.com/ja/) には「[実績時間](https://support-ja.backlog.com/hc/ja/articles/360035642814-%E8%AA%B2%E9%A1%8C%E3%81%AE%E5%B1%9E%E6%80%A7#%E5%AE%9F%E7%B8%BE%E6%99%82%E9%96%93)」という機能があり、課題ごとに費やした時間を記録できるようになっています。  
この実績時間は、あくまで "課題ごと" でしか記録 / 集計することができません。  
(メンバーみんなで 1 つのフィールド内に加算していくようなイメージ)
![スクリーンショット 2022-12-03 180339.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/a6a7f834-871b-1af8-2c87-3cbde1f46e63.png)  
この実績時間はコメントとして "誰が" "いつ" "どんな変更をしたのか" がすべて記録されます。  
しかし、これを個人別に集計して CSV ファイルに出力する機能は実装されていません。  
そこで、ちょうど UiPath の学習もしていたので、「勉強がてらに UiPath を使ってコメントの情報を取得してみよう！」と考えました。  

# Backlog API って？

https://developer.nulab.com/ja/docs/backlog/

Backlog 上でできる操作の大部分は、この Backlog API を使えば可能です。  
今回はこの中の「課題一覧の取得」「課題コメント数の取得」「課題コメントの取得」という 3 種類のリクエストを使っていきます。

https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list

https://developer.nulab.com/ja/docs/backlog/api/2/count-comment

https://developer.nulab.com/ja/docs/backlog/api/2/get-comment-list

# 実際に作成してみよう

UiPath で実際に開発をします。

## 作成環境

* UiPath Studio 2022.10.3
* Backlog プレミアム (スタンダード以上であれば OK)
* Excel for Microsoft 365 MSO (バージョン 2210 ビルド 16.0.15726.20188) 64 ビット

## おおまかなフロー構成

以下のような流れで処理を行います。

1. 取得する日付範囲を指定する
1. 指定した日付範囲の課題一覧を取得する
1. 取得した課題一覧内の 課題 ID をもとに課題コメント数を取得する
1. 課題のコメントを取得する
1. Excel ファイルに出力し、集計を行う

## 事前準備

### Backlog 側での準備 - API キーを取得する

Backlog ヘルプセンターに掲載されている手順 ([APIの設定](https://support-ja.backlog.com/hc/ja/articles/360035641754)) に沿って API キーを取得しておきます。

### UiPath 側での準備 - パッケージをインストールする

UiPath のプロジェクト内に、WebAPI を利用するためのパッケージをインストールしておきます。

* UiPath.WebAPI.Activities

## 1. 取得する日付範囲を指定する

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![A001_Backlog_取得日範囲設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/dcd41659-5618-e2dd-fb6b-1e29840f63d9.jpeg)

</details>

抽出開始日と抽出終了日を [入力ダイアログ] を使ってユーザーに指定させます。  
※ バリデーション処理を [文字列の一致をチェック] アクティビティを使って組み込んでいます。

~~~fundamental:正規表現オプション
"\d{4}-\d{2}-\d{2}"
~~~

## 2. 指定した日付範囲の課題一覧を取得する

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![A002_Backlog_課題一覧取得.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/85937602-1b41-cbe3-d4be-944effc711ba.jpeg)

</details>

ここでいよいよ Backlog の API を使います。

https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list

今回、Backlog への API リクエストを行う部分は別モジュール化しました。  
(部品化した)

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![Backlog_Get_Issue_List.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/63bd9073-9e6c-81dd-cc6a-b3d4c05a85c1.jpeg)

</details>

<details>
<summary>引数一覧</summary>

| 名前                | 方向 | 引数の型                      | 備考                                                                           |
|-------------------|----|---------------------------|------------------------------------------------------------------------------|
| in_Config         | 入力 | Dictionary<String,Object> | Excel で作った Config ファイルが中に格納。API キーやスペース ID (***.backlog.com の *** の部分) が入ってる |
| in_userId         | 入力 | Int32                     |                                                                              |
| in_count          | 入力 | Int32                     |                                                                              |
| in_order          | 入力 | String                    |                                                                              |
| in_projectId      | 入力 | List<Int32>               |                                                                              |
| in_issueTypeId    | 入力 | List<Int32>               |                                                                              |
| in_categoryId     | 入力 | List<Int32>               |                                                                              |
| in_versionId      | 入力 | List<Int32>               |                                                                              |
| in_milestoneId    | 入力 | List<Int32>               |                                                                              |
| in_statusId       | 入力 | List<Int32>               |                                                                              |
| in_priorityId     | 入力 | List<Int32>               |                                                                              |
| in_assigneeId     | 入力 | List<Int32>               |                                                                              |
| in_createdUserId  | 入力 | List<Int32>               |                                                                              |
| in_resolutionId   | 入力 | List<Int32>               |                                                                              |
| in_parentChild    | 入力 | Int32                     |                                                                              |
| in_attachment     | 入力 | String                    |                                                                              |
| in_sharedFile     | 入力 | String                    |                                                                              |
| in_sort           | 入力 | String                    |                                                                              |
| in_createdSince   | 入力 | String                    |                                                                              |
| in_createdUntil   | 入力 | String                    |                                                                              |
| in_updatedSince   | 入力 | String                    |                                                                              |
| in_updatedUntil   | 入力 | String                    |                                                                              |
| in_startDateSince | 入力 | String                    |                                                                              |
| in_startDateUntil | 入力 | String                    |                                                                              |
| in_dueDateSince   | 入力 | String                    |                                                                              |
| in_dueDateUntil   | 入力 | String                    |                                                                              |
| in_id             | 入力 | List<Int32>               |                                                                              |
| in_parentIssueId  | 入力 | List<Int32>               |                                                                              |
| in_keyword        | 入力 | String                    |                                                                              |
| in_offset         | 入力 | Int32                     |                                                                              |
| out_issueData     | 出力 | JArray                    |

</details>  
  
今回 API リクエストに使ったパラメーターは [offset] と [count]、[updatedSince] [updatedUntil] です。  

### offset

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/9bbf842d-9138-fe26-0fa6-c3e2a959d749.png)  
((引用 : [課題一覧の取得](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list)) でも offset の説明がない)  

この [課題一覧の取得] は 100 件単位でしか取得を行うことができず、ここで offset が効いてきます。  
offset は 〇 件目の項目から取得を開始するという設定値であり、"100" という数値をパラメーターとして設定すれば、"101" 件目から抽出が行われるというものです。

今回は、抽出したデータが 0 件となるまで繰り返し 100 件ずつ取得を継続するという仕組みにしています。

### 抽出したデータの扱い  

データは JSON 形式で吐き出されるようなのですが、この JSON を UiPath で扱う方法がイマイチわからず…。  
※ JArray だとか、JObject だとか…  
いろいろと模索をして、私は次のように理解しました。

~~~json:受け取るレスポンス例に合わせて
[ // JArray の内容
    {
        // JObject の内容
    }
]
~~~  

JArray が無事取り出せたら、JArray 内の JObject ごとに繰り返し処理を組み、リスト内に課題の ID を格納しました。  

## 3. 取得した課題一覧内の 課題 ID をもとに課題コメント数を取得する  

https://developer.nulab.com/ja/docs/backlog/api/2/count-comment

後述する [4. 課題のコメントを取得する] [5. Excel ファイルに出力し、集計を行う] も 1 つの xaml ファイルに組み込んでしまったのでまずはフロー図から。  
※ かなり繰り返し処理をネストさせることになってしまったのは少し反省点…。

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![A003_Backlog_コメント一覧取得.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/59e24b56-8caa-1bfa-6414-6d7eee643320.jpeg)

</details>  

ここでコメント数を取得するのは、実は意図があってのことなのです。  
後続の処理である [4. 課題のコメントを取得する] には 先ほどの [offset] というパラメーターが存在しないのです。  
(100 件ずつしか取得できないのに)  

ではどうしたのか、については後述の [4. 課題のコメントを取得する] で説明しますね。  

ひとまず Backlog からコメントの件数を取得する処理の xaml ファイルから。  

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![Backlog_Count_Comment.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/c8a14f1a-3122-a9fe-c576-3b0a730e9508.jpeg)

</details>  

<details>
<summary>引数一覧</summary>

| 名前              | 方向 | 引数の型                      | 備考                                                                           |
|-----------------|----|---------------------------|------------------------------------------------------------------------------|
| in_Config       | 入力 | Dictionary<String,Object> | Excel で作った Config ファイルが中に格納。API キーやスペース ID (***.backlog.com の *** の部分) が入ってる |
| in_issueIdOrKey | 入力 | String                    |                                                                              |
| out_issueData   | 出力 | Jobject                   |

</details>  
  
今回 API リクエストに使ったパラメーターは [issueIdOrKey] です。  

## 4. 課題のコメントを取得する  

https://developer.nulab.com/ja/docs/backlog/api/2/get-comment-list

さて、ここから怒涛のネスト地獄。  
JArray の中の JObject の中の中の中の・・・・・的な具合。  

まずは [課題コメントの取得] に使った xaml ファイルから。  

<details>
<summary>フロー図 (画面キャプチャー)</summary>

![Backlog_Get_Comment_List.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/ebbb725e-3e59-583b-2289-6082122a9bcf.jpeg)

</details>  

<details>
<summary>引数一覧</summary>

| 名前              | 方向 | 引数の型                      | 備考                                                                           |
|-----------------|----|---------------------------|------------------------------------------------------------------------------|
| in_Config       | 入力 | Dictionary<String,Object> | Excel で作った Config ファイルが中に格納。API キーやスペース ID (***.backlog.com の *** の部分) が入ってる |
| in_minId        | 入力 | Int32                     |                                                                              |
| in_maxId        | 入力 | Int32                     |                                                                              |
| in_count        | 入力 | Int32                     |                                                                              |
| in_order        | 入力 | String                    |                                                                              |
| in_issueIdOrKey | 入力 | String                    |                                                                              |
| out_issueData   | 出力 | JArray                    |

</details>  
  
今回 API リクエストに使ったパラメーターは [maxId] [count] [issueIdOrKey] です。  

### 課題コメント件数の取得 → 課題コメントの取得の大まかな流れ  

1. 格納するデータテーブルの初期化・列の追加
1. 課題 ID ごとの処理
   1. 課題 ID 内のコメントの件数を取得しておく
   1. (項番 0. 処理終了後) の件数が 1 件以上残っていれば最後に処理したコメント ID を [maxId] のパラメーターに設定する
   1. 課題コメントを取得 (100 件ごと)
   1. コメントごとの処理 
      1. データ行に課題 ID を代入
      1. jsonProperty ごとの処理
         1. jsonProperty の名前が "changeLog" であればその JSON 配列をデシリアライズ
            1. jsonObject ごとの処理
               1. jsonObject の field の値が "actualHours" であれば
                  1. データ行に実績時間の更新前の値、更新後の値、その差を代入
         1. jsonProperty の名前が "createdUser" であればその JSON をデシリアライズし、データ行にコメントの登録者 (createdUser) を代入
      1. データ行にコメントの登録日時を代入
      1. あらかじめ設定した日付範囲内であれば、データテーブルに格納
      1. [項番 2.1.] で取得したコメントの件数から 1 件引き算をする
   1. 残りのコメントが 1 件以上あれば、項番 2.2. に戻る

・・・という具合なのである。  
(もしもっといいやり方があれば教えてほしいです)  

### maxId って？

先ほど 100 件以上のデータを取得する際には offset を使った、が。  
今回の [課題コメントの取得] にはそんなパラメーターはない。  

そこで今回 [maxId] というパラメーターを使った。  
コメントの ID は数値になっており、既定ではコメント ID の降順で出力が行われる。  
そこで 100 件目のコメント ID を [maxId] に格納して、もう 1 度リクエストを行えば、101 件目以降の 100 件が取得されるってわけ。  

なるほど！！(自分で)  

## 5. Excel ファイルに出力し、集計を行う  

ごめんなさい。  
ここまで UiPath で頑張りましたが、ピボットテーブル絡みの機能は UiPath StudioX にしか実装されていないようで。  

VBA で実装しちゃいました。Excel ファイル内に。  
一応コードをば。  

~~~vb:pivotTableOperations()
Sub pivotTableOperations()

    Sheets("日次_個人別").Select
    ActiveWorkbook.PivotCaches.Create(xlDatabase, "元データ").CreatePivotTable "日次_個人別!R1C1"
        
    With ActiveSheet.PivotTables(1)
        .PivotFields("name").Orientation = xlRowField
        .PivotFields("updated").Orientation = xlColumnField
        .PivotFields("workload").Orientation = xlDataField
    End With
    
    Range("B2").Select
    Selection.Group Start:=True, End:=True, By:=1, Periods:=Array(False, False, False, True, False, False, False)

    Sheets("プロジェクト_個人別").Select
    ActiveWorkbook.Worksheets("日次_個人別").PivotTables(1).PivotCache.CreatePivotTable "プロジェクト_個人別!R1C1"
    
    With ActiveSheet.PivotTables(1)
        .PivotFields("name").Orientation = xlRowField
        .PivotFields("projectId").Orientation = xlColumnField
        .PivotFields("workload").Orientation = xlDataField
    End With
    
    Sheets("日次_プロジェクト別").Select
    ActiveWorkbook.Worksheets("日次_個人別").PivotTables(1).PivotCache.CreatePivotTable "日次_プロジェクト別!R1C1"
    
    With ActiveSheet.PivotTables(1)
        .PivotFields("projectId").Orientation = xlRowField
        .PivotFields("updated").Orientation = xlColumnField
        .PivotFields("workload").Orientation = xlDataField
    End With
    
    Range("B2").Select
    Selection.Group Start:=True, End:=True, By:=1, Periods:=Array(False, False, False, True, False, False, False)

End Sub
~~~ 

## 出力結果

![スクリーンショット 2022-12-04 145141.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/fef0f400-e4b7-3693-f8f7-ea2849ea2275.png)

個人別で日付ごとの工数を出すことができました。  

## Backlog API の制限

https://backlog.com/ja/blog/backlog-api-rate-limit-announcement/

| APIの種類 | 有料プラン      | フリープラン    |
|--------|------------|-----------|
| 読み込み   | 600 回/分 以内 | 60 回/分 以内 |
| 更新     | 150 回/分 以内 | 15 回/分 以内 |
| 検索     | 150 回/分 以内 | 15 回/分 以内 |
| アイコン取得 | 60 回/分 以内  | 6 回/分 以内  |

Backlog API には一定のレート制限があり、これに伴うエラーハンドリングを行わなければいけません。  

今回使ったリクエストでいえば (有料プランの場合)、「課題一覧の取得」は「検索」にあたり、150 回/分  
「課題コメント数の取得」「課題コメントの取得」は「読み込み」にあたり、600 回/分という制限になります。

具体的には、レート制限に引っかかった際には "429" というステータスコードが返却されるため、"429" の際には 60 秒待つ挙動を追加しています。  

![1Backlog_Count_Comment.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/0990e9ad-a49b-e405-2530-0f6b00077bdc.jpeg)

繰り返し (後判定) で "429" が返却されたときは再度取得を試みるようにして、  

![条件分岐 (if)_429エラー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583721/5d418697-b363-e0e6-da84-6d37a59c0d06.jpeg)

繰り返した頭には、"429" の時は 60 秒待機させるような処理を組み込めば出来上がり。  

# まとめ

これで個人別での工数管理が Backlog と UiPath で完結できます。  
もちろん、メンバーの入力の精度に依存してしまう部分はあるので、精緻なデータにならないときもありますが、簡単に取り出せるのでこれはこれでアリだな、といった具合です。  

なお、あくまで Backlog の標準機能ではないため、ご利用の際は自己責任でお願いします。

あと、はじめて Qiita に投稿してみましたが、これを機にもっと自分の知識整理のためにも投稿してみようかな、などと思ったり。  
ここまでお読みいただきありがとうございました。  

明日の [PERSOL PROCESS & TECHNOLOGY Advent Calendar 2022](https://qiita.com/advent-calendar/2022/ppt) もお楽しみに！